Merge "Revert "Disable failing tests as cause is investigated"" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 1c6df75..7e6c30f 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -54,6 +54,7 @@
":android.service.controls.flags-aconfig-java{.generated_srcjars}",
":android.service.dreams.flags-aconfig-java{.generated_srcjars}",
":android.service.notification.flags-aconfig-java{.generated_srcjars}",
+ ":android.service.appprediction.flags-aconfig-java{.generated_srcjars}",
":android.service.voice.flags-aconfig-java{.generated_srcjars}",
":android.speech.flags-aconfig-java{.generated_srcjars}",
":android.systemserver.flags-aconfig-java{.generated_srcjars}",
@@ -125,6 +126,7 @@
"android.provider.flags-aconfig",
"android.security.flags-aconfig",
"android.server.app.flags-aconfig",
+ "android.service.appprediction.flags-aconfig",
"android.service.autofill.flags-aconfig",
"android.service.chooser.flags-aconfig",
"android.service.controls.flags-aconfig",
@@ -726,6 +728,19 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+// App prediction
+aconfig_declarations {
+ name: "android.service.appprediction.flags-aconfig",
+ package: "android.service.appprediction.flags",
+ srcs: ["core/java/android/service/appprediction/flags/*.aconfig"],
+}
+
+java_aconfig_library {
+ name: "android.service.appprediction.flags-aconfig-java",
+ aconfig_declarations: "android.service.appprediction.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Controls
aconfig_declarations {
name: "android.service.controls.flags-aconfig",
@@ -855,6 +870,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "device_policy_aconfig_flags_lib_host",
+ aconfig_declarations: "device_policy_aconfig_flags",
+ host_supported: true,
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
cc_aconfig_library {
name: "device_policy_aconfig_flags_c_lib",
aconfig_declarations: "device_policy_aconfig_flags",
diff --git a/OWNERS b/OWNERS
index 935b768..6bab92b 100644
--- a/OWNERS
+++ b/OWNERS
@@ -40,4 +40,6 @@
per-file PERFORMANCE_OWNERS = file:/PERFORMANCE_OWNERS
-per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS
\ No newline at end of file
+per-file PACKAGE_MANAGER_OWNERS = file:/PACKAGE_MANAGER_OWNERS
+
+per-file WEAR_OWNERS = file:/WEAR_OWNERS
diff --git a/Ravenwood.bp b/Ravenwood.bp
index d13c4d7..2babf6a 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -33,6 +33,7 @@
"@$(location ravenwood/ravenwood-standard-options.txt) " +
"--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+ "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
"--out-impl-jar $(location ravenwood.jar) " +
@@ -56,6 +57,7 @@
"hoststubgen_dump.txt",
"hoststubgen_framework-minus-apex.log",
+ "hoststubgen_framework-minus-apex_stats.csv",
],
visibility: ["//visibility:private"],
}
@@ -94,14 +96,19 @@
android_ravenwood_libgroup {
name: "ravenwood-runtime",
libs: [
- "framework-minus-apex.ravenwood",
- "hoststubgen-helper-runtime.ravenwood",
- "hoststubgen-helper-framework-runtime.ravenwood",
+ // Prefixed with "200" to ensure it's sorted early in Tradefed classpath
+ // so that we provide a concrete implementation before Mainline stubs
+ "200-kxml2-android",
"all-updatable-modules-system-stubs",
+ "android.test.mock.ravenwood",
+ "framework-minus-apex.ravenwood",
+ "hoststubgen-helper-framework-runtime.ravenwood",
+ "hoststubgen-helper-runtime.ravenwood",
+
+ // Provide runtime versions of utils linked in below
"junit",
"truth",
"ravenwood-junit-impl",
- "android.test.mock.ravenwood",
"mockito-ravenwood-prebuilt",
"inline-mockito-ravenwood-prebuilt",
],
diff --git a/WEAR_OWNERS b/WEAR_OWNERS
index 4127f99..da8c83e 100644
--- a/WEAR_OWNERS
+++ b/WEAR_OWNERS
@@ -10,3 +10,4 @@
rwmyers@google.com
nalmalki@google.com
shijianli@google.com
+latkin@google.com
diff --git a/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java b/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java
index 123b2ee..0fd2449 100644
--- a/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java
+++ b/apct-tests/perftests/core/src/android/view/HandwritingInitiatorPerfTest.java
@@ -21,8 +21,11 @@
import static android.view.MotionEvent.ACTION_UP;
import static android.view.MotionEvent.TOOL_TYPE_FINGER;
import static android.view.MotionEvent.TOOL_TYPE_STYLUS;
+import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
+import static org.junit.Assume.assumeFalse;
+
import android.app.Instrumentation;
import android.content.Context;
import android.perftests.utils.BenchmarkState;
@@ -186,6 +189,7 @@
@Test
public void onInputConnectionCreated() {
+ assumeFalse(initiationWithoutInputConnection());
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
final View view = new View(mContext);
final EditorInfo editorInfo = new EditorInfo();
@@ -199,6 +203,7 @@
@Test
public void onInputConnectionClosed() {
+ assumeFalse(initiationWithoutInputConnection());
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
final View view = new View(mContext);
while (state.keepRunning()) {
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 7284f47..7de6799 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -2354,9 +2354,9 @@
if (maxExecutionDelayMillis - windowStart < MIN_ALLOWED_TIME_WINDOW_MILLIS) {
if (enforceMinimumTimeWindows
&& Flags.enforceMinimumTimeWindows()) {
- throw new IllegalArgumentException("Jobs with a deadline and"
- + " functional constraints cannot have a time window less than "
- + MIN_ALLOWED_TIME_WINDOW_MILLIS + " ms."
+ throw new IllegalArgumentException("Time window too short. Constraints"
+ + " unlikely to be satisfied. Increase deadline to a reasonable"
+ + " duration."
+ " Job '" + service.flattenToShortString() + "#" + jobId + "'"
+ " has delay=" + windowStart
+ ", deadline=" + maxExecutionDelayMillis);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 6883d18..aec464d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -241,6 +241,8 @@
private static final long MAX_TIME_WINDOW_MS = 24 * HOUR_IN_MILLIS;
private final JobScoreBucket[] mScoreBuckets = new JobScoreBucket[NUM_SCORE_BUCKETS];
private int mScoreBucketIndex = 0;
+ private long mCachedScoreExpirationTimeElapsed;
+ private int mCachedScore;
public void addScore(int add, long nowElapsed) {
JobScoreBucket bucket = mScoreBuckets[mScoreBucketIndex];
@@ -248,10 +250,17 @@
bucket = new JobScoreBucket();
bucket.startTimeElapsed = nowElapsed;
mScoreBuckets[mScoreBucketIndex] = bucket;
+ // Brand new bucket, there's nothing to remove from the score,
+ // so just update the expiration time if needed.
+ mCachedScoreExpirationTimeElapsed = Math.min(mCachedScoreExpirationTimeElapsed,
+ nowElapsed + MAX_TIME_WINDOW_MS);
} else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS) {
// The bucket is too old.
bucket.reset();
bucket.startTimeElapsed = nowElapsed;
+ // Force a recalculation of the cached score instead of just updating the cached
+ // value and time in case there are multiple stale buckets.
+ mCachedScoreExpirationTimeElapsed = nowElapsed;
} else if (bucket.startTimeElapsed
< nowElapsed - MAX_TIME_WINDOW_MS / NUM_SCORE_BUCKETS) {
// The current bucket's duration has completed. Move on to the next bucket.
@@ -261,16 +270,26 @@
}
bucket.score += add;
+ mCachedScore += add;
}
public int getScore(long nowElapsed) {
+ if (nowElapsed < mCachedScoreExpirationTimeElapsed) {
+ return mCachedScore;
+ }
int score = 0;
final long earliestElapsed = nowElapsed - MAX_TIME_WINDOW_MS;
+ long earliestValidBucketTimeElapsed = Long.MAX_VALUE;
for (JobScoreBucket bucket : mScoreBuckets) {
if (bucket != null && bucket.startTimeElapsed >= earliestElapsed) {
score += bucket.score;
+ if (earliestValidBucketTimeElapsed > bucket.startTimeElapsed) {
+ earliestValidBucketTimeElapsed = bucket.startTimeElapsed;
+ }
}
}
+ mCachedScore = score;
+ mCachedScoreExpirationTimeElapsed = earliestValidBucketTimeElapsed + MAX_TIME_WINDOW_MS;
return score;
}
@@ -378,10 +397,16 @@
@Override
public void prepareForExecutionLocked(JobStatus jobStatus) {
+ if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+ // Don't include jobs for the TOP app in the score calculation.
+ return;
+ }
// Use the job's requested priority to determine its score since that is what the developer
// selected and it will be stable across job runs.
- final int score = mFallbackFlexibilityDeadlineScores
- .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100);
+ final int priority = jobStatus.getJob().getPriority();
+ final int score = mFallbackFlexibilityDeadlineScores.get(priority,
+ FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES
+ .get(priority, priority / 100));
JobScoreTracker jobScoreTracker =
mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
if (jobScoreTracker == null) {
@@ -394,6 +419,10 @@
@Override
public void unprepareFromExecutionLocked(JobStatus jobStatus) {
+ if (jobStatus.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) {
+ // Jobs for the TOP app are excluded from the score calculation.
+ return;
+ }
// The job didn't actually start. Undo the score increase.
JobScoreTracker jobScoreTracker =
mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName());
@@ -401,8 +430,10 @@
Slog.e(TAG, "Unprepared a job that didn't result in a score change");
return;
}
- final int score = mFallbackFlexibilityDeadlineScores
- .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100);
+ final int priority = jobStatus.getJob().getPriority();
+ final int score = mFallbackFlexibilityDeadlineScores.get(priority,
+ FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES
+ .get(priority, priority / 100));
jobScoreTracker.addScore(-score, sElapsedRealtimeClock.millis());
}
@@ -649,21 +680,24 @@
(long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2),
mMaxRescheduledDeadline);
}
+
+ // Intentionally use the effective priority here. If a job's priority was effectively
+ // lowered, it will be less likely to run quickly given other policies in JobScheduler.
+ // Thus, there's no need to further delay the job based on flex policy.
+ final int jobPriority = js.getEffectivePriority();
+ final int jobScore =
+ getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed);
+ // Set an upper limit on the fallback deadline so that the delay doesn't become extreme.
+ final long fallbackDurationMs = Math.min(3 * mFallbackFlexibilityDeadlineMs,
+ mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs)
+ + mFallbackFlexibilityAdditionalScoreTimeFactors
+ .get(jobPriority, MINUTE_IN_MILLIS) * jobScore);
+ final long fallbackDeadlineMs = earliest + fallbackDurationMs;
+
if (js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME) {
- // Intentionally use the effective priority here. If a job's priority was effectively
- // lowered, it will be less likely to run quickly given other policies in JobScheduler.
- // Thus, there's no need to further delay the job based on flex policy.
- final int jobPriority = js.getEffectivePriority();
- final int jobScore =
- getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed);
- // Set an upper limit on the fallback deadline so that the delay doesn't become extreme.
- final long fallbackDeadlineMs = Math.min(3 * mFallbackFlexibilityDeadlineMs,
- mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs)
- + mFallbackFlexibilityAdditionalScoreTimeFactors
- .get(jobPriority, MINUTE_IN_MILLIS) * jobScore);
- return earliest + fallbackDeadlineMs;
+ return fallbackDeadlineMs;
}
- return js.getLatestRunTimeElapsed();
+ return Math.max(fallbackDeadlineMs, js.getLatestRunTimeElapsed());
}
@VisibleForTesting
@@ -976,7 +1010,8 @@
// Something has gone horribly wrong. This has only occurred on incorrectly
// configured tests, but add a check here for safety.
Slog.wtf(TAG, "Got invalid latest when scheduling alarm."
- + " Prefetch=" + js.getJob().isPrefetch());
+ + " prefetch=" + js.getJob().isPrefetch()
+ + " periodic=" + js.getJob().isPeriodic());
// Since things have gone wrong, the safest and most reliable thing to do is
// stop applying flex policy to the job.
mFlexibilityTracker.setNumDroppedFlexibleConstraints(js,
@@ -991,7 +1026,7 @@
if (DEBUG) {
Slog.d(TAG, "scheduleDropNumConstraintsAlarm: "
- + js.getSourcePackageName() + " " + js.getSourceUserId()
+ + js.toShortString()
+ " numApplied: " + js.getNumAppliedFlexibleConstraints()
+ " numRequired: " + js.getNumRequiredFlexibleConstraints()
+ " numSatisfied: " + Integer.bitCount(
@@ -1199,11 +1234,11 @@
DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
.put(PRIORITY_MAX, 0);
DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
- .put(PRIORITY_HIGH, 4 * MINUTE_IN_MILLIS);
+ .put(PRIORITY_HIGH, 3 * MINUTE_IN_MILLIS);
DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
- .put(PRIORITY_DEFAULT, 3 * MINUTE_IN_MILLIS);
+ .put(PRIORITY_DEFAULT, 2 * MINUTE_IN_MILLIS);
DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
- .put(PRIORITY_LOW, 2 * MINUTE_IN_MILLIS);
+ .put(PRIORITY_LOW, 1 * MINUTE_IN_MILLIS);
DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS
.put(PRIORITY_MIN, 1 * MINUTE_IN_MILLIS);
DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
@@ -1220,7 +1255,7 @@
private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS;
private static final long DEFAULT_RESCHEDULED_JOB_DEADLINE_MS = HOUR_IN_MILLIS;
- private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = 5 * DAY_IN_MILLIS;
+ private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = DAY_IN_MILLIS;
@VisibleForTesting
static final long DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = 3 * DAY_IN_MILLIS;
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index a3a686f..a0b9c5f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -16,8 +16,6 @@
package com.android.server.job.controllers;
-import static android.text.format.DateUtils.HOUR_IN_MILLIS;
-
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.NEVER_INDEX;
@@ -430,9 +428,6 @@
*/
public static final int INTERNAL_FLAG_DEMOTED_BY_SYSTEM_UIJ = 1 << 2;
- /** Minimum difference between start and end time to have flexible constraint */
- @VisibleForTesting
- static final long MIN_WINDOW_FOR_FLEXIBILITY_MS = HOUR_IN_MILLIS;
/**
* Versatile, persistable flags for a job that's updated within the system server,
* as opposed to {@link JobInfo#flags} that's set by callers.
@@ -708,14 +703,10 @@
final boolean lacksSomeFlexibleConstraints =
((~requiredConstraints) & SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS) != 0
|| mCanApplyTransportAffinities;
- final boolean satisfiesMinWindowException =
- (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis)
- >= MIN_WINDOW_FOR_FLEXIBILITY_MS;
// The first time a job is rescheduled it will not be subject to flexible constraints.
// Otherwise, every consecutive reschedule increases a jobs' flexibility deadline.
if (!isRequestedExpeditedJob() && !job.isUserInitiated()
- && satisfiesMinWindowException
&& (numFailures + numSystemStops) != 1
&& lacksSomeFlexibleConstraints) {
requiredConstraints |= CONSTRAINT_FLEXIBLE;
diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md
index f177586..b6e4e0d 100644
--- a/cmds/uinput/README.md
+++ b/cmds/uinput/README.md
@@ -154,8 +154,7 @@
#### `delay`
-Add a delay between the processing of commands. The delay will be timed from when the last delay
-ended, rather than from the current time, to allow for more precise timings to be produced.
+Add a delay to command processing
| Field | Type | Description |
|:-------------:|:-------------:|:-------------------------- |
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
index bd61000..a78a465 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
@@ -166,14 +166,14 @@
::ioctl(mFd, UI_DEV_DESTROY);
}
-void UinputDevice::injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code,
- int32_t value) {
+void UinputDevice::injectEvent(uint16_t type, uint16_t code, int32_t value) {
struct input_event event = {};
event.type = type;
event.code = code;
event.value = value;
- event.time.tv_sec = timestamp.count() / 1'000'000;
- event.time.tv_usec = timestamp.count() % 1'000'000;
+ timespec ts;
+ clock_gettime(CLOCK_MONOTONIC, &ts);
+ TIMESPEC_TO_TIMEVAL(&event.time, &ts);
if (::write(mFd, &event, sizeof(input_event)) < 0) {
ALOGE("Could not write event %" PRIu16 " %" PRIu16 " with value %" PRId32 " : %s", type,
@@ -268,12 +268,12 @@
}
}
-static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jlong timestampMicros,
- jint type, jint code, jint value) {
+static void injectEvent(JNIEnv* /* env */, jclass /* clazz */, jlong ptr, jint type, jint code,
+ jint value) {
uinput::UinputDevice* d = reinterpret_cast<uinput::UinputDevice*>(ptr);
if (d != nullptr) {
- d->injectEvent(std::chrono::microseconds(timestampMicros), static_cast<uint16_t>(type),
- static_cast<uint16_t>(code), static_cast<int32_t>(value));
+ d->injectEvent(static_cast<uint16_t>(type), static_cast<uint16_t>(code),
+ static_cast<int32_t>(value));
} else {
ALOGE("Could not inject event, Device* is null!");
}
@@ -330,7 +330,7 @@
"(Ljava/lang/String;IIIIIILjava/lang/String;"
"Lcom/android/commands/uinput/Device$DeviceCallback;)J",
reinterpret_cast<void*>(openUinputDevice)},
- {"nativeInjectEvent", "(JJIII)V", reinterpret_cast<void*>(injectEvent)},
+ {"nativeInjectEvent", "(JIII)V", reinterpret_cast<void*>(injectEvent)},
{"nativeConfigure", "(II[I)V", reinterpret_cast<void*>(configure)},
{"nativeSetAbsInfo", "(IILandroid/os/Parcel;)V", reinterpret_cast<void*>(setAbsInfo)},
{"nativeCloseUinputDevice", "(J)V", reinterpret_cast<void*>(closeUinputDevice)},
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.h b/cmds/uinput/jni/com_android_commands_uinput_Device.h
index 72c8647..9769a75 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.h
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.h
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-#include <android-base/unique_fd.h>
-#include <jni.h>
-#include <linux/input.h>
-
-#include <chrono>
#include <memory>
#include <vector>
+#include <jni.h>
+#include <linux/input.h>
+
+#include <android-base/unique_fd.h>
#include "src/com/android/commands/uinput/InputAbsInfo.h"
namespace android {
@@ -54,8 +53,7 @@
virtual ~UinputDevice();
- void injectEvent(std::chrono::microseconds timestamp, uint16_t type, uint16_t code,
- int32_t value);
+ void injectEvent(uint16_t type, uint16_t code, int32_t value);
int handleEvents(int events);
private:
diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java
index 76ab475..25d3a34 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Device.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Device.java
@@ -55,7 +55,7 @@
private final SparseArray<InputAbsInfo> mAbsInfo;
private final OutputStream mOutputStream;
private final Object mCond = new Object();
- private long mTimeToSendNanos;
+ private long mTimeToSend;
static {
System.loadLibrary("uinputcommand_jni");
@@ -65,8 +65,7 @@
int productId, int versionId, int bus, int ffEffectsMax, String port,
DeviceCallback callback);
private static native void nativeCloseUinputDevice(long ptr);
- private static native void nativeInjectEvent(long ptr, long timestampMicros, int type, int code,
- int value);
+ private static native void nativeInjectEvent(long ptr, int type, int code, int value);
private static native void nativeConfigure(int handle, int code, int[] configs);
private static native void nativeSetAbsInfo(int handle, int axisCode, Parcel axisParcel);
private static native int nativeGetEvdevEventTypeByLabel(String label);
@@ -102,54 +101,27 @@
}
mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget();
- mTimeToSendNanos = SystemClock.uptimeNanos();
- }
-
- private long getTimeToSendMillis() {
- // Since we can only specify delays in milliseconds but evemu timestamps are in
- // microseconds, we have to round up the delays to avoid setting event timestamps
- // which are in the future (which the kernel would silently reject and replace with
- // the current time).
- //
- // This should be the same as (long) Math.ceil(mTimeToSendNanos / 1_000_000.0), except
- // without the precision loss that comes from converting from long to double and back.
- return mTimeToSendNanos / 1_000_000 + ((mTimeToSendNanos % 1_000_000 > 0) ? 1 : 0);
+ mTimeToSend = SystemClock.uptimeMillis();
}
/**
* Inject uinput events to device
*
* @param events Array of raw uinput events.
- * @param offsetMicros The difference in microseconds between the timestamps of the previous
- * batch of events injected and this batch. If set to -1, the current
- * timestamp will be used.
*/
- public void injectEvent(int[] events, long offsetMicros) {
+ public void injectEvent(int[] events) {
// if two messages are sent at identical time, they will be processed in order received
- SomeArgs args = SomeArgs.obtain();
- args.arg1 = events;
- args.argl1 = offsetMicros;
- args.argl2 = mTimeToSendNanos;
- Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, args);
- mHandler.sendMessageAtTime(msg, getTimeToSendMillis());
+ Message msg = mHandler.obtainMessage(MSG_INJECT_EVENT, events);
+ mHandler.sendMessageAtTime(msg, mTimeToSend);
}
/**
- * Delay subsequent device activity by the specified amount of time.
+ * Impose a delay to the device for execution.
*
- * <p>Note that although the delay is specified in nanoseconds, due to limitations of {@link
- * Handler}'s API, scheduling only occurs with millisecond precision. When scheduling an
- * injection or sync, the time at which it is scheduled will be rounded up to the nearest
- * millisecond. While this means that a particular injection cannot be scheduled precisely,
- * rounding errors will not accumulate over time. For example, if five injections are scheduled
- * with a delay of 1,200,000ns before each one, the total delay will be 6ms, as opposed to the
- * 10ms it would have been if each individual delay had been rounded up (as {@link EvemuParser}
- * would otherwise have to do to avoid sending timestamps that are in the future).
- *
- * @param delayNanos Time to delay in unit of nanoseconds.
+ * @param delay Time to delay in unit of milliseconds.
*/
- public void addDelayNanos(long delayNanos) {
- mTimeToSendNanos += delayNanos;
+ public void addDelay(int delay) {
+ mTimeToSend = Math.max(SystemClock.uptimeMillis(), mTimeToSend) + delay;
}
/**
@@ -159,8 +131,7 @@
* @param syncToken The token for this sync command.
*/
public void syncEvent(String syncToken) {
- mHandler.sendMessageAtTime(
- mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), getTimeToSendMillis());
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_SYNC_EVENT, syncToken), mTimeToSend);
}
/**
@@ -169,8 +140,7 @@
*/
public void close() {
Message msg = mHandler.obtainMessage(MSG_CLOSE_UINPUT_DEVICE);
- mHandler.sendMessageAtTime(
- msg, Math.max(SystemClock.uptimeMillis(), getTimeToSendMillis()) + 1);
+ mHandler.sendMessageAtTime(msg, Math.max(SystemClock.uptimeMillis(), mTimeToSend) + 1);
try {
synchronized (mCond) {
mCond.wait();
@@ -181,7 +151,6 @@
private class DeviceHandler extends Handler {
private long mPtr;
- private long mLastInjectTimestampMicros = -1;
private int mBarrierToken;
DeviceHandler(Looper looper) {
@@ -191,7 +160,7 @@
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_OPEN_UINPUT_DEVICE: {
+ case MSG_OPEN_UINPUT_DEVICE:
SomeArgs args = (SomeArgs) msg.obj;
String name = (String) args.arg1;
mPtr = nativeOpenUinputDevice(name, args.argi1 /* id */,
@@ -208,44 +177,15 @@
}
args.recycle();
break;
- }
- case MSG_INJECT_EVENT: {
- SomeArgs args = (SomeArgs) msg.obj;
- if (mPtr == 0) {
- args.recycle();
- break;
+ case MSG_INJECT_EVENT:
+ if (mPtr != 0) {
+ int[] events = (int[]) msg.obj;
+ for (int pos = 0; pos + 2 < events.length; pos += 3) {
+ nativeInjectEvent(mPtr, events[pos], events[pos + 1], events[pos + 2]);
+ }
}
- long offsetMicros = args.argl1;
- if (mLastInjectTimestampMicros == -1 || offsetMicros == -1) {
- // There's often a delay of a few milliseconds between the time specified to
- // Handler.sendMessageAtTime and the handler actually being called, due to
- // the way threads are scheduled. We don't take this into account when
- // calling addDelayNanos between the first batch of event injections (when
- // we set the "base timestamp" from which all others will be offset) and the
- // second batch, meaning that the actual time between the handler calls for
- // those batches may be less than the offset between their timestamps. When
- // that happens, we would pass a timestamp for the second batch that's
- // actually in the future. The kernel's uinput API rejects timestamps that
- // are in the future and uses the current time instead, making the reported
- // timestamps inconsistent with the recording we're replaying.
- //
- // To prevent this, we need to use the time we scheduled this first batch
- // for (in microseconds, to avoid potential rounding up from
- // getTimeToSendMillis), rather than the actual current time.
- mLastInjectTimestampMicros = args.argl2 / 1000;
- } else {
- mLastInjectTimestampMicros += offsetMicros;
- }
-
- int[] events = (int[]) args.arg1;
- for (int pos = 0; pos + 2 < events.length; pos += 3) {
- nativeInjectEvent(mPtr, mLastInjectTimestampMicros, events[pos],
- events[pos + 1], events[pos + 2]);
- }
- args.recycle();
break;
- }
- case MSG_CLOSE_UINPUT_DEVICE: {
+ case MSG_CLOSE_UINPUT_DEVICE:
if (mPtr != 0) {
nativeCloseUinputDevice(mPtr);
getLooper().quitSafely();
@@ -258,14 +198,11 @@
mCond.notify();
}
break;
- }
- case MSG_SYNC_EVENT: {
+ case MSG_SYNC_EVENT:
handleSyncEvent((String) msg.obj);
break;
- }
- default: {
+ default:
throw new IllegalArgumentException("Unknown device message");
- }
}
}
diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
index da99162..7652f24 100644
--- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
@@ -44,7 +44,7 @@
* recordings, this will always be the same.
*/
private static final int DEVICE_ID = 1;
- private static final int REGISTRATION_DELAY_NANOS = 500_000_000;
+ private static final int REGISTRATION_DELAY_MILLIS = 500;
private static class CommentAwareReader {
private final LineNumberReader mReader;
@@ -152,7 +152,7 @@
final Event.Builder delayEb = new Event.Builder();
delayEb.setId(DEVICE_ID);
delayEb.setCommand(Event.Command.DELAY);
- delayEb.setDurationNanos(REGISTRATION_DELAY_NANOS);
+ delayEb.setDurationMillis(REGISTRATION_DELAY_MILLIS);
mQueuedEvents.add(delayEb.build());
}
@@ -175,6 +175,7 @@
throw new ParsingException(
"Invalid timestamp '" + parts[0] + "' (should contain a single '.')", mReader);
}
+ // TODO(b/310958309): use timeMicros to set the timestamp on the event being sent.
final long timeMicros =
parseLong(timeParts[0], 10) * 1_000_000 + parseInt(timeParts[1], 10);
final Event.Builder eb = new Event.Builder();
@@ -191,18 +192,21 @@
return eb.build();
} else {
final long delayMicros = timeMicros - mLastEventTimeMicros;
- eb.setTimestampOffsetMicros(delayMicros);
- if (delayMicros == 0) {
+ // The shortest delay supported by Handler.sendMessageAtTime (used for timings by the
+ // Device class) is 1ms, so ignore time differences smaller than that.
+ if (delayMicros < 1000) {
+ mLastEventTimeMicros = timeMicros;
return eb.build();
+ } else {
+ // Send a delay now, and queue the actual event for the next call.
+ mQueuedEvents.add(eb.build());
+ mLastEventTimeMicros = timeMicros;
+ final Event.Builder delayEb = new Event.Builder();
+ delayEb.setId(DEVICE_ID);
+ delayEb.setCommand(Event.Command.DELAY);
+ delayEb.setDurationMillis((int) (delayMicros / 1000));
+ return delayEb.build();
}
- // Send a delay now, and queue the actual event for the next call.
- mQueuedEvents.add(eb.build());
- mLastEventTimeMicros = timeMicros;
- final Event.Builder delayEb = new Event.Builder();
- delayEb.setId(DEVICE_ID);
- delayEb.setCommand(Event.Command.DELAY);
- delayEb.setDurationNanos(delayMicros * 1000);
- return delayEb.build();
}
}
diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java
index 9e7ee09..0f16a27 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Event.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Event.java
@@ -99,9 +99,8 @@
private int mVersionId;
private int mBusId;
private int[] mInjections;
- private long mTimestampOffsetMicros = -1;
private SparseArray<int[]> mConfiguration;
- private long mDurationNanos;
+ private int mDurationMillis;
private int mFfEffectsMax = 0;
private String mInputPort;
private SparseArray<InputAbsInfo> mAbsInfo;
@@ -140,28 +139,19 @@
}
/**
- * Returns the number of microseconds that should be added to the previous {@code INJECT}
- * event's timestamp to produce the timestamp for this {@code INJECT} event. A value of -1
- * indicates that the current timestamp should be used instead.
- */
- public long getTimestampOffsetMicros() {
- return mTimestampOffsetMicros;
- }
-
- /**
* Returns a {@link SparseArray} describing the event codes that should be registered for the
* device. The keys are uinput ioctl codes (such as those returned from {@link
* UinputControlCode#getValue()}, while the values are arrays of event codes to be enabled with
* those ioctls. For example, key 101 (corresponding to {@link UinputControlCode#UI_SET_KEYBIT})
- * could have values 0x110 ({@code BTN_LEFT}), 0x111 ({@code BTN_RIGHT}), and 0x112
+ * could have values 0x110 ({@code BTN_LEFT}, 0x111 ({@code BTN_RIGHT}), and 0x112
* ({@code BTN_MIDDLE}).
*/
public SparseArray<int[]> getConfiguration() {
return mConfiguration;
}
- public long getDurationNanos() {
- return mDurationNanos;
+ public int getDurationMillis() {
+ return mDurationMillis;
}
public int getFfEffectsMax() {
@@ -192,7 +182,7 @@
+ ", busId=" + mBusId
+ ", events=" + Arrays.toString(mInjections)
+ ", configuration=" + mConfiguration
- + ", duration=" + mDurationNanos + "ns"
+ + ", duration=" + mDurationMillis + "ms"
+ ", ff_effects_max=" + mFfEffectsMax
+ ", port=" + mInputPort
+ "}";
@@ -221,10 +211,6 @@
mEvent.mInjections = events;
}
- public void setTimestampOffsetMicros(long offsetMicros) {
- mEvent.mTimestampOffsetMicros = offsetMicros;
- }
-
/**
* Sets the event codes that should be registered with a {@code register} command.
*
@@ -251,8 +237,8 @@
mEvent.mBusId = busId;
}
- public void setDurationNanos(long durationNanos) {
- mEvent.mDurationNanos = durationNanos;
+ public void setDurationMillis(int durationMillis) {
+ mEvent.mDurationMillis = durationMillis;
}
public void setFfEffectsMax(int ffEffectsMax) {
@@ -285,7 +271,7 @@
}
}
case DELAY -> {
- if (mEvent.mDurationNanos <= 0) {
+ if (mEvent.mDurationMillis <= 0) {
throw new IllegalStateException("Delay has missing or invalid duration");
}
}
diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
index 6994f0c..ed3ff33 100644
--- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
@@ -71,8 +71,7 @@
case "configuration" -> eb.setConfiguration(readConfiguration());
case "ff_effects_max" -> eb.setFfEffectsMax(readInt());
case "abs_info" -> eb.setAbsInfo(readAbsInfoArray());
- // Duration is specified in milliseconds in the JSON-style format.
- case "duration" -> eb.setDurationNanos(readInt() * 1_000_000L);
+ case "duration" -> eb.setDurationMillis(readInt());
case "port" -> eb.setInputPort(mReader.nextString());
case "syncToken" -> eb.setSyncToken(mReader.nextString());
default -> mReader.skipValue();
diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
index 760e981..04df279 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Uinput.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
@@ -134,8 +134,8 @@
switch (Objects.requireNonNull(e.getCommand())) {
case REGISTER ->
error("Device id=" + e.getId() + " is already registered. Ignoring event.");
- case INJECT -> d.injectEvent(e.getInjections(), e.getTimestampOffsetMicros());
- case DELAY -> d.addDelayNanos(e.getDurationNanos());
+ case INJECT -> d.injectEvent(e.getInjections());
+ case DELAY -> d.addDelay(e.getDurationMillis());
case SYNC -> d.syncEvent(e.getSyncToken());
}
}
diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
index 4dc4b68..a05cc67 100644
--- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
+++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
@@ -183,22 +183,16 @@
}
private void assertInjectEvent(Event event, int eventType, int eventCode, int value) {
- assertInjectEvent(event, eventType, eventCode, value, 0);
- }
-
- private void assertInjectEvent(Event event, int eventType, int eventCode, int value,
- long timestampOffsetMicros) {
assertThat(event).isNotNull();
assertThat(event.getCommand()).isEqualTo(Event.Command.INJECT);
assertThat(event.getInjections()).asList()
.containsExactly(eventType, eventCode, value).inOrder();
- assertThat(event.getTimestampOffsetMicros()).isEqualTo(timestampOffsetMicros);
}
- private void assertDelayEvent(Event event, int durationNanos) {
+ private void assertDelayEvent(Event event, int durationMillis) {
assertThat(event).isNotNull();
assertThat(event.getCommand()).isEqualTo(Event.Command.DELAY);
- assertThat(event.getDurationNanos()).isEqualTo(durationNanos);
+ assertThat(event.getDurationMillis()).isEqualTo(durationMillis);
}
@Test
@@ -213,7 +207,7 @@
EvemuParser parser = new EvemuParser(reader);
assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.REGISTER);
assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY);
- assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1, -1);
+ assertInjectEvent(parser.getNextEvent(), 0x2, 0x0, 1);
assertInjectEvent(parser.getNextEvent(), 0x2, 0x1, -2);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
}
@@ -234,17 +228,17 @@
assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.REGISTER);
assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY);
- assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, -1);
+ assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
- assertDelayEvent(parser.getNextEvent(), 10_000_000);
+ assertDelayEvent(parser.getNextEvent(), 10);
- assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0, 10_000);
+ assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 0);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
- assertDelayEvent(parser.getNextEvent(), 1_000_000_000);
+ assertDelayEvent(parser.getNextEvent(), 1000);
- assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1, 1_000_000);
+ assertInjectEvent(parser.getNextEvent(), 0x1, 0x15, 1);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
}
@@ -455,7 +449,7 @@
assertThat(regEvent.getBus()).isEqualTo(0x001d);
assertThat(regEvent.getVendorId()).isEqualTo(0x6cb);
assertThat(regEvent.getProductId()).isEqualTo(0x0000);
- // TODO(b/302297266): check version ID once it's supported
+ assertThat(regEvent.getVersionId()).isEqualTo(0x0000);
assertThat(regEvent.getConfiguration().get(UinputControlCode.UI_SET_PROPBIT.getValue()))
.asList().containsExactly(0, 2);
@@ -483,7 +477,7 @@
assertThat(parser.getNextEvent().getCommand()).isEqualTo(Event.Command.DELAY);
- assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0, -1);
+ assertInjectEvent(parser.getNextEvent(), 0x3, 0x39, 0);
assertInjectEvent(parser.getNextEvent(), 0x3, 0x35, 891);
assertInjectEvent(parser.getNextEvent(), 0x3, 0x36, 333);
assertInjectEvent(parser.getNextEvent(), 0x3, 0x3a, 56);
@@ -496,8 +490,8 @@
assertInjectEvent(parser.getNextEvent(), 0x3, 0x18, 56);
assertInjectEvent(parser.getNextEvent(), 0x0, 0x0, 0);
- assertDelayEvent(parser.getNextEvent(), 6_080_000);
+ assertDelayEvent(parser.getNextEvent(), 6);
- assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888, 6_080);
+ assertInjectEvent(parser.getNextEvent(), 0x3, 0x0035, 888);
}
}
diff --git a/core/api/current.txt b/core/api/current.txt
index acb4146..c4cd575 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1052,6 +1052,7 @@
field public static final int label = 16842753; // 0x1010001
field public static final int labelFor = 16843718; // 0x10103c6
field @Deprecated public static final int labelTextSize = 16843317; // 0x1010235
+ field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") public static final int languageSettingsActivity;
field public static final int languageTag = 16844040; // 0x1010508
field public static final int largeHeap = 16843610; // 0x101035a
field public static final int largeScreens = 16843398; // 0x1010286
@@ -1803,6 +1804,7 @@
field public static final int useEmbeddedDex = 16844190; // 0x101059e
field public static final int useIntrinsicSizeAsMinimum = 16843536; // 0x1010310
field public static final int useLevel = 16843167; // 0x101019f
+ field @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public static final int useLocalePreferredLineHeightForMinimum;
field public static final int userVisible = 16843409; // 0x1010291
field public static final int usesCleartextTraffic = 16844012; // 0x10104ec
field public static final int usesPermissionFlags = 16844356; // 0x1010644
@@ -3321,11 +3323,13 @@
method @FlaggedApi("android.view.accessibility.a11y_overlay_callbacks") public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
method public boolean clearCache();
method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void clearTestBrailleDisplayController();
method public final void disableSelf();
method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler);
method public android.view.accessibility.AccessibilityNodeInfo findFocus(int);
method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController();
method @NonNull public final android.accessibilityservice.AccessibilityButtonController getAccessibilityButtonController(int);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") @NonNull public android.accessibilityservice.BrailleDisplayController getBrailleDisplayController();
method @NonNull @RequiresPermission(android.Manifest.permission.USE_FINGERPRINT) public final android.accessibilityservice.FingerprintGestureController getFingerprintGestureController();
method @Nullable public final android.accessibilityservice.InputMethod getInputMethod();
method @NonNull public final android.accessibilityservice.AccessibilityService.MagnificationController getMagnificationController();
@@ -3355,6 +3359,7 @@
method public boolean setCacheEnabled(boolean);
method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region);
method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void setTestBrailleDisplayController(@NonNull android.accessibilityservice.BrailleDisplayController);
method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region);
method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback);
@@ -3559,6 +3564,25 @@
field public String[] packageNames;
}
+ @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT) public void connect(@NonNull android.bluetooth.BluetoothDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void connect(@NonNull android.hardware.usb.UsbDevice, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void disconnect();
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public boolean isConnected();
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void write(@NonNull byte[]) throws java.io.IOException;
+ }
+
+ @FlaggedApi("android.view.accessibility.braille_display_hid") public static interface BrailleDisplayController.BrailleDisplayCallback {
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnected(@NonNull byte[]);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onConnectionFailed(int);
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onDisconnected();
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") public void onInput(@NonNull byte[]);
+ field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 2; // 0x2
+ field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final int FLAG_ERROR_CANNOT_ACCESS = 1; // 0x1
+ }
+
public final class FingerprintGestureController {
method public boolean isGestureDetectionAvailable();
method public void registerFingerprintGestureCallback(@NonNull android.accessibilityservice.FingerprintGestureController.FingerprintGestureCallback, @Nullable android.os.Handler);
@@ -5424,7 +5448,6 @@
}
@FlaggedApi("android.security.content_uri_permission_apis") public final class ComponentCaller {
- ctor public ComponentCaller(@NonNull android.os.IBinder, @Nullable android.os.IBinder);
method public int checkContentUriPermission(@NonNull android.net.Uri, int);
method @Nullable public String getPackage();
method public int getUid();
@@ -7401,6 +7424,7 @@
method public int onStartCommand(android.content.Intent, int, int);
method public void onTaskRemoved(android.content.Intent);
method public void onTimeout(int);
+ method @FlaggedApi("android.app.introduce_new_service_ontimeout_callback") public void onTimeout(int, int);
method public void onTrimMemory(int);
method public boolean onUnbind(android.content.Intent);
method public final void startForeground(int, android.app.Notification);
@@ -7816,6 +7840,7 @@
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.app.admin.DeviceAdminInfo> CREATOR;
field public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1; // 0x1
+ field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; // 0x2
field public static final int HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED = 0; // 0x0
field public static final int USES_ENCRYPTED_STORAGE = 7; // 0x7
field public static final int USES_POLICY_DISABLE_CAMERA = 8; // 0x8
@@ -10020,11 +10045,22 @@
method public CharSequence coerceToText(android.content.Context);
method public String getHtmlText();
method public android.content.Intent getIntent();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @Nullable public android.app.PendingIntent getPendingIntent();
method public CharSequence getText();
method @Nullable public android.view.textclassifier.TextLinks getTextLinks();
method public android.net.Uri getUri();
}
+ @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final class ClipData.Item.Builder {
+ ctor public ClipData.Item.Builder();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item build();
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setHtmlText(@Nullable String);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setIntent(@Nullable android.content.Intent);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setPendingIntent(@Nullable android.app.PendingIntent);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setText(@Nullable CharSequence);
+ method @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") @NonNull public android.content.ClipData.Item.Builder setUri(@Nullable android.net.Uri);
+ }
+
public class ClipDescription implements android.os.Parcelable {
ctor public ClipDescription(CharSequence, String[]);
ctor public ClipDescription(android.content.ClipDescription);
@@ -11283,6 +11319,8 @@
field public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS = "android.intent.extra.CHOOSER_CUSTOM_ACTIONS";
field public static final String EXTRA_CHOOSER_MODIFY_SHARE_ACTION = "android.intent.extra.CHOOSER_MODIFY_SHARE_ACTION";
field public static final String EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER";
+ field @FlaggedApi("android.service.chooser.enable_chooser_result") public static final String EXTRA_CHOOSER_RESULT = "android.intent.extra.CHOOSER_RESULT";
+ field @FlaggedApi("android.service.chooser.enable_chooser_result") public static final String EXTRA_CHOOSER_RESULT_INTENT_SENDER = "android.intent.extra.CHOOSER_RESULT_INTENT_SENDER";
field public static final String EXTRA_CHOOSER_TARGETS = "android.intent.extra.CHOOSER_TARGETS";
field public static final String EXTRA_CHOSEN_COMPONENT = "android.intent.extra.CHOSEN_COMPONENT";
field public static final String EXTRA_CHOSEN_COMPONENT_INTENT_SENDER = "android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER";
@@ -11311,6 +11349,7 @@
field public static final String EXTRA_LOCALE_LIST = "android.intent.extra.LOCALE_LIST";
field public static final String EXTRA_LOCAL_ONLY = "android.intent.extra.LOCAL_ONLY";
field public static final String EXTRA_LOCUS_ID = "android.intent.extra.LOCUS_ID";
+ field @FlaggedApi("android.service.chooser.enable_sharesheet_metadata_extra") public static final String EXTRA_METADATA_TEXT = "android.intent.extra.METADATA_TEXT";
field public static final String EXTRA_MIME_TYPES = "android.intent.extra.MIME_TYPES";
field public static final String EXTRA_NOT_UNKNOWN_SOURCE = "android.intent.extra.NOT_UNKNOWN_SOURCE";
field public static final String EXTRA_ORIGINATING_URI = "android.intent.extra.ORIGINATING_URI";
@@ -18822,6 +18861,7 @@
method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView();
method @Nullable public CharSequence getDescription();
method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.graphics.Bitmap getLogoBitmap();
+ method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public String getLogoDescription();
method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public int getLogoRes();
method @Nullable public CharSequence getNegativeButtonText();
method @Nullable public CharSequence getSubtitle();
@@ -18873,6 +18913,7 @@
method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence);
method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean);
method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap);
+ method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoDescription(@NonNull String);
method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int);
method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener);
method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence);
@@ -24405,6 +24446,7 @@
}
public final class MediaRouter2 {
+ method @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") public void cancelScanRequest(@NonNull android.media.MediaRouter2.ScanToken);
method @Nullable public android.media.MediaRouter2.RoutingController getController(@NonNull String);
method @NonNull public java.util.List<android.media.MediaRouter2.RoutingController> getControllers();
method @NonNull public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context);
@@ -24416,6 +24458,7 @@
method public void registerRouteCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaRouter2.RouteCallback, @NonNull android.media.RouteDiscoveryPreference);
method @FlaggedApi("com.android.media.flags.enable_rlp_callbacks_in_media_router2") public void registerRouteListingPreferenceUpdatedCallback(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.media.RouteListingPreference>);
method public void registerTransferCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.MediaRouter2.TransferCallback);
+ method @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") @NonNull public android.media.MediaRouter2.ScanToken requestScan(@NonNull android.media.MediaRouter2.ScanRequest);
method public void setOnGetControllerHintsListener(@Nullable android.media.MediaRouter2.OnGetControllerHintsListener);
method public void setRouteListingPreference(@Nullable android.media.RouteListingPreference);
method public boolean showSystemOutputSwitcher();
@@ -24452,6 +24495,7 @@
method @NonNull public android.media.RoutingSessionInfo getRoutingSessionInfo();
method @NonNull public java.util.List<android.media.MediaRoute2Info> getSelectableRoutes();
method @NonNull public java.util.List<android.media.MediaRoute2Info> getSelectedRoutes();
+ method @FlaggedApi("com.android.media.flags.enable_get_transferable_routes") @NonNull public java.util.List<android.media.MediaRoute2Info> getTransferableRoutes();
method public int getVolume();
method public int getVolumeHandling();
method public int getVolumeMax();
@@ -24462,6 +24506,19 @@
method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferInitiatedBySelf();
}
+ @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") public static final class MediaRouter2.ScanRequest {
+ method public boolean isScreenOffScan();
+ }
+
+ public static final class MediaRouter2.ScanRequest.Builder {
+ ctor public MediaRouter2.ScanRequest.Builder();
+ method @NonNull public android.media.MediaRouter2.ScanRequest build();
+ method @NonNull public android.media.MediaRouter2.ScanRequest.Builder setScreenOffScan(boolean);
+ }
+
+ @FlaggedApi("com.android.media.flags.enable_screen_off_scanning") public static final class MediaRouter2.ScanToken {
+ }
+
public abstract static class MediaRouter2.TransferCallback {
ctor public MediaRouter2.TransferCallback();
method public void onStop(@NonNull android.media.MediaRouter2.RoutingController);
@@ -25829,6 +25886,9 @@
method public int describeContents();
method public int getErrorCode();
method public int getFinalState();
+ method @NonNull public java.util.List<android.media.metrics.MediaItemInfo> getInputMediaItemInfos();
+ method public long getOperationTypes();
+ method @Nullable public android.media.metrics.MediaItemInfo getOutputMediaItemInfo();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR;
field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12
@@ -25853,14 +25913,25 @@
field public static final int FINAL_STATE_CANCELED = 2; // 0x2
field public static final int FINAL_STATE_ERROR = 3; // 0x3
field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1
+ field public static final long OPERATION_TYPE_AUDIO_EDIT = 8L; // 0x8L
+ field public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 2L; // 0x2L
+ field public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 32L; // 0x20L
+ field public static final long OPERATION_TYPE_PAUSED = 64L; // 0x40L
+ field public static final long OPERATION_TYPE_RESUMED = 128L; // 0x80L
+ field public static final long OPERATION_TYPE_VIDEO_EDIT = 4L; // 0x4L
+ field public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1L; // 0x1L
+ field public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 16L; // 0x10L
field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff
}
@FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder {
ctor public EditingEndedEvent.Builder(int);
+ method @NonNull public android.media.metrics.EditingEndedEvent.Builder addInputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo);
+ method @NonNull public android.media.metrics.EditingEndedEvent.Builder addOperationType(long);
method @NonNull public android.media.metrics.EditingEndedEvent build();
method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int);
method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle);
+ method @NonNull public android.media.metrics.EditingEndedEvent.Builder setOutputMediaItemInfo(@NonNull android.media.metrics.MediaItemInfo);
method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long);
}
@@ -25880,6 +25951,65 @@
field @NonNull public static final android.media.metrics.LogSessionId LOG_SESSION_ID_NONE;
}
+ @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class MediaItemInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAudioChannelCount();
+ method public long getAudioSampleCount();
+ method public int getAudioSampleRateHz();
+ method public long getClipDurationMillis();
+ method @NonNull public java.util.List<java.lang.String> getCodecNames();
+ method @Nullable public String getContainerMimeType();
+ method public long getDataTypes();
+ method public long getDurationMillis();
+ method @NonNull public java.util.List<java.lang.String> getSampleMimeTypes();
+ method public int getSourceType();
+ method public int getVideoDataSpace();
+ method public float getVideoFrameRate();
+ method public long getVideoSampleCount();
+ method @NonNull public android.util.Size getVideoSize();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.MediaItemInfo> CREATOR;
+ field public static final long DATA_TYPE_AUDIO = 4L; // 0x4L
+ field public static final long DATA_TYPE_CUE_POINTS = 128L; // 0x80L
+ field public static final long DATA_TYPE_DEPTH = 16L; // 0x10L
+ field public static final long DATA_TYPE_GAIN_MAP = 32L; // 0x20L
+ field public static final long DATA_TYPE_GAPLESS = 256L; // 0x100L
+ field public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1024L; // 0x400L
+ field public static final long DATA_TYPE_HIGH_FRAME_RATE = 64L; // 0x40L
+ field public static final long DATA_TYPE_IMAGE = 1L; // 0x1L
+ field public static final long DATA_TYPE_METADATA = 8L; // 0x8L
+ field public static final long DATA_TYPE_SPATIAL_AUDIO = 512L; // 0x200L
+ field public static final long DATA_TYPE_VIDEO = 2L; // 0x2L
+ field public static final int SOURCE_TYPE_CAMERA = 2; // 0x2
+ field public static final int SOURCE_TYPE_EDITING_SESSION = 3; // 0x3
+ field public static final int SOURCE_TYPE_GALLERY = 1; // 0x1
+ field public static final int SOURCE_TYPE_GENERATED = 7; // 0x7
+ field public static final int SOURCE_TYPE_LOCAL_FILE = 4; // 0x4
+ field public static final int SOURCE_TYPE_REMOTE_FILE = 5; // 0x5
+ field public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6; // 0x6
+ field public static final int SOURCE_TYPE_UNSPECIFIED = 0; // 0x0
+ field public static final int VALUE_UNSPECIFIED = -1; // 0xffffffff
+ }
+
+ @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class MediaItemInfo.Builder {
+ ctor public MediaItemInfo.Builder();
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder addCodecName(@NonNull String);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder addDataType(long);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder addSampleMimeType(@NonNull String);
+ method @NonNull public android.media.metrics.MediaItemInfo build();
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioChannelCount(@IntRange(from=0) int);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleCount(@IntRange(from=0) long);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setAudioSampleRateHz(@IntRange(from=0) int);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setClipDurationMillis(long);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setContainerMimeType(@NonNull String);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setDurationMillis(long);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setSourceType(int);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoDataSpace(int);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoFrameRate(@FloatRange(from=0) float);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSampleCount(@IntRange(from=0) long);
+ method @NonNull public android.media.metrics.MediaItemInfo.Builder setVideoSize(@NonNull android.util.Size);
+ }
+
public final class MediaMetricsManager {
method @NonNull public android.media.metrics.BundleSession createBundleSession();
method @NonNull public android.media.metrics.EditingSession createEditingSession();
@@ -27678,7 +27808,7 @@
package android.media.tv.ad {
- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public class TvAdManager {
+ @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") public final class TvAdManager {
method @NonNull public java.util.List<android.media.tv.ad.TvAdServiceInfo> getTvAdServiceList();
method public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdManager.TvAdServiceCallback);
method public void sendAppLinkCommand(@NonNull String, @NonNull android.os.Bundle);
@@ -27689,6 +27819,14 @@
field public static final String APP_LINK_KEY_COMMAND_TYPE = "command_type";
field public static final String APP_LINK_KEY_PACKAGE_NAME = "package_name";
field public static final String APP_LINK_KEY_SERVICE_ID = "service_id";
+ field public static final int ERROR_BLOCKED = 5; // 0x5
+ field public static final int ERROR_ENCRYPTED = 6; // 0x6
+ field public static final int ERROR_NONE = 0; // 0x0
+ field public static final int ERROR_NOT_SUPPORTED = 2; // 0x2
+ field public static final int ERROR_RESOURCE_UNAVAILABLE = 4; // 0x4
+ field public static final int ERROR_UNKNOWN = 1; // 0x1
+ field public static final int ERROR_UNKNOWN_CHANNEL = 7; // 0x7
+ field public static final int ERROR_WEAK_SIGNAL = 3; // 0x3
field public static final String INTENT_KEY_AD_SERVICE_ID = "ad_service_id";
field public static final String INTENT_KEY_CHANNEL_URI = "channel_uri";
field public static final String INTENT_KEY_COMMAND_TYPE = "command_type";
@@ -27701,6 +27839,9 @@
field public static final String SESSION_DATA_TYPE_AD_REQUEST = "ad_request";
field public static final String SESSION_DATA_TYPE_BROADCAST_INFO_REQUEST = "broadcast_info_request";
field public static final String SESSION_DATA_TYPE_REMOVE_BROADCAST_INFO_REQUEST = "remove_broadcast_info_request";
+ field public static final int SESSION_STATE_ERROR = 3; // 0x3
+ field public static final int SESSION_STATE_RUNNING = 2; // 0x2
+ field public static final int SESSION_STATE_STOPPED = 1; // 0x1
}
public abstract static class TvAdManager.TvAdServiceCallback {
@@ -27723,7 +27864,12 @@
ctor public TvAdService.Session(@NonNull android.content.Context);
method public boolean isMediaViewEnabled();
method @CallSuper public void layoutSurface(int, int, int, int);
+ method @CallSuper public void notifySessionStateChanged(int, int);
method @Nullable public android.view.View onCreateMediaView();
+ method public void onCurrentChannelUri(@Nullable android.net.Uri);
+ method public void onCurrentTvInputId(@Nullable String);
+ method public void onCurrentVideoBounds(@NonNull android.graphics.Rect);
+ method public void onError(@NonNull String, @NonNull android.os.Bundle);
method public boolean onGenericMotionEvent(@NonNull android.view.MotionEvent);
method public boolean onKeyDown(int, @Nullable android.view.KeyEvent);
method public boolean onKeyLongPress(int, @Nullable android.view.KeyEvent);
@@ -27733,12 +27879,20 @@
method public abstract void onRelease();
method public void onResetAdService();
method public abstract boolean onSetSurface(@Nullable android.view.Surface);
+ method public void onSigningResult(@NonNull String, @NonNull byte[]);
method public void onStartAdService();
method public void onStopAdService();
method public void onSurfaceChanged(int, int, int);
method public boolean onTouchEvent(@NonNull android.view.MotionEvent);
+ method public void onTrackInfoList(@NonNull java.util.List<android.media.tv.TvTrackInfo>);
method public boolean onTrackballEvent(@NonNull android.view.MotionEvent);
method public void onTvInputSessionData(@NonNull String, @NonNull android.os.Bundle);
+ method public void onTvMessage(int, @NonNull android.os.Bundle);
+ method @CallSuper public void requestCurrentChannelUri();
+ method @CallSuper public void requestCurrentTvInputId();
+ method @CallSuper public void requestCurrentVideoBounds();
+ method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method @CallSuper public void requestTrackInfoList();
method public void sendTvAdSessionData(@NonNull String, @NonNull android.os.Bundle);
method @CallSuper public void setMediaViewEnabled(boolean);
}
@@ -27757,9 +27911,12 @@
ctor public TvAdView(@NonNull android.content.Context);
ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet);
ctor public TvAdView(@NonNull android.content.Context, @Nullable android.util.AttributeSet, int);
+ method public void clearCallback();
method public void clearOnUnhandledInputEventListener();
method public boolean dispatchUnhandledInputEvent(@NonNull android.view.InputEvent);
method @Nullable public android.media.tv.ad.TvAdView.OnUnhandledInputEventListener getOnUnhandledInputEventListener();
+ method public void notifyError(@NonNull String, @NonNull android.os.Bundle);
+ method public void notifyTvMessage(@NonNull int, @NonNull android.os.Bundle);
method public void onAttachedToWindow();
method public void onDetachedFromWindow();
method public void onLayout(boolean, int, int, int, int);
@@ -27769,16 +27926,34 @@
method public void prepareAdService(@NonNull String, @NonNull String);
method public void reset();
method public void resetAdService();
+ method public void sendCurrentChannelUri(@Nullable android.net.Uri);
+ method public void sendCurrentTvInputId(@Nullable String);
+ method public void sendCurrentVideoBounds(@NonNull android.graphics.Rect);
+ method public void sendSigningResult(@NonNull String, @NonNull byte[]);
+ method public void sendTrackInfoList(@Nullable java.util.List<android.media.tv.TvTrackInfo>);
+ method public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.TvAdCallback);
method public void setOnUnhandledInputEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.ad.TvAdView.OnUnhandledInputEventListener);
method public boolean setTvView(@Nullable android.media.tv.TvView);
method public void startAdService();
method public void stopAdService();
+ field public static final String ERROR_KEY_ERROR_CODE = "error_code";
+ field public static final String ERROR_KEY_METHOD_NAME = "method_name";
}
public static interface TvAdView.OnUnhandledInputEventListener {
method public boolean onUnhandledInputEvent(@NonNull android.view.InputEvent);
}
+ public abstract static class TvAdView.TvAdCallback {
+ ctor public TvAdView.TvAdCallback();
+ method public void onRequestCurrentChannelUri(@NonNull String);
+ method public void onRequestCurrentTvInputId(@NonNull String);
+ method public void onRequestCurrentVideoBounds(@NonNull String);
+ method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
+ method public void onRequestTrackInfoList(@NonNull String);
+ method public void onStateChanged(@NonNull String, int, int);
+ }
+
}
package android.media.tv.interactive {
@@ -35352,6 +35527,7 @@
ctor public CallLog.Calls();
method public static String getLastOutgoingCall(android.content.Context);
field public static final int ANSWERED_EXTERNALLY_TYPE = 7; // 0x7
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String ASSERTED_DISPLAY_NAME = "asserted_display_name";
field public static final long AUTO_MISSED_EMERGENCY_CALL = 1L; // 0x1L
field public static final long AUTO_MISSED_MAXIMUM_DIALING = 4L; // 0x4L
field public static final long AUTO_MISSED_MAXIMUM_RINGING = 2L; // 0x2L
@@ -35398,6 +35574,7 @@
field public static final int FEATURES_WIFI = 8; // 0x8
field public static final String GEOCODED_LOCATION = "geocoded_location";
field public static final int INCOMING_TYPE = 1; // 0x1
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String IS_BUSINESS_CALL = "is_business_call";
field public static final String IS_READ = "is_read";
field public static final String LAST_MODIFIED = "last_modified";
field public static final String LIMIT_PARAM_KEY = "limit";
@@ -35446,7 +35623,7 @@
field public static final String LONGITUDE = "longitude";
}
- @FlaggedApi("android.provider.user_keys") public class ContactKeysManager {
+ @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager {
method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.ContactKey> getAllContactKeys(@NonNull String);
method @NonNull @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public java.util.List<android.provider.ContactKeysManager.SelfKey> getAllSelfKeys();
method @Nullable @RequiresPermission(android.Manifest.permission.READ_CONTACTS) public android.provider.ContactKeysManager.ContactKey getContactKey(@NonNull String, @NonNull String, @NonNull String);
@@ -35461,9 +35638,9 @@
method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public void updateOrInsertContactKey(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateOrInsertSelfKey(@NonNull String, @NonNull String, @NonNull byte[]);
method @RequiresPermission(android.Manifest.permission.WRITE_CONTACTS) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, int);
- field public static final int UNVERIFIED = 0; // 0x0
- field public static final int VERIFICATION_FAILED = 1; // 0x1
- field public static final int VERIFIED = 2; // 0x2
+ field public static final int VERIFICATION_STATE_UNVERIFIED = 0; // 0x0
+ field public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1; // 0x1
+ field public static final int VERIFICATION_STATE_VERIFIED = 2; // 0x2
}
public static final class ContactKeysManager.ContactKey implements android.os.Parcelable {
@@ -36960,6 +37137,7 @@
field public static final String ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
field public static final String ACTION_REQUEST_MANAGE_MEDIA = "android.settings.REQUEST_MANAGE_MEDIA";
field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL";
+ field @FlaggedApi("android.provider.backup_tasks_settings_screen") public static final String ACTION_REQUEST_RUN_BACKUP_JOBS = "android.settings.REQUEST_RUN_BACKUP_JOBS";
field public static final String ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM";
field public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE = "android.settings.REQUEST_SET_AUTOFILL_SERVICE";
field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING";
@@ -40110,6 +40288,19 @@
method @NonNull public android.service.chooser.ChooserAction build();
}
+ @FlaggedApi("android.service.chooser.enable_chooser_result") public final class ChooserResult implements android.os.Parcelable {
+ method public int describeContents();
+ method @Nullable public android.content.ComponentName getSelectedComponent();
+ method public int getType();
+ method public boolean isShortcut();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field public static final int CHOOSER_RESULT_COPY = 1; // 0x1
+ field public static final int CHOOSER_RESULT_EDIT = 2; // 0x2
+ field public static final int CHOOSER_RESULT_SELECTED_COMPONENT = 0; // 0x0
+ field public static final int CHOOSER_RESULT_UNKNOWN = -1; // 0xffffffff
+ field @NonNull public static final android.os.Parcelable.Creator<android.service.chooser.ChooserResult> CREATOR;
+ }
+
@Deprecated public final class ChooserTarget implements android.os.Parcelable {
ctor @Deprecated public ChooserTarget(CharSequence, android.graphics.drawable.Icon, float, android.content.ComponentName, @Nullable android.os.Bundle);
method @Deprecated public int describeContents();
@@ -41874,8 +42065,10 @@
field @Deprecated public static final String AVAILABLE_PHONE_ACCOUNTS = "selectPhoneAccountAccounts";
field public static final String EVENT_CLEAR_DIAGNOSTIC_MESSAGE = "android.telecom.event.CLEAR_DIAGNOSTIC_MESSAGE";
field public static final String EVENT_DISPLAY_DIAGNOSTIC_MESSAGE = "android.telecom.event.DISPLAY_DIAGNOSTIC_MESSAGE";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_ASSERTED_DISPLAY_NAME = "android.telecom.extra.ASSERTED_DISPLAY_NAME";
field public static final String EXTRA_DIAGNOSTIC_MESSAGE = "android.telecom.extra.DIAGNOSTIC_MESSAGE";
field public static final String EXTRA_DIAGNOSTIC_MESSAGE_ID = "android.telecom.extra.DIAGNOSTIC_MESSAGE_ID";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_IS_BUSINESS_CALL = "android.telecom.extra.IS_BUSINESS_CALL";
field public static final String EXTRA_IS_SUPPRESSED_BY_DO_NOT_DISTURB = "android.telecom.extra.IS_SUPPRESSED_BY_DO_NOT_DISTURB";
field public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS = "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
field public static final String EXTRA_SILENT_RINGING_REQUESTED = "android.telecom.extra.SILENT_RINGING_REQUESTED";
@@ -43503,6 +43696,7 @@
field public static final String KEY_SMS_REQUIRES_DESTINATION_NUMBER_CONVERSION_BOOL = "sms_requires_destination_number_conversion_bool";
field public static final String KEY_SUBSCRIPTION_GROUP_UUID_STRING = "subscription_group_uuid_string";
field public static final String KEY_SUPPORTED_PREMIUM_CAPABILITIES_INT_ARRAY = "supported_premium_capabilities_int_array";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL = "supports_business_call_composer_bool";
field public static final String KEY_SUPPORTS_CALL_COMPOSER_BOOL = "supports_call_composer_bool";
field public static final String KEY_SUPPORTS_DEVICE_TO_DEVICE_COMMUNICATION_USING_DTMF_BOOL = "supports_device_to_device_communication_using_dtmf_bool";
field public static final String KEY_SUPPORTS_DEVICE_TO_DEVICE_COMMUNICATION_USING_RTP_BOOL = "supports_device_to_device_communication_using_rtp_bool";
@@ -45768,6 +45962,7 @@
field public static final int AUTHTYPE_EAP_SIM = 128; // 0x80
field public static final int AUTHTYPE_GBA_BOOTSTRAP = 132; // 0x84
field public static final int AUTHTYPE_GBA_NAF_KEY_EXTERNAL = 133; // 0x85
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final int CALL_COMPOSER_STATUS_BUSINESS_ONLY = 2; // 0x2
field public static final int CALL_COMPOSER_STATUS_OFF = 0; // 0x0
field public static final int CALL_COMPOSER_STATUS_ON = 1; // 0x1
field public static final int CALL_STATE_IDLE = 0; // 0x0
@@ -46804,6 +46999,7 @@
public static class MmTelFeature.MmTelCapabilities {
method public final boolean isCapable(int);
field public static final int CAPABILITY_TYPE_CALL_COMPOSER = 16; // 0x10
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final int CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY = 32; // 0x20
field public static final int CAPABILITY_TYPE_SMS = 8; // 0x8
field public static final int CAPABILITY_TYPE_UT = 4; // 0x4
field public static final int CAPABILITY_TYPE_VIDEO = 2; // 0x2
@@ -52202,6 +52398,7 @@
method public final boolean getClipToOutline();
method @Nullable public final android.view.contentcapture.ContentCaptureSession getContentCaptureSession();
method public CharSequence getContentDescription();
+ method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final int getContentSensitivity();
method @UiContext public final android.content.Context getContext();
method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo();
method public final boolean getDefaultFocusHighlightEnabled();
@@ -52381,6 +52578,7 @@
method public boolean isAttachedToWindow();
method public boolean isAutoHandwritingEnabled();
method public boolean isClickable();
+ method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final boolean isContentSensitive();
method public boolean isContextClickable();
method public boolean isCredential();
method public boolean isDirty();
@@ -52585,6 +52783,7 @@
method public void setClipToOutline(boolean);
method public void setContentCaptureSession(@Nullable android.view.contentcapture.ContentCaptureSession);
method public void setContentDescription(CharSequence);
+ method @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public final void setContentSensitivity(int);
method public void setContextClickable(boolean);
method public void setDefaultFocusHighlightEnabled(boolean);
method @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int);
@@ -52769,13 +52968,18 @@
field public static final int AUTOFILL_TYPE_NONE = 0; // 0x0
field public static final int AUTOFILL_TYPE_TEXT = 1; // 0x1
field public static final int AUTOFILL_TYPE_TOGGLE = 2; // 0x2
+ field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_AUTO = 0; // 0x0
+ field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 2; // 0x2
+ field @FlaggedApi("android.view.flags.sensitive_content_app_protection_api") public static final int CONTENT_SENSITIVITY_SENSITIVE = 1; // 0x1
field public static final int DRAG_FLAG_ACCESSIBILITY_ACTION = 1024; // 0x400
field public static final int DRAG_FLAG_GLOBAL = 256; // 0x100
field public static final int DRAG_FLAG_GLOBAL_PERSISTABLE_URI_PERMISSION = 64; // 0x40
field public static final int DRAG_FLAG_GLOBAL_PREFIX_URI_PERMISSION = 128; // 0x80
+ field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 4096; // 0x1000
field public static final int DRAG_FLAG_GLOBAL_URI_READ = 1; // 0x1
field public static final int DRAG_FLAG_GLOBAL_URI_WRITE = 2; // 0x2
field public static final int DRAG_FLAG_OPAQUE = 512; // 0x200
+ field @FlaggedApi("com.android.window.flags.delegate_unhandled_drags") public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 8192; // 0x2000
field @Deprecated public static final int DRAWING_CACHE_QUALITY_AUTO = 0; // 0x0
field @Deprecated public static final int DRAWING_CACHE_QUALITY_HIGH = 1048576; // 0x100000
field @Deprecated public static final int DRAWING_CACHE_QUALITY_LOW = 524288; // 0x80000
@@ -53865,8 +54069,11 @@
method @Deprecated @NonNull public android.view.WindowInsets consumeDisplayCutout();
method @Deprecated @NonNull public android.view.WindowInsets consumeStableInsets();
method @Deprecated @NonNull public android.view.WindowInsets consumeSystemWindowInsets();
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public java.util.List<android.graphics.Rect> getBoundingRects(int);
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public java.util.List<android.graphics.Rect> getBoundingRectsIgnoringVisibility(int);
method @Nullable public android.view.DisplayCutout getDisplayCutout();
method @Nullable public android.view.DisplayShape getDisplayShape();
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.util.Size getFrame();
method @NonNull public android.graphics.Insets getInsets(int);
method @NonNull public android.graphics.Insets getInsetsIgnoringVisibility(int);
method @Deprecated @NonNull public android.graphics.Insets getMandatorySystemGestureInsets();
@@ -53901,8 +54108,11 @@
ctor public WindowInsets.Builder();
ctor public WindowInsets.Builder(@NonNull android.view.WindowInsets);
method @NonNull public android.view.WindowInsets build();
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setBoundingRects(int, @NonNull java.util.List<android.graphics.Rect>);
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setBoundingRectsIgnoringVisibility(int, @NonNull java.util.List<android.graphics.Rect>);
method @NonNull public android.view.WindowInsets.Builder setDisplayCutout(@Nullable android.view.DisplayCutout);
method @NonNull public android.view.WindowInsets.Builder setDisplayShape(@NonNull android.view.DisplayShape);
+ method @FlaggedApi("android.view.flags.customizable_window_headers") @NonNull public android.view.WindowInsets.Builder setFrame(int, int);
method @NonNull public android.view.WindowInsets.Builder setInsets(int, @NonNull android.graphics.Insets);
method @NonNull public android.view.WindowInsets.Builder setInsetsIgnoringVisibility(int, @NonNull android.graphics.Insets) throws java.lang.IllegalArgumentException;
method @Deprecated @NonNull public android.view.WindowInsets.Builder setMandatorySystemGestureInsets(@NonNull android.graphics.Insets);
@@ -55858,6 +56068,7 @@
public final class InputMethodInfo implements android.os.Parcelable {
ctor public InputMethodInfo(android.content.Context, android.content.pm.ResolveInfo) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
ctor public InputMethodInfo(String, String, CharSequence, String);
+ method @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") @Nullable public android.content.Intent createImeLanguageSettingsActivityIntent();
method @Nullable public android.content.Intent createStylusHandwritingSettingsActivityIntent();
method public int describeContents();
method public void dump(android.util.Printer, String);
@@ -55877,6 +56088,7 @@
method public boolean supportsStylusHandwriting();
method public boolean suppressesSpellChecker();
method public void writeToParcel(android.os.Parcel, int);
+ field @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") public static final String ACTION_IME_LANGUAGE_SETTINGS = "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS";
field public static final String ACTION_STYLUS_HANDWRITING_SETTINGS = "android.view.inputmethod.action.STYLUS_HANDWRITING_SETTINGS";
field @NonNull public static final android.os.Parcelable.Creator<android.view.inputmethod.InputMethodInfo> CREATOR;
}
@@ -60487,6 +60699,7 @@
method public boolean isFallbackLineSpacing();
method public final boolean isHorizontallyScrollable();
method public boolean isInputMethodTarget();
+ method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public boolean isLocalePreferredLineHeightForMinimumUsed();
method public boolean isSingleLine();
method public boolean isSuggestionsEnabled();
method public boolean isTextSelectable();
@@ -60569,6 +60782,7 @@
method public final void setLinkTextColor(@ColorInt int);
method public final void setLinkTextColor(android.content.res.ColorStateList);
method public final void setLinksClickable(boolean);
+ method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void setLocalePreferredLineHeightForMinimumUsed(boolean);
method public void setMarqueeRepeatLimit(int);
method public void setMaxEms(int);
method public void setMaxHeight(int);
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index c10cc73..3d01ef6 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -282,6 +282,7 @@
field public static final String RADIO_SCAN_WITHOUT_LOCATION = "android.permission.RADIO_SCAN_WITHOUT_LOCATION";
field public static final String READ_ACTIVE_EMERGENCY_SESSION = "android.permission.READ_ACTIVE_EMERGENCY_SESSION";
field public static final String READ_APP_SPECIFIC_LOCALES = "android.permission.READ_APP_SPECIFIC_LOCALES";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String READ_BLOCKED_NUMBERS = "android.permission.READ_BLOCKED_NUMBERS";
field public static final String READ_CARRIER_APP_INFO = "android.permission.READ_CARRIER_APP_INFO";
field public static final String READ_CELL_BROADCASTS = "android.permission.READ_CELL_BROADCASTS";
field public static final String READ_CLIPBOARD_IN_BACKGROUND = "android.permission.READ_CLIPBOARD_IN_BACKGROUND";
@@ -409,6 +410,7 @@
field public static final String WIFI_UPDATE_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS";
field public static final String WIFI_UPDATE_USABILITY_STATS_SCORE = "android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE";
field public static final String WRITE_ALLOWLISTED_DEVICE_CONFIG = "android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String WRITE_BLOCKED_NUMBERS = "android.permission.WRITE_BLOCKED_NUMBERS";
field public static final String WRITE_DEVICE_CONFIG = "android.permission.WRITE_DEVICE_CONFIG";
field public static final String WRITE_DREAM_STATE = "android.permission.WRITE_DREAM_STATE";
field public static final String WRITE_EMBEDDED_SUBSCRIPTIONS = "android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS";
@@ -1389,6 +1391,7 @@
field public static final int STATUS_DEVICE_ADMIN_NOT_SUPPORTED = 13; // 0xd
field public static final int STATUS_HAS_DEVICE_OWNER = 1; // 0x1
field public static final int STATUS_HAS_PAIRED = 8; // 0x8
+ field @FlaggedApi("android.app.admin.flags.headless_device_owner_single_user_enabled") public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17; // 0x11
field public static final int STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED = 16; // 0x10
field public static final int STATUS_MANAGED_USERS_NOT_SUPPORTED = 9; // 0x9
field public static final int STATUS_NONSYSTEM_USER_EXISTS = 5; // 0x5
@@ -2204,6 +2207,7 @@
method public void notifyLaunchLocationShown(@NonNull String, @NonNull java.util.List<android.app.prediction.AppTargetId>);
method public void registerPredictionUpdates(@NonNull java.util.concurrent.Executor, @NonNull android.app.prediction.AppPredictor.Callback);
method public void requestPredictionUpdate();
+ method @FlaggedApi("android.service.appprediction.flags.service_features_api") public void requestServiceFeatures(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.os.Bundle>);
method @Nullable public void sortTargets(@NonNull java.util.List<android.app.prediction.AppTarget>, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>);
method public void unregisterPredictionUpdates(@NonNull android.app.prediction.AppPredictor.Callback);
}
@@ -4359,10 +4363,12 @@
public final class DomainVerificationManager {
method @Nullable @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public android.content.pm.verify.domain.DomainVerificationInfo getDomainVerificationInfo(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public java.util.SortedSet<android.content.pm.verify.domain.DomainOwner> getOwnersForDomain(@NonNull String);
+ method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @NonNull public java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> getUriRelativeFilterGroups(@NonNull String, @NonNull java.util.List<java.lang.String>);
method @NonNull @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public java.util.List<java.lang.String> queryValidVerificationPackageNames();
method @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public void setDomainVerificationLinkHandlingAllowed(@NonNull String, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
method @CheckResult @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public int setDomainVerificationStatus(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, int) throws android.content.pm.PackageManager.NameNotFoundException;
method @CheckResult @RequiresPermission(android.Manifest.permission.UPDATE_DOMAIN_VERIFICATION_USER_SELECTION) public int setDomainVerificationUserSelection(@NonNull java.util.UUID, @NonNull java.util.Set<java.lang.String>, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
+ method @FlaggedApi("android.content.pm.relative_reference_intent_filters") @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT) public void setUriRelativeFilterGroups(@NonNull String, @NonNull java.util.Map<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>>);
field public static final int ERROR_DOMAIN_SET_ID_INVALID = 1; // 0x1
field public static final int ERROR_UNABLE_TO_APPROVE = 3; // 0x3
field public static final int ERROR_UNKNOWN_DOMAIN = 2; // 0x2
@@ -5676,6 +5682,7 @@
method @NonNull public android.hardware.location.ContextHubInfo getAttachedHub();
method @IntRange(from=0, to=65535) public int getId();
method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int sendMessageToNanoApp(@NonNull android.hardware.location.NanoAppMessage);
+ method @FlaggedApi("android.chre.flags.reliable_message") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> sendReliableMessageToNanoApp(@NonNull android.hardware.location.NanoAppMessage);
}
public class ContextHubClientCallback {
@@ -5711,6 +5718,7 @@
method public String getToolchain();
method public int getToolchainVersion();
method public String getVendor();
+ method @FlaggedApi("android.chre.flags.reliable_message") public boolean supportsReliableMessages();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.location.ContextHubInfo> CREATOR;
}
@@ -5794,6 +5802,7 @@
field public static final int RESULT_FAILED_BAD_PARAMS = 2; // 0x2
field public static final int RESULT_FAILED_BUSY = 4; // 0x4
field public static final int RESULT_FAILED_HAL_UNAVAILABLE = 8; // 0x8
+ field @FlaggedApi("android.chre.flags.reliable_message") public static final int RESULT_FAILED_NOT_SUPPORTED = 9; // 0x9
field public static final int RESULT_FAILED_SERVICE_INTERNAL_FAILURE = 7; // 0x7
field public static final int RESULT_FAILED_TIMEOUT = 6; // 0x6
field public static final int RESULT_FAILED_UNINITIALIZED = 3; // 0x3
@@ -5803,6 +5812,7 @@
field public static final int TYPE_ENABLE_NANOAPP = 2; // 0x2
field public static final int TYPE_LOAD_NANOAPP = 0; // 0x0
field public static final int TYPE_QUERY_NANOAPPS = 4; // 0x4
+ field @FlaggedApi("android.chre.flags.reliable_message") public static final int TYPE_RELIABLE_MESSAGE = 5; // 0x5
field public static final int TYPE_UNLOAD_NANOAPP = 1; // 0x1
}
@@ -5985,12 +5995,17 @@
public final class NanoAppMessage implements android.os.Parcelable {
method public static android.hardware.location.NanoAppMessage createMessageFromNanoApp(long, int, byte[], boolean);
+ method @FlaggedApi("android.chre.flags.reliable_message") @NonNull public static android.hardware.location.NanoAppMessage createMessageFromNanoApp(long, int, @NonNull byte[], boolean, boolean, int);
method public static android.hardware.location.NanoAppMessage createMessageToNanoApp(long, int, byte[]);
method public int describeContents();
method public byte[] getMessageBody();
+ method @FlaggedApi("android.chre.flags.reliable_message") public int getMessageSequenceNumber();
method public int getMessageType();
method public long getNanoAppId();
method public boolean isBroadcastMessage();
+ method @FlaggedApi("android.chre.flags.reliable_message") public boolean isReliable();
+ method @FlaggedApi("android.chre.flags.reliable_message") public void setIsReliable(boolean);
+ method @FlaggedApi("android.chre.flags.reliable_message") public void setMessageSequenceNumber(int);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.location.NanoAppMessage> CREATOR;
}
@@ -11053,6 +11068,7 @@
field public static final String USER_TYPE_FULL_SYSTEM = "android.os.usertype.full.SYSTEM";
field public static final String USER_TYPE_PROFILE_CLONE = "android.os.usertype.profile.CLONE";
field public static final String USER_TYPE_PROFILE_MANAGED = "android.os.usertype.profile.MANAGED";
+ field @FlaggedApi("android.os.allow_private_profile") public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE";
field public static final String USER_TYPE_SYSTEM_HEADLESS = "android.os.usertype.system.HEADLESS";
}
@@ -11443,6 +11459,29 @@
package android.provider {
+ public static class BlockedNumberContract.BlockedNumbers {
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void endBlockSuppression(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @NonNull @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static android.provider.BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus getBlockSuppressionStatus(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static boolean getBlockedNumberSetting(@NonNull android.content.Context, @NonNull String);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void notifyEmergencyContact(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static void setBlockedNumberSetting(@NonNull android.content.Context, @NonNull String, boolean);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static boolean shouldShowEmergencyCallNotification(@NonNull android.content.Context);
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(allOf={android.Manifest.permission.READ_BLOCKED_NUMBERS, android.Manifest.permission.WRITE_BLOCKED_NUMBERS}) public static int shouldSystemBlockNumber(@NonNull android.content.Context, @NonNull String, int, boolean);
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ACTION_BLOCK_SUPPRESSION_STATE_CHANGED = "android.provider.action.BLOCK_SUPPRESSION_STATE_CHANGED";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_PAYPHONE = "block_payphone_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_PRIVATE = "block_private_number_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE = "block_unavailable_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNKNOWN = "block_unknown_calls_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED = "block_numbers_not_in_contacts_setting";
+ field @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final String ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION = "show_emergency_call_notification";
+ }
+
+ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final class BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus {
+ ctor public BlockedNumberContract.BlockedNumbers.BlockSuppressionStatus(boolean, long);
+ method public boolean getIsSuppressed();
+ method public long getUntilTimestampMillis();
+ }
+
public class CallLog {
method @RequiresPermission(allOf={android.Manifest.permission.WRITE_CALL_LOG, android.Manifest.permission.INTERACT_ACROSS_USERS}) public static void storeCallComposerPicture(@NonNull android.content.Context, @NonNull java.io.InputStream, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.net.Uri,android.provider.CallLog.CallComposerLoggingException>);
}
@@ -11456,7 +11495,7 @@
field public static final int ERROR_UNKNOWN = 0; // 0x0
}
- @FlaggedApi("android.provider.user_keys") public class ContactKeysManager {
+ @FlaggedApi("android.provider.user_keys") public final class ContactKeysManager {
method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyLocalVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateContactKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, @NonNull String, int);
method @RequiresPermission(allOf={android.Manifest.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS, android.Manifest.permission.WRITE_CONTACTS}) public boolean updateSelfKeyRemoteVerificationState(@NonNull String, @NonNull String, @NonNull String, int);
@@ -12046,6 +12085,7 @@
method @MainThread public void onDestroyPredictionSession(@NonNull android.app.prediction.AppPredictionSessionId);
method @MainThread public abstract void onLaunchLocationShown(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull String, @NonNull java.util.List<android.app.prediction.AppTargetId>);
method @MainThread public abstract void onRequestPredictionUpdate(@NonNull android.app.prediction.AppPredictionSessionId);
+ method @FlaggedApi("android.service.appprediction.flags.service_features_api") @MainThread public void onRequestServiceFeatures(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull java.util.function.Consumer<android.os.Bundle>);
method @MainThread public abstract void onSortAppTargets(@NonNull android.app.prediction.AppPredictionSessionId, @NonNull java.util.List<android.app.prediction.AppTarget>, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<java.util.List<android.app.prediction.AppTarget>>);
method @MainThread public void onStartPredictionUpdates();
method @MainThread public void onStopPredictionUpdates();
@@ -16103,6 +16143,7 @@
field public static final int DIALSTRING_USSD = 2; // 0x2
field public static final String EXTRA_ADDITIONAL_CALL_INFO = "AdditionalCallInfo";
field public static final String EXTRA_ADDITIONAL_SIP_INVITE_FIELDS = "android.telephony.ims.extra.ADDITIONAL_SIP_INVITE_FIELDS";
+ field @FlaggedApi("com.android.server.telecom.flags.business_call_composer") public static final String EXTRA_ASSERTED_DISPLAY_NAME = "android.telephony.ims.extra.ASSERTED_DISPLAY_NAME";
field public static final String EXTRA_CALL_DISCONNECT_CAUSE = "android.telephony.ims.extra.CALL_DISCONNECT_CAUSE";
field public static final String EXTRA_CALL_NETWORK_TYPE = "android.telephony.ims.extra.CALL_NETWORK_TYPE";
field @Deprecated public static final String EXTRA_CALL_RAT_TYPE = "CallRadioTech";
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index fc095d4..dd79c4a4 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -104,6 +104,14 @@
method @FlaggedApi("android.view.accessibility.motion_event_observing") public void setObservedMotionEventSources(int);
}
+ @FlaggedApi("android.view.accessibility.braille_display_hid") public interface BrailleDisplayController {
+ method @FlaggedApi("android.view.accessibility.braille_display_hid") @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY) public static void setTestBrailleDisplayData(@NonNull android.accessibilityservice.AccessibilityService, @NonNull java.util.List<android.os.Bundle>);
+ field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+ field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+ field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+ field @FlaggedApi("android.view.accessibility.braille_display_hid") public static final String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+ }
+
}
package android.animation {
@@ -2081,7 +2089,7 @@
package android.media.projection {
public final class MediaProjectionManager {
- method @NonNull public android.content.Intent createScreenCaptureIntent(@Nullable android.app.ActivityOptions.LaunchCookie);
+ method @NonNull public android.content.Intent createScreenCaptureIntent(@NonNull android.app.ActivityOptions.LaunchCookie);
}
}
@@ -2454,7 +2462,6 @@
method public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported();
method public boolean isVisibleBackgroundUsersSupported();
method @Deprecated @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo preCreateUser(@NonNull String) throws android.os.UserManager.UserOperationException;
- field @FlaggedApi("android.os.allow_private_profile") public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE";
}
public final class VibrationAttributes implements android.os.Parcelable {
@@ -3894,7 +3901,7 @@
}
public final class InputMethodInfo implements android.os.Parcelable {
- ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, boolean, @NonNull String);
+ ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, @NonNull String, boolean, @NonNull String);
ctor public InputMethodInfo(@NonNull String, @NonNull String, @NonNull CharSequence, @NonNull String, int);
field public static final int COMPONENT_NAME_MAX_LENGTH = 1000; // 0x3e8
field public static final int MAX_IMES_PER_PACKAGE = 20; // 0x14
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index b938f0f..c1181f5 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -2189,6 +2189,8 @@
New API must be flagged with @FlaggedApi: field android.view.accessibility.AccessibilityWindowInfo.UNDEFINED_WINDOW_ID
UnflaggedApi: android.view.animation.AnimationUtils#lockAnimationClock(long, long):
New API must be flagged with @FlaggedApi: method android.view.animation.AnimationUtils.lockAnimationClock(long,long)
+UnflaggedApi: android.view.inputmethod.InputMethodInfo#InputMethodInfo(String, String, CharSequence, String, String, boolean, String):
+ New API must be flagged with @FlaggedApi: constructor android.view.inputmethod.InputMethodInfo(String,String,CharSequence,String,String,boolean,String)
UnflaggedApi: android.view.inputmethod.InputMethodManager#getEnabledInputMethodListAsUser(android.os.UserHandle):
New API must be flagged with @FlaggedApi: method android.view.inputmethod.InputMethodManager.getEnabledInputMethodListAsUser(android.os.UserHandle)
UnflaggedApi: android.view.inputmethod.InputMethodManager#getEnabledInputMethodSubtypeListAsUser(String, boolean, android.os.UserHandle):
diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java
index 2a7dbab..f7d7522 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -80,6 +80,7 @@
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.IntConsumer;
@@ -364,12 +365,12 @@
public static final int GESTURE_SWIPE_UP_AND_RIGHT = 14;
/**
- * The user has performed an down and left gesture on the touch screen.
+ * The user has performed a down and left gesture on the touch screen.
*/
public static final int GESTURE_SWIPE_DOWN_AND_LEFT = 15;
/**
- * The user has performed an down and right gesture on the touch screen.
+ * The user has performed a down and right gesture on the touch screen.
*/
public static final int GESTURE_SWIPE_DOWN_AND_RIGHT = 16;
@@ -850,6 +851,8 @@
private boolean mInputMethodInitialized = false;
private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
new SparseArray<>(0);
+ private BrailleDisplayController mBrailleDisplayController;
+ private BrailleDisplayController mTestBrailleDisplayController;
private int mGestureStatusCallbackSequence;
@@ -3634,4 +3637,56 @@
.attachAccessibilityOverlayToWindow(
mConnectionId, accessibilityWindowId, sc, executor, callback);
}
+
+ /**
+ * Returns the {@link BrailleDisplayController} which may be used to communicate with
+ * refreshable Braille displays that provide USB or Bluetooth Braille display HID support.
+ */
+ @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @NonNull
+ public BrailleDisplayController getBrailleDisplayController() {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ synchronized (mLock) {
+ if (mTestBrailleDisplayController != null) {
+ return mTestBrailleDisplayController;
+ }
+
+ if (mBrailleDisplayController == null) {
+ mBrailleDisplayController = new BrailleDisplayControllerImpl(this, mLock);
+ }
+ return mBrailleDisplayController;
+ }
+ }
+
+ /**
+ * Set the {@link BrailleDisplayController} implementation that will be returned by
+ * {@link #getBrailleDisplayController}, to allow this accessibility service to test its
+ * interaction with BrailleDisplayController without requiring a real Braille display.
+ *
+ * <p>For full test fidelity, ensure that this test-only implementation follows the same
+ * behavior specified in the documentation for {@link BrailleDisplayController}, including
+ * thrown exceptions.
+ *
+ * @param controller A test-only implementation of {@link BrailleDisplayController}.
+ */
+ @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void setTestBrailleDisplayController(@NonNull BrailleDisplayController controller) {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ Objects.requireNonNull(controller);
+ synchronized (mLock) {
+ mTestBrailleDisplayController = controller;
+ }
+ }
+
+ /**
+ * Clears the {@link BrailleDisplayController} previously set by
+ * {@link #setTestBrailleDisplayController}.
+ */
+ @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void clearTestBrailleDisplayController() {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ synchronized (mLock) {
+ mTestBrailleDisplayController = null;
+ }
+ }
}
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
index fc342fa..8bb2857 100644
--- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
+++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java
@@ -647,11 +647,44 @@
private int mObservedMotionEventSources = 0;
+ // Default values for each dynamic property
+ // LINT.IfChange(dynamic_property_defaults)
+ private final DynamicPropertyDefaults mDynamicPropertyDefaults;
+
+ private static class DynamicPropertyDefaults {
+ private final int mEventTypesDefault;
+ private final List<String> mPackageNamesDefault;
+ private final int mFeedbackTypeDefault;
+ private final long mNotificationTimeoutDefault;
+ private final int mFlagsDefault;
+ private final int mNonInteractiveUiTimeoutDefault;
+ private final int mInteractiveUiTimeoutDefault;
+ private final int mMotionEventSourcesDefault;
+ private final int mObservedMotionEventSourcesDefault;
+
+ DynamicPropertyDefaults(AccessibilityServiceInfo info) {
+ mEventTypesDefault = info.eventTypes;
+ if (info.packageNames != null) {
+ mPackageNamesDefault = List.of(info.packageNames);
+ } else {
+ mPackageNamesDefault = null;
+ }
+ mFeedbackTypeDefault = info.feedbackType;
+ mNotificationTimeoutDefault = info.notificationTimeout;
+ mNonInteractiveUiTimeoutDefault = info.mNonInteractiveUiTimeout;
+ mInteractiveUiTimeoutDefault = info.mInteractiveUiTimeout;
+ mFlagsDefault = info.flags;
+ mMotionEventSourcesDefault = info.mMotionEventSources;
+ mObservedMotionEventSourcesDefault = info.mObservedMotionEventSources;
+ }
+ }
+ // LINT.ThenChange(:dynamic_property_reset)
+
/**
* Creates a new instance.
*/
public AccessibilityServiceInfo() {
- /* do nothing */
+ mDynamicPropertyDefaults = new DynamicPropertyDefaults(this);
}
/**
@@ -758,7 +791,7 @@
}
}
peekedValue = asAttributes.peekValue(
- com.android.internal.R.styleable.AccessibilityService_summary);
+ com.android.internal.R.styleable.AccessibilityService_summary);
if (peekedValue != null) {
mSummaryResId = peekedValue.resourceId;
CharSequence nonLocalizedSummary = peekedValue.coerceToString();
@@ -793,10 +826,38 @@
if (parser != null) {
parser.close();
}
+
+ mDynamicPropertyDefaults = new DynamicPropertyDefaults(this);
}
}
/**
+ * Resets all dynamically configurable properties to their default values.
+ *
+ * @hide
+ */
+ // LINT.IfChange(dynamic_property_reset)
+ public void resetDynamicallyConfigurableProperties() {
+ eventTypes = mDynamicPropertyDefaults.mEventTypesDefault;
+ if (mDynamicPropertyDefaults.mPackageNamesDefault == null) {
+ packageNames = null;
+ } else {
+ packageNames = mDynamicPropertyDefaults.mPackageNamesDefault.toArray(new String[0]);
+ }
+ feedbackType = mDynamicPropertyDefaults.mFeedbackTypeDefault;
+ notificationTimeout = mDynamicPropertyDefaults.mNotificationTimeoutDefault;
+ mNonInteractiveUiTimeout = mDynamicPropertyDefaults.mNonInteractiveUiTimeoutDefault;
+ mInteractiveUiTimeout = mDynamicPropertyDefaults.mInteractiveUiTimeoutDefault;
+ flags = mDynamicPropertyDefaults.mFlagsDefault;
+ mMotionEventSources = mDynamicPropertyDefaults.mMotionEventSourcesDefault;
+ if (Flags.motionEventObserving()) {
+ mObservedMotionEventSources = mDynamicPropertyDefaults
+ .mObservedMotionEventSourcesDefault;
+ }
+ }
+ // LINT.ThenChange(:dynamic_property_update)
+
+ /**
* Updates the properties that an AccessibilityService can change dynamically.
* <p>
* Note: A11y services targeting APIs > Q, it cannot update flagRequestAccessibilityButton
@@ -808,6 +869,7 @@
*
* @hide
*/
+ // LINT.IfChange(dynamic_property_update)
public void updateDynamicallyConfigurableProperties(IPlatformCompat platformCompat,
AccessibilityServiceInfo other) {
if (isRequestAccessibilityButtonChangeEnabled(platformCompat)) {
@@ -828,6 +890,7 @@
// NOTE: Ensure that only properties that are safe to be modified by the service itself
// are included here (regardless of hidden setters, etc.).
}
+ // LINT.ThenChange(:dynamic_property_defaults)
private boolean isRequestAccessibilityButtonChangeEnabled(IPlatformCompat platformCompat) {
if (mResolveInfo == null) {
diff --git a/core/java/android/accessibilityservice/BrailleDisplayController.java b/core/java/android/accessibilityservice/BrailleDisplayController.java
new file mode 100644
index 0000000..5282aa3
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayController.java
@@ -0,0 +1,308 @@
+/*
+ * 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 android.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Used to communicate with a Braille display that supports the Braille display HID standard
+ * (usage page 0x41).
+ *
+ * <p>Only one Braille display may be connected at a time.
+ */
+// This interface doesn't actually own resources. Its I/O connections are owned, monitored,
+// and automatically closed by the system after the accessibility service is disconnected.
+@SuppressLint("NotCloseable")
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+public interface BrailleDisplayController {
+
+ /**
+ * Throw {@link IllegalStateException} if this feature's aconfig flag is disabled.
+ *
+ * @hide
+ */
+ static void checkApiFlagIsEnabled() {
+ if (!Flags.brailleDisplayHid()) {
+ throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+ }
+ }
+
+ /**
+ * Interface provided to {@link BrailleDisplayController} connection methods to
+ * receive callbacks from the system.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ interface BrailleDisplayCallback {
+ /**
+ * The system cannot access connected HID devices.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ int FLAG_ERROR_CANNOT_ACCESS = 1 << 0;
+ /**
+ * A unique Braille display matching the requested properties could not be identified.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ int FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND = 1 << 1;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = "FLAG_ERROR_", value = {
+ FLAG_ERROR_CANNOT_ACCESS,
+ FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND,
+ })
+ @interface ErrorCode {
+ }
+
+ /**
+ * Callback to observe a successful Braille display connection.
+ *
+ * <p>The provided HID report descriptor should be used to understand the input bytes
+ * received from the Braille display via {@link #onInput} and to prepare
+ * the output sent to the Braille display via {@link #write}.
+ *
+ * @param hidDescriptor The HID report descriptor for this Braille display.
+ * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+ * @see #connect(UsbDevice, BrailleDisplayCallback)
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void onConnected(@NonNull byte[] hidDescriptor);
+
+ /**
+ * Callback to observe a failed Braille display connection.
+ *
+ * @param errorFlags A bitmask of error codes for the connection failure.
+ * @see #connect(BluetoothDevice, BrailleDisplayCallback)
+ * @see #connect(UsbDevice, BrailleDisplayCallback)
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void onConnectionFailed(@ErrorCode int errorFlags);
+
+ /**
+ * Callback to observe input bytes from the currently connected Braille display.
+ *
+ * @param input The input bytes from the Braille display, formatted according to the HID
+ * report descriptor and the HIDRAW kernel driver.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void onInput(@NonNull byte[] input);
+
+ /**
+ * Callback to observe when the currently connected Braille display is disconnected by the
+ * system.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void onDisconnected();
+ }
+
+ /**
+ * Connects to the requested bluetooth Braille display using the Braille
+ * display HID standard (usage page 0x41).
+ *
+ * <p>If successful then the HID report descriptor will be provided to
+ * {@link BrailleDisplayCallback#onConnected}
+ * and the Braille display will start sending incoming input bytes to
+ * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+ * then the system will disconnect the Braille display.
+ *
+ * <p>Note that the callbacks will be executed on the main thread using
+ * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+ * {@link #connect(BluetoothDevice, Executor, BrailleDisplayCallback)}.
+ *
+ * @param bluetoothDevice The Braille display device.
+ * @param callback Callbacks used to provide connection results.
+ * @see BrailleDisplayCallback#onConnected
+ * @see BrailleDisplayCallback#onConnectionFailed
+ * @throws IllegalStateException if a Braille display is already connected to this controller.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ void connect(@NonNull BluetoothDevice bluetoothDevice,
+ @NonNull BrailleDisplayCallback callback);
+
+ /**
+ * Connects to the requested bluetooth Braille display using the Braille
+ * display HID standard (usage page 0x41).
+ *
+ * <p>If successful then the HID report descriptor will be provided to
+ * {@link BrailleDisplayCallback#onConnected}
+ * and the Braille display will start sending incoming input bytes to
+ * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+ * then the system will disconnect the Braille display.
+ *
+ * @param bluetoothDevice The Braille display device.
+ * @param callbackExecutor Executor for executing the provided callbacks.
+ * @param callback Callbacks used to provide connection results.
+ * @see BrailleDisplayCallback#onConnected
+ * @see BrailleDisplayCallback#onConnectionFailed
+ * @throws IllegalStateException if a Braille display is already connected to this controller.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ void connect(@NonNull BluetoothDevice bluetoothDevice,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull BrailleDisplayCallback callback);
+
+ /**
+ * Connects to the requested USB Braille display using the Braille
+ * display HID standard (usage page 0x41).
+ *
+ * <p>If successful then the HID report descriptor will be provided to
+ * {@link BrailleDisplayCallback#onConnected}
+ * and the Braille display will start sending incoming input bytes to
+ * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+ * then the system will disconnect the Braille display.
+ *
+ * <p>The accessibility service app must already have approval to access the USB device
+ * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+ *
+ * <p>Note that the callbacks will be executed on the main thread using
+ * {@link AccessibilityService#getMainExecutor()}. To specify the execution thread, use
+ * {@link #connect(UsbDevice, Executor, BrailleDisplayCallback)}.
+ *
+ * @param usbDevice The Braille display device.
+ * @param callback Callbacks used to provide connection results.
+ * @see BrailleDisplayCallback#onConnected
+ * @see BrailleDisplayCallback#onConnectionFailed
+ * @throws SecurityException if the caller does not have USB device approval.
+ * @throws IllegalStateException if a Braille display is already connected to this controller.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void connect(@NonNull UsbDevice usbDevice,
+ @NonNull BrailleDisplayCallback callback);
+
+ /**
+ * Connects to the requested USB Braille display using the Braille
+ * display HID standard (usage page 0x41).
+ *
+ * <p>If successful then the HID report descriptor will be provided to
+ * {@link BrailleDisplayCallback#onConnected}
+ * and the Braille display will start sending incoming input bytes to
+ * {@link BrailleDisplayCallback#onInput}. If there is an error in reading input
+ * then the system will disconnect the Braille display.
+ *
+ * <p>The accessibility service app must already have approval to access the USB device
+ * from the standard {@link android.hardware.usb.UsbManager} access approval process.
+ *
+ * @param usbDevice The Braille display device.
+ * @param callbackExecutor Executor for executing the provided callbacks.
+ * @param callback Callbacks used to provide connection results.
+ * @see BrailleDisplayCallback#onConnected
+ * @see BrailleDisplayCallback#onConnectionFailed
+ * @throws SecurityException if the caller does not have USB device approval.
+ * @throws IllegalStateException if a Braille display is already connected to this controller.
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void connect(@NonNull UsbDevice usbDevice,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull BrailleDisplayCallback callback);
+
+ /**
+ * Returns true if a Braille display is currently connected, otherwise false.
+ *
+ * @see #connect
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ boolean isConnected();
+
+ /**
+ * Writes a HID report to the currently connected Braille display.
+ *
+ * <p>This method returns immediately after dispatching the write request to the system.
+ * If the system experiences an error in writing output (e.g. the Braille display is unplugged
+ * after the system receives the write request but before writing the bytes to the Braille
+ * display) then the system will disconnect the Braille display, which calls
+ * {@link BrailleDisplayCallback#onDisconnected()}.
+ *
+ * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+ * according to the HID report descriptor and the HIDRAW kernel driver.
+ * @throws IOException if there is no currently connected Braille display.
+ * @throws IllegalArgumentException if the buffer exceeds the maximum safe payload size for
+ * binder transactions of
+ * {@link IBinder#getSuggestedMaxIpcSizeBytes()}
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void write(@NonNull byte[] buffer) throws IOException;
+
+ /**
+ * Disconnects from the currently connected Braille display.
+ *
+ * @see #isConnected()
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ void disconnect();
+
+ /**
+ * Provides test Braille display data to be used for automated CTS tests.
+ *
+ * <p>See {@code TEST_BRAILLE_DISPLAY_*} bundle keys.
+ *
+ * @hide
+ */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)
+ @TestApi
+ static void setTestBrailleDisplayData(
+ @NonNull AccessibilityService service,
+ @NonNull List<Bundle> brailleDisplays) {
+ checkApiFlagIsEnabled();
+ final IAccessibilityServiceConnection serviceConnection =
+ AccessibilityInteractionClient.getConnection(service.getConnectionId());
+ if (serviceConnection != null) {
+ try {
+ serviceConnection.setTestBrailleDisplayData(brailleDisplays);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /** @hide */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @TestApi
+ String TEST_BRAILLE_DISPLAY_HIDRAW_PATH = "HIDRAW_PATH";
+ /** @hide */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @TestApi
+ String TEST_BRAILLE_DISPLAY_DESCRIPTOR = "DESCRIPTOR";
+ /** @hide */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @TestApi
+ String TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH = "BUS_BLUETOOTH";
+ /** @hide */
+ @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+ @TestApi
+ String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+}
diff --git a/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
new file mode 100644
index 0000000..cac1dc4
--- /dev/null
+++ b/core/java/android/accessibilityservice/BrailleDisplayControllerImpl.java
@@ -0,0 +1,267 @@
+/*
+ * 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 android.accessibilityservice;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.accessibility.AccessibilityInteractionClient;
+import android.view.accessibility.Flags;
+
+import com.android.internal.util.FunctionalUtils;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Default implementation of {@link BrailleDisplayController}.
+ */
+// BrailleDisplayControllerImpl is not an API, but it implements BrailleDisplayController APIs.
+// This @FlaggedApi annotation tells the linter that this method delegates API checks to its
+// callers.
+@FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
+final class BrailleDisplayControllerImpl implements BrailleDisplayController {
+
+ private final AccessibilityService mAccessibilityService;
+ private final Object mLock;
+
+ private IBrailleDisplayConnection mBrailleDisplayConnection;
+ private Executor mCallbackExecutor;
+ private BrailleDisplayCallback mCallback;
+
+ BrailleDisplayControllerImpl(AccessibilityService accessibilityService,
+ Object lock) {
+ mAccessibilityService = accessibilityService;
+ mLock = lock;
+ }
+
+ @Override
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void connect(@NonNull BluetoothDevice bluetoothDevice,
+ @NonNull BrailleDisplayCallback callback) {
+ connect(bluetoothDevice, mAccessibilityService.getMainExecutor(), callback);
+ }
+
+ @Override
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
+ public void connect(@NonNull BluetoothDevice bluetoothDevice,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull BrailleDisplayCallback callback) {
+ Objects.requireNonNull(bluetoothDevice);
+ Objects.requireNonNull(callbackExecutor);
+ Objects.requireNonNull(callback);
+ connect(serviceConnection -> serviceConnection.connectBluetoothBrailleDisplay(
+ bluetoothDevice.getAddress(), new IBrailleDisplayControllerWrapper()),
+ callbackExecutor, callback);
+ }
+
+ @Override
+ public void connect(@NonNull UsbDevice usbDevice,
+ @NonNull BrailleDisplayCallback callback) {
+ connect(usbDevice, mAccessibilityService.getMainExecutor(), callback);
+ }
+
+ @Override
+ public void connect(@NonNull UsbDevice usbDevice,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull BrailleDisplayCallback callback) {
+ Objects.requireNonNull(usbDevice);
+ Objects.requireNonNull(callbackExecutor);
+ Objects.requireNonNull(callback);
+ connect(serviceConnection -> serviceConnection.connectUsbBrailleDisplay(
+ usbDevice, new IBrailleDisplayControllerWrapper()),
+ callbackExecutor, callback);
+ }
+
+ /**
+ * Shared implementation for the {@code connect()} API methods.
+ *
+ * <p>Performs a blocking call to system_server to create the connection. Success is
+ * returned through {@link BrailleDisplayCallback#onConnected} while normal connection
+ * errors are returned through {@link BrailleDisplayCallback#onConnectionFailed}. This
+ * connection is implemented using cached data from the HIDRAW driver so it returns
+ * quickly without needing to perform any I/O with the Braille display.
+ *
+ * <p>The AIDL call to system_server is blocking (not posted to a handler thread) so
+ * that runtime exceptions signaling abnormal connection errors from API misuse
+ * (e.g. lacking permissions, providing an invalid BluetoothDevice, calling connect
+ * while already connected) are propagated to the API caller.
+ */
+ private void connect(
+ FunctionalUtils.RemoteExceptionIgnoringConsumer<IAccessibilityServiceConnection>
+ createConnection,
+ @NonNull Executor callbackExecutor, @NonNull BrailleDisplayCallback callback) {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ if (isConnected()) {
+ throw new IllegalStateException(
+ "This service already has a connected Braille display");
+ }
+ final IAccessibilityServiceConnection serviceConnection =
+ AccessibilityInteractionClient.getConnection(
+ mAccessibilityService.getConnectionId());
+ if (serviceConnection == null) {
+ throw new IllegalStateException("Accessibility service is not connected");
+ }
+ synchronized (mLock) {
+ mCallbackExecutor = callbackExecutor;
+ mCallback = callback;
+ }
+ try {
+ createConnection.acceptOrThrow(serviceConnection);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean isConnected() {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ return mBrailleDisplayConnection != null;
+ }
+
+ @Override
+ public void write(@NonNull byte[] buffer) throws IOException {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ Objects.requireNonNull(buffer);
+ if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+ // This same check must be performed in the system to prevent reflection misuse,
+ // but perform it here too to prevent unnecessary IPCs from non-reflection callers.
+ throw new IllegalArgumentException("Invalid write buffer size " + buffer.length);
+ }
+ synchronized (mLock) {
+ if (mBrailleDisplayConnection == null) {
+ throw new IOException("Braille display is not connected");
+ }
+ try {
+ mBrailleDisplayConnection.write(buffer);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ @Override
+ public void disconnect() {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ synchronized (mLock) {
+ try {
+ if (mBrailleDisplayConnection != null) {
+ mBrailleDisplayConnection.disconnect();
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } finally {
+ clearConnectionLocked();
+ }
+ }
+ }
+
+ /**
+ * Implementation of the {@code IBrailleDisplayController} AIDL interface provided to
+ * system_server, which system_server uses to pass messages back to this
+ * {@code BrailleDisplayController}.
+ *
+ * <p>Messages from system_server are routed to the {@link BrailleDisplayCallback} callbacks
+ * implemented by the accessibility service.
+ *
+ * <p>Note: Per API Guidelines 7.5 the Binder identity must be cleared before invoking the
+ * callback executor so that Binder identity checks in the callbacks are performed using the
+ * app's identity.
+ */
+ private final class IBrailleDisplayControllerWrapper extends IBrailleDisplayController.Stub {
+ /**
+ * Called when the system successfully connects to a Braille display.
+ */
+ @Override
+ public void onConnected(IBrailleDisplayConnection connection, byte[] hidDescriptor) {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mBrailleDisplayConnection = connection;
+ mCallbackExecutor.execute(() -> mCallback.onConnected(hidDescriptor));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Called when the system is unable to connect to a Braille display.
+ */
+ @Override
+ public void onConnectionFailed(@BrailleDisplayCallback.ErrorCode int errorCode) {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mCallbackExecutor.execute(() -> mCallback.onConnectionFailed(errorCode));
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Called when input is received from the currently connected Braille display.
+ */
+ @Override
+ public void onInput(byte[] input) {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ // Ignore input that arrives after disconnection.
+ if (mBrailleDisplayConnection != null) {
+ mCallbackExecutor.execute(() -> mCallback.onInput(input));
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Called when the currently connected Braille display is disconnected.
+ */
+ @Override
+ public void onDisconnected() {
+ BrailleDisplayController.checkApiFlagIsEnabled();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ mCallbackExecutor.execute(mCallback::onDisconnected);
+ clearConnectionLocked();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ private void clearConnectionLocked() {
+ mBrailleDisplayConnection = null;
+ }
+
+}
diff --git a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
index 96716db..dc5c7f6 100644
--- a/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
+++ b/core/java/android/accessibilityservice/IAccessibilityServiceConnection.aidl
@@ -17,10 +17,12 @@
package android.accessibilityservice;
import android.accessibilityservice.AccessibilityServiceInfo;
+import android.accessibilityservice.IBrailleDisplayController;
import android.accessibilityservice.MagnificationConfig;
import android.content.pm.ParceledListSlice;
import android.graphics.Bitmap;
import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
import android.os.Bundle;
import android.os.RemoteCallback;
import android.view.MagnificationSpec;
@@ -160,4 +162,12 @@
void attachAccessibilityOverlayToDisplay(int interactionId, int displayId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
void attachAccessibilityOverlayToWindow(int interactionId, int accessibilityWindowId, in SurfaceControl sc, IAccessibilityInteractionConnectionCallback callback);
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)")
+ void connectBluetoothBrailleDisplay(in String bluetoothAddress, in IBrailleDisplayController controller);
+
+ void connectUsbBrailleDisplay(in UsbDevice usbDevice, in IBrailleDisplayController controller);
+
+ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_ACCESSIBILITY)")
+ void setTestBrailleDisplayData(in List<Bundle> brailleDisplays);
}
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
new file mode 100644
index 0000000..ec4d7b1
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayConnection.aidl
@@ -0,0 +1,12 @@
+package android.accessibilityservice;
+
+/**
+ * Interface given to a BrailleDisplayController to talk to a BrailleDisplayConnection
+ * in system_server.
+ *
+ * @hide
+ */
+interface IBrailleDisplayConnection {
+ oneway void disconnect();
+ oneway void write(in byte[] output);
+}
\ No newline at end of file
diff --git a/core/java/android/accessibilityservice/IBrailleDisplayController.aidl b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
new file mode 100644
index 0000000..7a5d83e
--- /dev/null
+++ b/core/java/android/accessibilityservice/IBrailleDisplayController.aidl
@@ -0,0 +1,17 @@
+package android.accessibilityservice;
+
+import android.accessibilityservice.IBrailleDisplayConnection;
+
+/**
+ * Interface given to a BrailleDisplayConnection to talk to a BrailleDisplayController
+ * in an accessibility service.
+ *
+ * IPCs from system_server to apps must be oneway, so designate this entire interface as oneway.
+ * @hide
+ */
+oneway interface IBrailleDisplayController {
+ void onConnected(in IBrailleDisplayConnection connection, in byte[] hidDescriptor);
+ void onConnectionFailed(int error);
+ void onInput(in byte[] input);
+ void onDisconnected();
+}
\ No newline at end of file
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 2c00c99..de6a848 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1232,6 +1232,15 @@
}
@Override
+ public final void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ "scheduleTimeoutServiceForType. token=" + token);
+ }
+ sendMessage(H.TIMEOUT_SERVICE_FOR_TYPE, token, startId, fgsType);
+ }
+
+ @Override
public final void bindApplication(
String processName,
ApplicationInfo appInfo,
@@ -2288,6 +2297,8 @@
public static final int INSTRUMENT_WITHOUT_RESTART = 170;
public static final int FINISH_INSTRUMENTATION_WITHOUT_RESTART = 171;
+ public static final int TIMEOUT_SERVICE_FOR_TYPE = 172;
+
String codeToString(int code) {
if (DEBUG_MESSAGES) {
switch (code) {
@@ -2341,6 +2352,7 @@
case DUMP_RESOURCES: return "DUMP_RESOURCES";
case TIMEOUT_SERVICE: return "TIMEOUT_SERVICE";
case PING: return "PING";
+ case TIMEOUT_SERVICE_FOR_TYPE: return "TIMEOUT_SERVICE_FOR_TYPE";
}
}
return Integer.toString(code);
@@ -2427,6 +2439,14 @@
case PING:
((RemoteCallback) msg.obj).sendResult(null);
break;
+ case TIMEOUT_SERVICE_FOR_TYPE:
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
+ "serviceTimeoutForType: " + msg.obj);
+ }
+ handleTimeoutServiceForType((IBinder) msg.obj, msg.arg1, msg.arg2);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+ break;
case CONFIGURATION_CHANGED:
mConfigurationController.handleConfigurationChanged((Configuration) msg.obj);
break;
@@ -5136,6 +5156,26 @@
Slog.wtf(TAG, "handleTimeoutService: token=" + token + " not found.");
}
}
+
+ private void handleTimeoutServiceForType(IBinder token, int startId, int fgsType) {
+ Service s = mServices.get(token);
+ if (s != null) {
+ try {
+ if (localLOGV) Slog.v(TAG, "Timeout service " + s);
+
+ s.callOnTimeLimitExceeded(startId, fgsType);
+ } catch (Exception e) {
+ if (!mInstrumentation.onException(s, e)) {
+ throw new RuntimeException(
+ "Unable to call onTimeLimitExceeded on service " + s + ": " + e, e);
+ }
+ Slog.i(TAG, "handleTimeoutServiceForType: exception for " + token, e);
+ }
+ } else {
+ Slog.wtf(TAG, "handleTimeoutServiceForType: token=" + token + " not found.");
+ }
+ }
+
/**
* Resume the activity.
* @param r Target activity record.
diff --git a/core/java/android/app/ComponentCaller.java b/core/java/android/app/ComponentCaller.java
index a440dbc..44e8a0a 100644
--- a/core/java/android/app/ComponentCaller.java
+++ b/core/java/android/app/ComponentCaller.java
@@ -42,6 +42,9 @@
private final IBinder mActivityToken;
private final IBinder mCallerToken;
+ /**
+ * @hide
+ */
public ComponentCaller(@NonNull IBinder activityToken, @Nullable IBinder callerToken) {
mActivityToken = activityToken;
mCallerToken = callerToken;
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index ceeaf5d..cc0aafd 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -942,6 +942,8 @@
/** Returns if the service is a short-service is still "alive" and past the timeout. */
boolean shouldServiceTimeOut(in ComponentName className, in IBinder token);
+ /** Returns if the service has a time-limit restricted type and is past the time limit. */
+ boolean hasServiceTimeLimitExceeded(in ComponentName className, in IBinder token);
void registerUidFrozenStateChangedCallback(in IUidFrozenStateChangedCallback callback);
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)")
diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl
index 59e0e99..a04620c 100644
--- a/core/java/android/app/IApplicationThread.aidl
+++ b/core/java/android/app/IApplicationThread.aidl
@@ -178,5 +178,6 @@
in TranslationSpec targetSpec, in List<AutofillId> viewIds,
in UiTranslationSpec uiTranslationSpec);
void scheduleTimeoutService(IBinder token, int startId);
+ void scheduleTimeoutServiceForType(IBinder token, int startId, int fgsType);
void schedulePing(in RemoteCallback pong);
}
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 24a5157..6255260 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -550,7 +550,7 @@
@UnsupportedAppUsage
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key,
@Nullable ApkAssetsSupplier apkSupplier) {
- final AssetManager.Builder builder = new AssetManager.Builder();
+ final AssetManager.Builder builder = new AssetManager.Builder().setNoInit();
final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
for (int i = 0, n = apkKeys.size(); i < n; i++) {
@@ -1555,7 +1555,7 @@
} else if(overlayPaths == null) {
return ArrayUtils.cloneOrNull(resourceDirs);
} else {
- final ArrayList<String> paths = new ArrayList<>();
+ final var paths = new ArrayList<String>(overlayPaths.length + resourceDirs.length);
for (final String path : overlayPaths) {
paths.add(path);
}
diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java
index a155457..d470299 100644
--- a/core/java/android/app/Service.java
+++ b/core/java/android/app/Service.java
@@ -20,6 +20,7 @@
import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
import static android.text.TextUtils.formatSimple;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1161,4 +1162,37 @@
*/
public void onTimeout(int startId) {
}
+
+ /** @hide */
+ public final void callOnTimeLimitExceeded(int startId, int fgsType) {
+ // Note, because all the service callbacks (and other similar callbacks, e.g. activity
+ // callbacks) are delivered using the main handler, it's possible the service is already
+ // stopped when before this method is called, so we do a double check here.
+ if (mToken == null) {
+ Log.w(TAG, "Service already destroyed, skipping onTimeLimitExceeded()");
+ return;
+ }
+ try {
+ if (!mActivityManager.hasServiceTimeLimitExceeded(
+ new ComponentName(this, mClassName), mToken)) {
+ Log.w(TAG, "Service no longer relevant, skipping onTimeLimitExceeded()");
+ return;
+ }
+ } catch (RemoteException ex) {
+ }
+ if (Flags.introduceNewServiceOntimeoutCallback()) {
+ onTimeout(startId, fgsType);
+ }
+ }
+
+ /**
+ * Callback called when a particular foreground service type has timed out.
+ *
+ * @param startId the startId passed to {@link #onStartCommand(Intent, int, int)} when
+ * the service started.
+ * @param fgsType the foreground service type which caused the timeout.
+ */
+ @FlaggedApi(Flags.FLAG_INTRODUCE_NEW_SERVICE_ONTIMEOUT_CALLBACK)
+ public void onTimeout(int startId, int fgsType) {
+ }
}
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index c0b299b..ff23f09 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -27,3 +27,10 @@
description: "API to add OnUidImportanceListener with targetted UIDs"
bug: "286258140"
}
+
+flag {
+ namespace: "backstage_power"
+ name: "introduce_new_service_ontimeout_callback"
+ description: "Add a new callback in Service to indicate a FGS has reached its timeout."
+ bug: "317799821"
+}
diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java
index 14462b8..7d5d5c1 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -16,8 +16,10 @@
package android.app.admin;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.app.admin.flags.Flags;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
@@ -176,7 +178,18 @@
*/
public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1;
- @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED})
+ /**
+ * Value for {@link #getHeadlessDeviceOwnerMode} which indicates that this DPC should be
+ * provisioned into the first secondary user when on a Headless System User Mode device.
+ *
+ * <p>This mode only allows a single secondary user on the device blocking the creation of
+ * additional secondary users.
+ */
+ @FlaggedApi(Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
+ public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2;
+
+ @IntDef({HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED, HEADLESS_DEVICE_OWNER_MODE_AFFILIATED,
+ HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER})
@Retention(RetentionPolicy.SOURCE)
private @interface HeadlessDeviceOwnerMode {}
@@ -373,6 +386,8 @@
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED;
} else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) {
mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
+ } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) {
+ mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
} else {
throw new XmlPullParserException("headless-system-user mode must be valid");
}
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index c8762c6..c649e62 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -84,6 +84,7 @@
import android.app.IServiceConnection;
import android.app.KeyguardManager;
import android.app.admin.SecurityLog.SecurityEvent;
+import android.app.admin.flags.Flags;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
@@ -2863,6 +2864,19 @@
public static final int STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED = 16;
/**
+ * Result code for {@link #checkProvisioningPrecondition}.
+ *
+ * <p>Returned for {@link #ACTION_PROVISION_MANAGED_DEVICE} when provisioning a DPC into the
+ * {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER} mode but only the system
+ * user exists on the device.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED)
+ public static final int STATUS_HEADLESS_ONLY_SYSTEM_USER = 17;
+
+ /**
* Result codes for {@link #checkProvisioningPrecondition} indicating all the provisioning pre
* conditions.
*
@@ -2876,7 +2890,7 @@
STATUS_CANNOT_ADD_MANAGED_PROFILE, STATUS_DEVICE_ADMIN_NOT_SUPPORTED,
STATUS_SPLIT_SYSTEM_USER_DEVICE_SYSTEM_USER,
STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS,
- STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED
+ STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED, STATUS_HEADLESS_ONLY_SYSTEM_USER
})
public @interface ProvisioningPrecondition {}
@@ -9178,9 +9192,11 @@
* <p>Calling this after the setup phase of the device owner user has completed is allowed only
* if the caller is the {@link Process#SHELL_UID Shell UID}, and there are no additional users
* (except when the device runs on headless system user mode, in which case it could have exact
- * one extra user, which is the current user - the device owner will be set in the
- * {@link UserHandle#SYSTEM system} user and a profile owner will be set in the current user)
- * and no accounts.
+ * one extra user, which is the current user.
+ *
+ * <p>On a headless devices, if it is in affiliated mode the device owner will be set in the
+ * {@link UserHandle#SYSTEM system} user. If the device is in single user mode, the device owner
+ * will be set in the first secondary user.
*
* @param who the component name to be registered as device owner.
* @param userId ID of the user on which the device owner runs.
@@ -11371,7 +11387,9 @@
* @see UserHandle
* @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the
* user could not be created.
- * @throws SecurityException if {@code admin} is not a device owner.
+ * @throws SecurityException if headless device is in
+ * {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER} mode.
+ * @throws SecurityException if {@code admin} is not a device owner
* @throws UserOperationException if the user could not be created and the calling app is
* targeting {@link android.os.Build.VERSION_CODES#P} and running on
* {@link android.os.Build.VERSION_CODES#P}.
@@ -16612,7 +16630,10 @@
* before calling this method.
*
* <p>Holders of {@link android.Manifest.permission#PROVISION_DEMO_DEVICE} can call this API
- * only if {@link FullyManagedDeviceProvisioningParams#isDemoDevice()} is {@code true}.</p>
+ * only if {@link FullyManagedDeviceProvisioningParams#isDemoDevice()} is {@code true}.
+ *
+ * <p>If headless device is in {@link DeviceAdminInfo#HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER}
+ * mode then it sets the device owner on the first secondary user.</p>
*
* @param provisioningParams Params required to provision a fully managed device,
* see {@link FullyManagedDeviceProvisioningParams}.
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 6cc8af8..3c98ef9 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -76,3 +76,10 @@
description: "Enable APIs to provision and manage eSIMs"
bug: "295301164"
}
+
+flag {
+ name: "headless_device_owner_single_user_enabled"
+ namespace: "enterprise"
+ description: "Add Headless DO support."
+ bug: "289515470"
+}
diff --git a/core/java/android/app/prediction/AppPredictor.java b/core/java/android/app/prediction/AppPredictor.java
index d628b7f..0c1a28a 100644
--- a/core/java/android/app/prediction/AppPredictor.java
+++ b/core/java/android/app/prediction/AppPredictor.java
@@ -16,6 +16,7 @@
package android.app.prediction;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -24,9 +25,12 @@
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.os.Binder;
+import android.os.Bundle;
import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.service.appprediction.flags.Flags;
import android.util.ArrayMap;
import android.util.Log;
@@ -263,6 +267,34 @@
}
/**
+ * Requests a Bundle which includes service features info or {@code null} if the service is not
+ * available.
+ *
+ * @param callbackExecutor The callback executor to use when calling the callback. It cannot be
+ * null.
+ * @param callback The callback to return the Bundle which includes service features info. It
+ * cannot be null.
+ *
+ * @throws IllegalStateException If this AppPredictor has already been destroyed.
+ * @throws RuntimeException If there is a failure communicating with the remote service.
+ */
+ @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API)
+ public void requestServiceFeatures(@NonNull Executor callbackExecutor,
+ @NonNull Consumer<Bundle> callback) {
+ if (mIsClosed.get()) {
+ throw new IllegalStateException("This client has already been destroyed.");
+ }
+
+ try {
+ mPredictionManager.requestServiceFeatures(mSessionId,
+ new RemoteCallbackWrapper(callbackExecutor, callback));
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to request service feature info", e);
+ e.rethrowAsRuntimeException();
+ }
+ }
+
+ /**
* Destroys the client and unregisters the callback. Any method on this class after this call
* with throw {@link IllegalStateException}.
*/
@@ -347,6 +379,28 @@
}
}
+ static class RemoteCallbackWrapper extends IRemoteCallback.Stub {
+
+ private final Consumer<Bundle> mCallback;
+ private final Executor mExecutor;
+
+ RemoteCallbackWrapper(@NonNull Executor callbackExecutor,
+ @NonNull Consumer<Bundle> callback) {
+ mExecutor = callbackExecutor;
+ mCallback = callback;
+ }
+
+ @Override
+ public void sendResult(Bundle result) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mExecutor.execute(() -> mCallback.accept(result));
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+ }
+
private static class Token {
static final IBinder sBinder = new Binder(TAG);
}
diff --git a/core/java/android/app/prediction/IPredictionManager.aidl b/core/java/android/app/prediction/IPredictionManager.aidl
index 863fc6f9..94b4f5b 100644
--- a/core/java/android/app/prediction/IPredictionManager.aidl
+++ b/core/java/android/app/prediction/IPredictionManager.aidl
@@ -22,6 +22,7 @@
import android.app.prediction.AppPredictionSessionId;
import android.app.prediction.IPredictionCallback;
import android.content.pm.ParceledListSlice;
+import android.os.IRemoteCallback;
/**
* @hide
@@ -48,4 +49,6 @@
void requestPredictionUpdate(in AppPredictionSessionId sessionId);
void onDestroyPredictionSession(in AppPredictionSessionId sessionId);
+
+ void requestServiceFeatures(in AppPredictionSessionId sessionId, in IRemoteCallback callback);
}
diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java
index 67759f4..eb357fe 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -21,7 +21,13 @@
import static android.content.ContentResolver.SCHEME_CONTENT;
import static android.content.ContentResolver.SCHEME_FILE;
+import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo;
import android.content.res.AssetFileDescriptor;
@@ -207,6 +213,7 @@
final CharSequence mText;
final String mHtmlText;
final Intent mIntent;
+ final PendingIntent mPendingIntent;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
Uri mUri;
private TextLinks mTextLinks;
@@ -214,12 +221,91 @@
// if the data is obtained from {@link #copyForTransferWithActivityInfo}
private ActivityInfo mActivityInfo;
+ /**
+ * A builder for a ClipData Item.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @SuppressLint("PackageLayering")
+ public static final class Builder {
+ private CharSequence mText;
+ private String mHtmlText;
+ private Intent mIntent;
+ private PendingIntent mPendingIntent;
+ private Uri mUri;
+
+ /**
+ * Sets the text for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setText(@Nullable CharSequence text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * Sets the HTML text for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setHtmlText(@Nullable String htmlText) {
+ mHtmlText = htmlText;
+ return this;
+ }
+
+ /**
+ * Sets the Intent for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setIntent(@Nullable Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ /**
+ * Sets the PendingIntent for the item to be constructed. To prevent receiving apps from
+ * improperly manipulating the intent to launch another activity as this caller, the
+ * provided PendingIntent must be immutable (see {@link PendingIntent#FLAG_IMMUTABLE}).
+ * The system will clean up the PendingIntent when it is no longer used.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setPendingIntent(@Nullable PendingIntent pendingIntent) {
+ if (pendingIntent != null && !pendingIntent.isImmutable()) {
+ throw new IllegalArgumentException("Expected pending intent to be immutable");
+ }
+ mPendingIntent = pendingIntent;
+ return this;
+ }
+
+ /**
+ * Sets the URI for the item to be constructed.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Builder setUri(@Nullable Uri uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Constructs a new Item with the properties set on this builder.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @NonNull
+ public Item build() {
+ return new Item(mText, mHtmlText, mIntent, mPendingIntent, mUri);
+ }
+ }
+
/** @hide */
public Item(Item other) {
mText = other.mText;
mHtmlText = other.mHtmlText;
mIntent = other.mIntent;
+ mPendingIntent = other.mPendingIntent;
mUri = other.mUri;
mActivityInfo = other.mActivityInfo;
mTextLinks = other.mTextLinks;
@@ -229,10 +315,7 @@
* Create an Item consisting of a single block of (possibly styled) text.
*/
public Item(CharSequence text) {
- mText = text;
- mHtmlText = null;
- mIntent = null;
- mUri = null;
+ this(text, null, null, null, null);
}
/**
@@ -245,30 +328,21 @@
* </p>
*/
public Item(CharSequence text, String htmlText) {
- mText = text;
- mHtmlText = htmlText;
- mIntent = null;
- mUri = null;
+ this(text, htmlText, null, null, null);
}
/**
* Create an Item consisting of an arbitrary Intent.
*/
public Item(Intent intent) {
- mText = null;
- mHtmlText = null;
- mIntent = intent;
- mUri = null;
+ this(null, null, intent, null, null);
}
/**
* Create an Item consisting of an arbitrary URI.
*/
public Item(Uri uri) {
- mText = null;
- mHtmlText = null;
- mIntent = null;
- mUri = uri;
+ this(null, null, null, null, uri);
}
/**
@@ -276,10 +350,7 @@
* text, Intent, and/or URI.
*/
public Item(CharSequence text, Intent intent, Uri uri) {
- mText = text;
- mHtmlText = null;
- mIntent = intent;
- mUri = uri;
+ this(text, null, intent, null, uri);
}
/**
@@ -289,6 +360,14 @@
* will not be done from HTML formatted text into plain text.
*/
public Item(CharSequence text, String htmlText, Intent intent, Uri uri) {
+ this(text, htmlText, intent, null, uri);
+ }
+
+ /**
+ * Builder ctor.
+ */
+ private Item(CharSequence text, String htmlText, Intent intent, PendingIntent pendingIntent,
+ Uri uri) {
if (htmlText != null && text == null) {
throw new IllegalArgumentException(
"Plain text must be supplied if HTML text is supplied");
@@ -296,6 +375,7 @@
mText = text;
mHtmlText = htmlText;
mIntent = intent;
+ mPendingIntent = pendingIntent;
mUri = uri;
}
@@ -321,6 +401,15 @@
}
/**
+ * Returns the pending intent in this Item.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ @Nullable
+ public PendingIntent getPendingIntent() {
+ return mPendingIntent;
+ }
+
+ /**
* Retrieve the raw URI contained in this Item.
*/
public Uri getUri() {
@@ -777,7 +866,7 @@
throw new NullPointerException("item is null");
}
mIcon = null;
- mItems = new ArrayList<Item>();
+ mItems = new ArrayList<>();
mItems.add(item);
mClipDescription.setIsStyledText(isStyledText());
}
@@ -794,7 +883,7 @@
throw new NullPointerException("item is null");
}
mIcon = null;
- mItems = new ArrayList<Item>();
+ mItems = new ArrayList<>();
mItems.add(item);
mClipDescription.setIsStyledText(isStyledText());
}
@@ -826,7 +915,7 @@
public ClipData(ClipData other) {
mClipDescription = other.mClipDescription;
mIcon = other.mIcon;
- mItems = new ArrayList<Item>(other.mItems);
+ mItems = new ArrayList<>(other.mItems);
}
/**
@@ -1042,6 +1131,35 @@
}
/**
+ * Checks if this clip data has a pending intent that is an activity type.
+ * @hide
+ */
+ public boolean hasActivityPendingIntents() {
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Item item = mItems.get(i);
+ if (item.mPendingIntent != null && item.mPendingIntent.isActivity()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Cleans up all pending intents in the ClipData.
+ * @hide
+ */
+ public void cleanUpPendingIntents() {
+ final int size = mItems.size();
+ for (int i = 0; i < size; i++) {
+ final Item item = mItems.get(i);
+ if (item.mPendingIntent != null) {
+ item.mPendingIntent.cancel();
+ }
+ }
+ }
+
+ /**
* Prepare this {@link ClipData} to leave an app process.
*
* @hide
@@ -1243,6 +1361,7 @@
TextUtils.writeToParcel(item.mText, dest, flags);
dest.writeString8(item.mHtmlText);
dest.writeTypedObject(item.mIntent, flags);
+ dest.writeTypedObject(item.mPendingIntent, flags);
dest.writeTypedObject(item.mUri, flags);
dest.writeTypedObject(mParcelItemActivityInfos ? item.mActivityInfo : null, flags);
dest.writeTypedObject(item.mTextLinks, flags);
@@ -1262,10 +1381,11 @@
CharSequence text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
String htmlText = in.readString8();
Intent intent = in.readTypedObject(Intent.CREATOR);
+ PendingIntent pendingIntent = in.readTypedObject(PendingIntent.CREATOR);
Uri uri = in.readTypedObject(Uri.CREATOR);
ActivityInfo info = in.readTypedObject(ActivityInfo.CREATOR);
TextLinks textLinks = in.readTypedObject(TextLinks.CREATOR);
- Item item = new Item(text, htmlText, intent, uri);
+ Item item = new Item(text, htmlText, intent, pendingIntent, uri);
item.setActivityInfo(info);
item.setTextLinks(textLinks);
mItems.add(item);
@@ -1273,7 +1393,7 @@
}
public static final @android.annotation.NonNull Parcelable.Creator<ClipData> CREATOR =
- new Parcelable.Creator<ClipData>() {
+ new Parcelable.Creator<>() {
@Override
public ClipData createFromParcel(Parcel source) {
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 08871d4..46232fb 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -19,6 +19,7 @@
import static android.app.sdksandbox.SdkSandboxManager.ACTION_START_SANDBOXED_ACTIVITY;
import static android.content.ContentProvider.maybeAddUserId;
import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE;
+import static android.service.chooser.Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA;
import android.Manifest;
import android.accessibilityservice.AccessibilityService;
@@ -73,6 +74,7 @@
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.service.chooser.ChooserAction;
+import android.service.chooser.ChooserResult;
import android.telecom.PhoneAccount;
import android.telecom.TelecomManager;
import android.text.TextUtils;
@@ -1059,7 +1061,7 @@
}
if (sender != null) {
- intent.putExtra(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER, sender);
+ intent.putExtra(EXTRA_CHOOSER_RESULT_INTENT_SENDER, sender);
}
// Migrate any clip data and flags from target.
@@ -6156,6 +6158,17 @@
public static final String EXTRA_INITIAL_INTENTS = "android.intent.extra.INITIAL_INTENTS";
/**
+ * A CharSequence of additional text describing the content being shared. This text will be
+ * displayed to the user as a part of the sharesheet when included in an
+ * {@link #ACTION_CHOOSER} {@link Intent}.
+ *
+ * <p>e.g. When sharing a photo, metadata could inform the user that location data is included
+ * in the photo they are sharing.</p>
+ */
+ @FlaggedApi(FLAG_ENABLE_SHARESHEET_METADATA_EXTRA)
+ public static final String EXTRA_METADATA_TEXT = "android.intent.extra.METADATA_TEXT";
+
+ /**
* A {@link IntentSender} to start after instant app installation success.
* @hide
*/
@@ -6296,6 +6309,25 @@
"android.intent.extra.CHOSEN_COMPONENT_INTENT_SENDER";
/**
+ * An {@link IntentSender} that will be notified when a user successfully chooses a target
+ * component or initiates an action such as copy or edit within an {@link #ACTION_CHOOSER}
+ * activity. The IntentSender will have the extra {@link #EXTRA_CHOOSER_RESULT} describing
+ * the result.
+ */
+ @FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT)
+ public static final String EXTRA_CHOOSER_RESULT_INTENT_SENDER =
+ "android.intent.extra.CHOOSER_RESULT_INTENT_SENDER";
+
+ /**
+ * A {@link ChooserResult} which describes how the sharing session completed.
+ * <p>
+ * An instance is supplied to the optional IntentSender provided to
+ * {@link #createChooser(Intent, CharSequence, IntentSender)} when the session completes.
+ */
+ @FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT)
+ public static final String EXTRA_CHOOSER_RESULT = "android.intent.extra.CHOOSER_RESULT";
+
+ /**
* The {@link ComponentName} chosen by the user to complete an action.
*
* @see #EXTRA_CHOSEN_COMPONENT_INTENT_SENDER
diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java
index 79af65a..d913581 100644
--- a/core/java/android/content/IntentFilter.java
+++ b/core/java/android/content/IntentFilter.java
@@ -559,6 +559,10 @@
sb.append(" sch=");
sb.append(mDataSchemes.toString());
}
+ if (Flags.relativeReferenceIntentFilters() && countUriRelativeFilterGroups() > 0) {
+ sb.append(" grp=");
+ sb.append(mUriRelativeFilterGroups.toString());
+ }
sb.append(" }");
return sb.toString();
}
@@ -1807,13 +1811,7 @@
if (mUriRelativeFilterGroups == null) {
return false;
}
- for (int i = 0; i < mUriRelativeFilterGroups.size(); i++) {
- UriRelativeFilterGroup group = mUriRelativeFilterGroups.get(i);
- if (group.matchData(data)) {
- return group.getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
- }
- }
- return false;
+ return UriRelativeFilterGroup.matchGroupsToUri(mUriRelativeFilterGroups, data);
}
/**
diff --git a/core/java/android/content/UriRelativeFilter.java b/core/java/android/content/UriRelativeFilter.java
index 9866cd0..6d33246 100644
--- a/core/java/android/content/UriRelativeFilter.java
+++ b/core/java/android/content/UriRelativeFilter.java
@@ -217,6 +217,15 @@
+ " }";
}
+ /** @hide */
+ public UriRelativeFilterParcel toParcel() {
+ UriRelativeFilterParcel parcel = new UriRelativeFilterParcel();
+ parcel.uriPart = mUriPart;
+ parcel.patternType = mPatternType;
+ parcel.filter = mFilter;
+ return parcel;
+ }
+
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
@@ -257,4 +266,11 @@
mPatternType = Integer.parseInt(parser.getAttributeValue(null, PATTERN_STR));
mFilter = parser.getAttributeValue(null, FILTER_STR);
}
+
+ /** @hide */
+ public UriRelativeFilter(UriRelativeFilterParcel parcel) {
+ mUriPart = parcel.uriPart;
+ mPatternType = parcel.patternType;
+ mFilter = parcel.filter;
+ }
}
diff --git a/core/java/android/content/UriRelativeFilterGroup.aidl b/core/java/android/content/UriRelativeFilterGroup.aidl
new file mode 100644
index 0000000..b251054
--- /dev/null
+++ b/core/java/android/content/UriRelativeFilterGroup.aidl
@@ -0,0 +1,19 @@
+/**
+ * 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 android.content;
+
+parcelable UriRelativeFilterGroup;
diff --git a/core/java/android/content/UriRelativeFilterGroup.java b/core/java/android/content/UriRelativeFilterGroup.java
index 72c396a..0e49b4f 100644
--- a/core/java/android/content/UriRelativeFilterGroup.java
+++ b/core/java/android/content/UriRelativeFilterGroup.java
@@ -19,6 +19,7 @@
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.pm.Flags;
import android.net.Uri;
import android.os.Parcel;
@@ -36,9 +37,11 @@
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
+import java.util.List;
import java.util.Objects;
/**
@@ -83,6 +86,40 @@
private final @Action int mAction;
private final ArraySet<UriRelativeFilter> mUriRelativeFilters = new ArraySet<>();
+ /** @hide */
+ public static boolean matchGroupsToUri(List<UriRelativeFilterGroup> groups, Uri uri) {
+ for (int i = 0; i < groups.size(); i++) {
+ if (groups.get(i).matchData(uri)) {
+ return groups.get(i).getAction() == UriRelativeFilterGroup.ACTION_ALLOW;
+ }
+ }
+ return false;
+ }
+
+ /** @hide */
+ public static List<UriRelativeFilterGroup> parcelsToGroups(
+ @Nullable List<UriRelativeFilterGroupParcel> parcels) {
+ List<UriRelativeFilterGroup> groups = new ArrayList<>();
+ if (parcels != null) {
+ for (int i = 0; i < parcels.size(); i++) {
+ groups.add(new UriRelativeFilterGroup(parcels.get(i)));
+ }
+ }
+ return groups;
+ }
+
+ /** @hide */
+ public static List<UriRelativeFilterGroupParcel> groupsToParcels(
+ @Nullable List<UriRelativeFilterGroup> groups) {
+ List<UriRelativeFilterGroupParcel> parcels = new ArrayList<>();
+ if (groups != null) {
+ for (int i = 0; i < groups.size(); i++) {
+ parcels.add(groups.get(i).toParcel());
+ }
+ }
+ return parcels;
+ }
+
/**
* New UriRelativeFilterGroup that matches a Intent data.
*
@@ -205,6 +242,35 @@
}
}
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ UriRelativeFilterGroup that = (UriRelativeFilterGroup) o;
+ if (mAction != that.mAction) return false;
+ return mUriRelativeFilters.equals(that.mUriRelativeFilters);
+ }
+
+ @Override
+ public int hashCode() {
+ int _hash = 0;
+ _hash = 31 * _hash + mAction;
+ _hash = 31 * _hash + java.util.Objects.hashCode(mUriRelativeFilters);
+ return _hash;
+ }
+
+ /** @hide */
+ public UriRelativeFilterGroupParcel toParcel() {
+ UriRelativeFilterGroupParcel parcel = new UriRelativeFilterGroupParcel();
+ parcel.action = mAction;
+ parcel.filters = new ArrayList<>();
+ for (UriRelativeFilter filter : mUriRelativeFilters) {
+ parcel.filters.add(filter.toParcel());
+ }
+ return parcel;
+ }
+
/** @hide */
UriRelativeFilterGroup(@NonNull Parcel src) {
mAction = src.readInt();
@@ -213,4 +279,12 @@
mUriRelativeFilters.add(new UriRelativeFilter(src));
}
}
+
+ /** @hide */
+ public UriRelativeFilterGroup(UriRelativeFilterGroupParcel parcel) {
+ mAction = parcel.action;
+ for (int i = 0; i < parcel.filters.size(); i++) {
+ mUriRelativeFilters.add(new UriRelativeFilter(parcel.filters.get(i)));
+ }
+ }
}
diff --git a/core/java/android/content/UriRelativeFilterGroupParcel.aidl b/core/java/android/content/UriRelativeFilterGroupParcel.aidl
new file mode 100644
index 0000000..3679e7f
--- /dev/null
+++ b/core/java/android/content/UriRelativeFilterGroupParcel.aidl
@@ -0,0 +1,28 @@
+/**
+ * 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 android.content;
+
+import android.content.UriRelativeFilterParcel;
+
+/**
+ * Class for holding UriRelativeFilterGroup data.
+ * @hide
+ */
+parcelable UriRelativeFilterGroupParcel {
+ int action;
+ List<UriRelativeFilterParcel> filters;
+}
diff --git a/core/java/android/content/UriRelativeFilterParcel.aidl b/core/java/android/content/UriRelativeFilterParcel.aidl
new file mode 100644
index 0000000..4fb196d
--- /dev/null
+++ b/core/java/android/content/UriRelativeFilterParcel.aidl
@@ -0,0 +1,27 @@
+/*
+ * 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 android.content;
+
+/**
+ * Class for holding UriRelativeFilter data.
+ * @hide
+ */
+parcelable UriRelativeFilterParcel {
+ int uriPart;
+ int patternType;
+ String filter;
+}
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index 2b378b1..5b0cee7 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -142,6 +142,28 @@
* the {@link android.R.attr#foregroundServiceType} attribute.
* Data(photo, file, account) upload/download, backup/restore, import/export, fetch,
* transfer over network between device and cloud.
+ *
+ * <p>This type has time limit of 6 hours starting from Android version
+ * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}.
+ * A foreground service of this type must be stopped within the timeout by
+ * {@link android.app.Service#stopSelf()},
+ * {@link android.content.Context#stopService(android.content.Intent)} or their overloads).
+ * {@link android.app.Service#stopForeground(int)} will also work, which will demote the
+ * service to a "background" service, which will soon be stopped by the system.
+ *
+ * <p>If the service isn't stopped within the timeout,
+ * {@link android.app.Service#onTimeout(int, int)} will be called.
+ *
+ * <p>Also note, even though
+ * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC} can be used on
+ * Android versions prior to {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, since
+ * {@link android.app.Service#onTimeout(int, int)} did not exist on such versions, it will
+ * never be called.
+ *
+ * Because of this, developers must make sure to stop the foreground service even if
+ * {@link android.app.Service#onTimeout(int, int)} is not called on such versions.
+ *
+ * @see android.app.Service#onTimeout(int, int)
*/
@RequiresPermission(
value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC,
@@ -483,6 +505,27 @@
* Constant corresponding to {@code mediaProcessing} in
* the {@link android.R.attr#foregroundServiceType} attribute.
* Media processing use cases such as video or photo editing and processing.
+ *
+ * This type has time limit of 6 hours.
+ * A foreground service of this type must be stopped within the timeout by
+ * {@link android.app.Service#stopSelf()},
+ * {@link android.content.Context#stopService(android.content.Intent)} or their overloads).
+ * {@link android.app.Service#stopForeground(int)} will also work, which will demote the
+ * service to a "background" service, which will soon be stopped by the system.
+ *
+ * <p>If the service isn't stopped within the timeout,
+ * {@link android.app.Service#onTimeout(int, int)} will be called.
+ *
+ * <p>Also note, even though
+ * {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING} was added in
+ * Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, it can be also used
+ * on prior android versions (just like other new foreground service types can be used).
+ * However, because {@link android.app.Service#onTimeout(int, int)} did not exist on prior
+ * versions, it will never be called on such versions.
+ * Because of this, developers must make sure to stop the foreground service even if
+ * {@link android.app.Service#onTimeout(int, int)} is not called on such versions.
+ *
+ * @see android.app.Service#onTimeout(int, int)
*/
@RequiresPermission(
value = Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROCESSING
diff --git a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
index 77bd147..4dcc517 100644
--- a/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
+++ b/core/java/android/content/pm/verify/domain/DomainVerificationManager.java
@@ -17,6 +17,7 @@
package android.content.pm.verify.domain;
import android.annotation.CheckResult;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -25,15 +26,21 @@
import android.annotation.SystemService;
import android.content.Context;
import android.content.Intent;
+import android.content.UriRelativeFilterGroup;
+import android.content.UriRelativeFilterGroupParcel;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
+import android.util.ArrayMap;
import com.android.internal.util.CollectionUtils;
+import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
@@ -156,6 +163,74 @@
}
/**
+ * Update the URI relative filter groups for a package. All previously existing groups
+ * will be cleared before the new groups will be applied.
+ *
+ * @param packageName The name of the package.
+ * @param domainToGroupsMap A map of domains to a list of {@link UriRelativeFilterGroup}s that
+ * should apply to them. Groups for each domain will replace any groups
+ * provided for that domain in a prior call to this method. Groups will
+ * be evaluated in the order they are provided.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.DOMAIN_VERIFICATION_AGENT)
+ @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ public void setUriRelativeFilterGroups(@NonNull String packageName,
+ @NonNull Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(domainToGroupsMap);
+ Bundle bundle = new Bundle();
+ for (String domain : domainToGroupsMap.keySet()) {
+ List<UriRelativeFilterGroup> groups = domainToGroupsMap.get(domain);
+ bundle.putParcelableList(domain, UriRelativeFilterGroup.groupsToParcels(groups));
+ }
+ try {
+ mDomainVerificationManager.setUriRelativeFilterGroups(packageName, bundle);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves a map of a package's verified domains to a list of {@link UriRelativeFilterGroup}s
+ * that applies to them.
+ *
+ * @param packageName The name of the package.
+ * @param domains List of domains for which to retrieve group matches.
+ * @return A map of domains to the lists of {@link UriRelativeFilterGroup}s that apply to them.
+ * @hide
+ */
+ @NonNull
+ @SystemApi
+ @FlaggedApi(android.content.pm.Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ public Map<String, List<UriRelativeFilterGroup>> getUriRelativeFilterGroups(
+ @NonNull String packageName,
+ @NonNull List<String> domains) {
+ Objects.requireNonNull(packageName);
+ Objects.requireNonNull(domains);
+ if (domains.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ try {
+ Bundle bundle = mDomainVerificationManager.getUriRelativeFilterGroups(packageName,
+ domains);
+ ArrayMap<String, List<UriRelativeFilterGroup>> map = new ArrayMap<>();
+ if (!bundle.isEmpty()) {
+ for (String domain : bundle.keySet()) {
+ List<UriRelativeFilterGroupParcel> parcels =
+ bundle.getParcelableArrayList(domain,
+ UriRelativeFilterGroupParcel.class);
+ map.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+ }
+ }
+ return map;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Used to iterate all {@link DomainVerificationInfo} values to do cleanup or retries. This is
* usually a heavy workload and should be done infrequently.
*
diff --git a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
index 53205f3..f5af82d 100644
--- a/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
+++ b/core/java/android/content/pm/verify/domain/IDomainVerificationManager.aidl
@@ -20,6 +20,8 @@
import android.content.pm.verify.domain.DomainSet;
import android.content.pm.verify.domain.DomainVerificationInfo;
import android.content.pm.verify.domain.DomainVerificationUserState;
+import android.content.UriRelativeFilterGroup;
+import android.os.Bundle;
import java.util.List;
/**
@@ -46,4 +48,8 @@
int setDomainVerificationUserSelection(String domainSetId, in DomainSet domains,
boolean enabled, int userId);
+
+ void setUriRelativeFilterGroups(String packageName, in Bundle domainToGroupsBundle);
+
+ Bundle getUriRelativeFilterGroups(String packageName, in List<String> domains);
}
diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java
index 23b9d0b..d259e97 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -137,6 +137,8 @@
private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>();
private ArrayList<ResourcesLoader> mLoaders = new ArrayList<>();
+ private boolean mNoInit = false;
+
public Builder addApkAssets(ApkAssets apkAssets) {
mUserApkAssets.add(apkAssets);
return this;
@@ -147,6 +149,11 @@
return this;
}
+ public Builder setNoInit() {
+ mNoInit = true;
+ return this;
+ }
+
public AssetManager build() {
// Retrieving the system ApkAssets forces their creation as well.
final ApkAssets[] systemApkAssets = getSystem().getApkAssets();
@@ -188,7 +195,7 @@
final AssetManager assetManager = new AssetManager(false /*sentinel*/);
assetManager.mApkAssets = apkAssets;
AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
- false /*invalidateCaches*/);
+ false /*invalidateCaches*/, mNoInit /*preset*/);
assetManager.mLoaders = mLoaders.isEmpty() ? null
: mLoaders.toArray(new ResourcesLoader[0]);
@@ -329,7 +336,7 @@
synchronized (this) {
ensureOpenLocked();
mApkAssets = newApkAssets;
- nativeSetApkAssets(mObject, mApkAssets, invalidateCaches);
+ nativeSetApkAssets(mObject, mApkAssets, invalidateCaches, false);
if (invalidateCaches) {
// Invalidate all caches.
invalidateCachesLocked(-1);
@@ -496,7 +503,7 @@
mApkAssets = Arrays.copyOf(mApkAssets, count + 1);
mApkAssets[count] = assets;
- nativeSetApkAssets(mObject, mApkAssets, true);
+ nativeSetApkAssets(mObject, mApkAssets, true, false);
invalidateCachesLocked(-1);
return count + 1;
}
@@ -1503,12 +1510,29 @@
int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp,
int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode,
int grammaticalGender, int majorVersion) {
+ setConfigurationInternal(mcc, mnc, defaultLocale, locales, orientation,
+ touchscreen, density, keyboard, keyboardHidden, navigation, screenWidth,
+ screenHeight, smallestScreenWidthDp, screenWidthDp, screenHeightDp,
+ screenLayout, uiMode, colorMode, grammaticalGender, majorVersion, false);
+ }
+
+ /**
+ * Change the configuration used when retrieving resources, and potentially force a refresh of
+ * the state. Not for use by applications.
+ * @hide
+ */
+ void setConfigurationInternal(int mcc, int mnc, String defaultLocale, String[] locales,
+ int orientation, int touchscreen, int density, int keyboard, int keyboardHidden,
+ int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp,
+ int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode,
+ int grammaticalGender, int majorVersion, boolean forceRefresh) {
synchronized (this) {
ensureValidLocked();
nativeSetConfiguration(mObject, mcc, mnc, defaultLocale, locales, orientation,
touchscreen, density, keyboard, keyboardHidden, navigation, screenWidth,
screenHeight, smallestScreenWidthDp, screenWidthDp, screenHeightDp,
- screenLayout, uiMode, colorMode, grammaticalGender, majorVersion);
+ screenLayout, uiMode, colorMode, grammaticalGender, majorVersion,
+ forceRefresh);
}
}
@@ -1593,13 +1617,13 @@
private static native long nativeCreate();
private static native void nativeDestroy(long ptr);
private static native void nativeSetApkAssets(long ptr, @NonNull ApkAssets[] apkAssets,
- boolean invalidateCaches);
+ boolean invalidateCaches, boolean preset);
private static native void nativeSetConfiguration(long ptr, int mcc, int mnc,
@Nullable String defaultLocale, @NonNull String[] locales, int orientation,
int touchscreen, int density, int keyboard, int keyboardHidden, int navigation,
int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp,
int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender,
- int majorVersion);
+ int majorVersion, boolean forceRefresh);
private static native @NonNull SparseArray<String> nativeGetAssignedPackageIdentifiers(
long ptr, boolean includeOverlays, boolean includeLoaders);
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 3671980..7fba3e8 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -89,7 +89,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
@@ -188,7 +187,7 @@
private int mBaseApkAssetsSize;
/** @hide */
- private static Set<Resources> sResourcesHistory = Collections.synchronizedSet(
+ private static final Set<Resources> sResourcesHistory = Collections.synchronizedSet(
Collections.newSetFromMap(
new WeakHashMap<>()));
@@ -2808,7 +2807,12 @@
public void dump(PrintWriter pw, String prefix) {
pw.println(prefix + "class=" + getClass());
pw.println(prefix + "resourcesImpl");
- mResourcesImpl.dump(pw, prefix + " ");
+ final var impl = mResourcesImpl;
+ if (impl != null) {
+ impl.dump(pw, prefix + " ");
+ } else {
+ pw.println(prefix + " " + "null");
+ }
}
/** @hide */
@@ -2816,15 +2820,22 @@
pw.println(prefix + "history");
// Putting into a map keyed on the apk assets to deduplicate resources that are different
// objects but ultimately represent the same assets
- Map<List<ApkAssets>, Resources> history = new ArrayMap<>();
+ ArrayMap<List<ApkAssets>, Resources> history = new ArrayMap<>();
sResourcesHistory.forEach(
- r -> history.put(Arrays.asList(r.mResourcesImpl.mAssets.getApkAssets()), r));
+ r -> {
+ if (r != null) {
+ final var impl = r.mResourcesImpl;
+ if (impl != null) {
+ history.put(Arrays.asList(impl.mAssets.getApkAssets()), r);
+ } else {
+ history.put(null, r);
+ }
+ }
+ });
int i = 0;
for (Resources r : history.values()) {
- if (r != null) {
- pw.println(prefix + i++);
- r.dump(pw, prefix + " ");
- }
+ pw.println(prefix + i++);
+ r.dump(pw, prefix + " ");
}
}
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index 5e442b8..079c2c1 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -200,7 +200,7 @@
mMetrics.setToDefaults();
mDisplayAdjustments = displayAdjustments;
mConfiguration.setToDefaults();
- updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
+ updateConfigurationImpl(config, metrics, displayAdjustments.getCompatibilityInfo(), true);
}
public DisplayAdjustments getDisplayAdjustments() {
@@ -402,7 +402,12 @@
}
public void updateConfiguration(Configuration config, DisplayMetrics metrics,
- CompatibilityInfo compat) {
+ CompatibilityInfo compat) {
+ updateConfigurationImpl(config, metrics, compat, false);
+ }
+
+ private void updateConfigurationImpl(Configuration config, DisplayMetrics metrics,
+ CompatibilityInfo compat, boolean forceAssetsRefresh) {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesImpl#updateConfiguration");
try {
synchronized (mAccessLock) {
@@ -528,7 +533,7 @@
keyboardHidden = mConfiguration.keyboardHidden;
}
- mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc,
+ mAssets.setConfigurationInternal(mConfiguration.mcc, mConfiguration.mnc,
defaultLocale,
selectedLocales,
mConfiguration.orientation,
@@ -539,7 +544,7 @@
mConfiguration.screenWidthDp, mConfiguration.screenHeightDp,
mConfiguration.screenLayout, mConfiguration.uiMode,
mConfiguration.colorMode, mConfiguration.getGrammaticalGender(),
- Build.VERSION.RESOURCES_SDK_INT);
+ Build.VERSION.RESOURCES_SDK_INT, forceAssetsRefresh);
if (DEBUG_CONFIG) {
Slog.i(TAG, "**** Updating config of " + this + ": final config is "
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index bdaf9d7..d4c58b2 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -200,6 +200,25 @@
return this;
}
+ /**
+ * Optional: Sets logo description text that will be shown on the prompt.
+ *
+ * <p> Note that using this method is not recommended in most scenarios because the calling
+ * application's name will be used by default. Setting the logo description is intended for
+ * large bundled applications that perform a wide range of functions and need to show
+ * distinct description for each function.
+ *
+ * @param logoDescription The logo description text that will be shown on the prompt.
+ * @return This builder.
+ */
+ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO)
+ @NonNull
+ public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) {
+ mPromptInfo.setLogoDescription(logoDescription);
+ return this;
+ }
+
/**
* Required: Sets the title that will be shown on the prompt.
@@ -743,7 +762,20 @@
return mPromptInfo.getLogoBitmap();
}
-
+ /**
+ * Gets the logo description for the prompt, as set by
+ * {@link Builder#setDescription(CharSequence)}.
+ * Currently for system applications use only.
+ *
+ * @return The logo description of the prompt, or null if the prompt has no logo description
+ * set.
+ */
+ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO)
+ @Nullable
+ public String getLogoDescription() {
+ return mPromptInfo.getLogoDescription();
+ }
/**
* Gets the title for the prompt, as set by {@link Builder#setTitle(CharSequence)}.
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index 0f9cadc..2236660 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -34,6 +34,7 @@
@DrawableRes private int mLogoRes = -1;
@Nullable private Bitmap mLogoBitmap;
+ @Nullable private String mLogoDescription;
@NonNull private CharSequence mTitle;
private boolean mUseDefaultTitle;
@Nullable private CharSequence mSubtitle;
@@ -62,6 +63,7 @@
PromptInfo(Parcel in) {
mLogoRes = in.readInt();
mLogoBitmap = in.readTypedObject(Bitmap.CREATOR);
+ mLogoDescription = in.readString();
mTitle = in.readCharSequence();
mUseDefaultTitle = in.readBoolean();
mSubtitle = in.readCharSequence();
@@ -106,6 +108,7 @@
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mLogoRes);
dest.writeTypedObject(mLogoBitmap, 0);
+ dest.writeString(mLogoDescription);
dest.writeCharSequence(mTitle);
dest.writeBoolean(mUseDefaultTitle);
dest.writeCharSequence(mSubtitle);
@@ -173,6 +176,8 @@
return true;
} else if (mLogoBitmap != null) {
return true;
+ } else if (mLogoDescription != null) {
+ return true;
}
return false;
}
@@ -189,6 +194,10 @@
checkOnlyOneLogoSet();
}
+ public void setLogoDescription(@NonNull String logoDescription) {
+ mLogoDescription = logoDescription;
+ }
+
public void setTitle(CharSequence title) {
mTitle = title;
}
@@ -282,6 +291,10 @@
return mLogoBitmap;
}
+ public String getLogoDescription() {
+ return mLogoDescription;
+ }
+
public CharSequence getTitle() {
return mTitle;
}
diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
index 7abe821..3b10e0d 100644
--- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java
@@ -19,7 +19,9 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
@@ -40,6 +42,7 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemProperties;
+import android.provider.Settings;
import android.util.IntArray;
import android.util.Log;
import android.util.Pair;
@@ -53,6 +56,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -243,16 +247,19 @@
private static final String PROXY_SERVICE_NAME =
"com.android.cameraextensions.CameraExtensionsProxyService";
+ @FlaggedApi(Flags.FLAG_CONCERT_MODE)
+ private static final int FALLBACK_PACKAGE_NAME =
+ com.android.internal.R.string.config_extensionFallbackPackageName;
+ @FlaggedApi(Flags.FLAG_CONCERT_MODE)
+ private static final int FALLBACK_SERVICE_NAME =
+ com.android.internal.R.string.config_extensionFallbackServiceName;
+
// Singleton instance
private static final CameraExtensionManagerGlobal GLOBAL_CAMERA_MANAGER =
new CameraExtensionManagerGlobal();
private final Object mLock = new Object();
private final int PROXY_SERVICE_DELAY_MS = 2000;
- private InitializerFuture mInitFuture = null;
- private ServiceConnection mConnection = null;
- private int mConnectionCount = 0;
- private ICameraExtensionsProxyService mProxy = null;
- private boolean mSupportsAdvancedExtensions = false;
+ private ExtensionConnectionManager mConnectionManager = new ExtensionConnectionManager();
// Singleton, don't allow construction
private CameraExtensionManagerGlobal() {}
@@ -261,17 +268,17 @@
return GLOBAL_CAMERA_MANAGER;
}
- private void releaseProxyConnectionLocked(Context ctx) {
- if (mConnection != null ) {
- ctx.unbindService(mConnection);
- mConnection = null;
- mProxy = null;
- mConnectionCount = 0;
+ private void releaseProxyConnectionLocked(Context ctx, int extension) {
+ if (mConnectionManager.getConnection(extension) != null) {
+ ctx.unbindService(mConnectionManager.getConnection(extension));
+ mConnectionManager.setConnection(extension, null);
+ mConnectionManager.setProxy(extension, null);
+ mConnectionManager.resetConnectionCount(extension);
}
}
- private void connectToProxyLocked(Context ctx) {
- if (mConnection == null) {
+ private void connectToProxyLocked(Context ctx, int extension, boolean useFallback) {
+ if (mConnectionManager.getConnection(extension) == null) {
Intent intent = new Intent();
intent.setClassName(PROXY_PACKAGE_NAME, PROXY_SERVICE_NAME);
String vendorProxyPackage = SystemProperties.get(
@@ -287,34 +294,55 @@
+ vendorProxyService);
intent.setClassName(vendorProxyPackage, vendorProxyService);
}
- mInitFuture = new InitializerFuture();
- mConnection = new ServiceConnection() {
+
+ if (Flags.concertMode() && useFallback) {
+ String packageName = ctx.getResources().getString(FALLBACK_PACKAGE_NAME);
+ String serviceName = ctx.getResources().getString(FALLBACK_SERVICE_NAME);
+
+ if (!packageName.isEmpty() && !serviceName.isEmpty()) {
+ Log.v(TAG,
+ "Choosing the fallback software implementation package: "
+ + packageName);
+ Log.v(TAG,
+ "Choosing the fallback software implementation service: "
+ + serviceName);
+ intent.setClassName(packageName, serviceName);
+ }
+ }
+
+ InitializerFuture initFuture = new InitializerFuture();
+ ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName component) {
- mConnection = null;
- mProxy = null;
+ mConnectionManager.setConnection(extension, null);
+ mConnectionManager.setProxy(extension, null);
}
@Override
public void onServiceConnected(ComponentName component, IBinder binder) {
- mProxy = ICameraExtensionsProxyService.Stub.asInterface(binder);
- if (mProxy == null) {
+ ICameraExtensionsProxyService proxy =
+ ICameraExtensionsProxyService.Stub.asInterface(binder);
+ mConnectionManager.setProxy(extension, proxy);
+ if (mConnectionManager.getProxy(extension) == null) {
throw new IllegalStateException("Camera Proxy service is null");
}
try {
- mSupportsAdvancedExtensions = mProxy.advancedExtensionsSupported();
+ mConnectionManager.setAdvancedExtensionsSupported(extension,
+ mConnectionManager.getProxy(extension)
+ .advancedExtensionsSupported());
} catch (RemoteException e) {
Log.e(TAG, "Remote IPC failed!");
}
- mInitFuture.setStatus(true);
+ initFuture.setStatus(true);
}
};
ctx.bindService(intent, Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT |
Context.BIND_ABOVE_CLIENT | Context.BIND_NOT_VISIBLE,
- android.os.AsyncTask.THREAD_POOL_EXECUTOR, mConnection);
+ android.os.AsyncTask.THREAD_POOL_EXECUTOR, connection);
+ mConnectionManager.setConnection(extension, connection);
try {
- mInitFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS);
+ initFuture.get(PROXY_SERVICE_DELAY_MS, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
Log.e(TAG, "Timed out while initializing proxy service!");
}
@@ -366,64 +394,102 @@
}
}
- public boolean registerClient(Context ctx, IBinder token) {
+ public boolean registerClientHelper(Context ctx, IBinder token, int extension,
+ boolean useFallback) {
synchronized (mLock) {
boolean ret = false;
- connectToProxyLocked(ctx);
- if (mProxy == null) {
+ connectToProxyLocked(ctx, extension, useFallback);
+ if (mConnectionManager.getProxy(extension) == null) {
return false;
}
- mConnectionCount++;
+ mConnectionManager.incrementConnectionCount(extension);
try {
- ret = mProxy.registerClient(token);
+ ret = mConnectionManager.getProxy(extension).registerClient(token);
} catch (RemoteException e) {
Log.e(TAG, "Failed to initialize extension! Extension service does "
+ " not respond!");
}
if (!ret) {
- mConnectionCount--;
+ mConnectionManager.decrementConnectionCount(extension);
}
- if (mConnectionCount <= 0) {
- releaseProxyConnectionLocked(ctx);
+ if (mConnectionManager.getConnectionCount(extension) <= 0) {
+ releaseProxyConnectionLocked(ctx, extension);
}
return ret;
}
}
- public void unregisterClient(Context ctx, IBinder token) {
+ @SuppressLint("NonUserGetterCalled")
+ public boolean registerClient(Context ctx, IBinder token, int extension,
+ String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) {
+ boolean ret = registerClientHelper(ctx, token, extension, false /*useFallback*/);
+
+ if (Flags.concertMode()) {
+ // Check if user enabled fallback impl
+ ContentResolver resolver = ctx.getContentResolver();
+ int userEnabled = Settings.Secure.getInt(resolver,
+ Settings.Secure.CAMERA_EXTENSIONS_FALLBACK, 1);
+
+ boolean vendorImpl = true;
+ if (ret && (mConnectionManager.getProxy(extension) != null) && (userEnabled == 1)) {
+ // At this point, we are connected to either CameraExtensionsProxyService or
+ // the vendor extension proxy service. If the vendor does not support the
+ // extension, unregisterClient and re-register client with the proxy service
+ // containing the fallback impl
+ vendorImpl = isExtensionSupported(cameraId, extension,
+ characteristicsMapNative);
+ }
+
+ if (!vendorImpl) {
+ unregisterClient(ctx, token, extension);
+ ret = registerClientHelper(ctx, token, extension, true /*useFallback*/);
+
+ }
+ }
+
+ return ret;
+ }
+
+ public void unregisterClient(Context ctx, IBinder token, int extension) {
synchronized (mLock) {
- if (mProxy != null) {
+ if (mConnectionManager.getProxy(extension) != null) {
try {
- mProxy.unregisterClient(token);
+ mConnectionManager.getProxy(extension).unregisterClient(token);
} catch (RemoteException e) {
Log.e(TAG, "Failed to de-initialize extension! Extension service does"
+ " not respond!");
} finally {
- mConnectionCount--;
- if (mConnectionCount <= 0) {
- releaseProxyConnectionLocked(ctx);
+ mConnectionManager.decrementConnectionCount(extension);
+ if (mConnectionManager.getConnectionCount(extension) <= 0) {
+ releaseProxyConnectionLocked(ctx, extension);
}
}
}
}
}
- public void initializeSession(IInitializeSessionCallback cb) throws RemoteException {
+ public void initializeSession(IInitializeSessionCallback cb, int extension)
+ throws RemoteException {
synchronized (mLock) {
- if (mProxy != null) {
- mProxy.initializeSession(cb);
+ if (mConnectionManager.getProxy(extension) != null
+ && !mConnectionManager.isSessionInitialized()) {
+ mConnectionManager.getProxy(extension).initializeSession(cb);
+ mConnectionManager.setSessionInitialized(true);
+ } else {
+ cb.onFailure();
}
}
}
- public void releaseSession() {
+ public void releaseSession(int extension) {
synchronized (mLock) {
- if (mProxy != null) {
+ if (mConnectionManager.getProxy(extension) != null) {
try {
- mProxy.releaseSession();
+ mConnectionManager.getProxy(extension).releaseSession();
+ mConnectionManager.setSessionInitialized(false);
} catch (RemoteException e) {
Log.e(TAG, "Failed to release session! Extension service does"
+ " not respond!");
@@ -432,77 +498,157 @@
}
}
- public boolean areAdvancedExtensionsSupported() {
- return mSupportsAdvancedExtensions;
+ public boolean areAdvancedExtensionsSupported(int extension) {
+ return mConnectionManager.areAdvancedExtensionsSupported(extension);
}
- public IPreviewExtenderImpl initializePreviewExtension(int extensionType)
+ public IPreviewExtenderImpl initializePreviewExtension(int extension)
throws RemoteException {
synchronized (mLock) {
- if (mProxy != null) {
- return mProxy.initializePreviewExtension(extensionType);
+ if (mConnectionManager.getProxy(extension) != null) {
+ return mConnectionManager.getProxy(extension)
+ .initializePreviewExtension(extension);
} else {
return null;
}
}
}
- public IImageCaptureExtenderImpl initializeImageExtension(int extensionType)
+ public IImageCaptureExtenderImpl initializeImageExtension(int extension)
throws RemoteException {
synchronized (mLock) {
- if (mProxy != null) {
- return mProxy.initializeImageExtension(extensionType);
+ if (mConnectionManager.getProxy(extension) != null) {
+ return mConnectionManager.getProxy(extension)
+ .initializeImageExtension(extension);
} else {
return null;
}
}
}
- public IAdvancedExtenderImpl initializeAdvancedExtension(int extensionType)
+ public IAdvancedExtenderImpl initializeAdvancedExtension(int extension)
throws RemoteException {
synchronized (mLock) {
- if (mProxy != null) {
- return mProxy.initializeAdvancedExtension(extensionType);
+ if (mConnectionManager.getProxy(extension) != null) {
+ return mConnectionManager.getProxy(extension)
+ .initializeAdvancedExtension(extension);
} else {
return null;
}
}
}
+
+ private class ExtensionConnectionManager {
+ // Maps extension to ExtensionConnection
+ private Map<Integer, ExtensionConnection> mConnections = new HashMap<>();
+ private boolean mSessionInitialized = false;
+
+ public ExtensionConnectionManager() {
+ IntArray extensionList = new IntArray(EXTENSION_LIST.length);
+ extensionList.addAll(EXTENSION_LIST);
+ if (Flags.concertMode()) {
+ extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY);
+ }
+
+ for (int extensionType : extensionList.toArray()) {
+ mConnections.put(extensionType, new ExtensionConnection());
+ }
+ }
+
+ public ICameraExtensionsProxyService getProxy(@Extension int extension) {
+ return mConnections.get(extension).mProxy;
+ }
+
+ public ServiceConnection getConnection(@Extension int extension) {
+ return mConnections.get(extension).mConnection;
+ }
+
+ public int getConnectionCount(@Extension int extension) {
+ return mConnections.get(extension).mConnectionCount;
+ }
+
+ public boolean areAdvancedExtensionsSupported(@Extension int extension) {
+ return mConnections.get(extension).mSupportsAdvancedExtensions;
+ }
+
+ public boolean isSessionInitialized() {
+ return mSessionInitialized;
+ }
+
+ public void setProxy(@Extension int extension, ICameraExtensionsProxyService proxy) {
+ mConnections.get(extension).mProxy = proxy;
+ }
+
+ public void setConnection(@Extension int extension, ServiceConnection connection) {
+ mConnections.get(extension).mConnection = connection;
+ }
+
+ public void incrementConnectionCount(@Extension int extension) {
+ mConnections.get(extension).mConnectionCount++;
+ }
+
+ public void decrementConnectionCount(@Extension int extension) {
+ mConnections.get(extension).mConnectionCount--;
+ }
+
+ public void resetConnectionCount(@Extension int extension) {
+ mConnections.get(extension).mConnectionCount = 0;
+ }
+
+ public void setAdvancedExtensionsSupported(@Extension int extension,
+ boolean advancedExtSupported) {
+ mConnections.get(extension).mSupportsAdvancedExtensions = advancedExtSupported;
+ }
+
+ public void setSessionInitialized(boolean initialized) {
+ mSessionInitialized = initialized;
+ }
+
+ private class ExtensionConnection {
+ public ICameraExtensionsProxyService mProxy = null;
+ public ServiceConnection mConnection = null;
+ public int mConnectionCount = 0;
+ public boolean mSupportsAdvancedExtensions = false;
+ }
+ }
}
/**
* @hide
*/
- public static boolean registerClient(Context ctx, IBinder token) {
- return CameraExtensionManagerGlobal.get().registerClient(ctx, token);
+ public static boolean registerClient(Context ctx, IBinder token, int extension,
+ String cameraId, Map<String, CameraMetadataNative> characteristicsMapNative) {
+ return CameraExtensionManagerGlobal.get().registerClient(ctx, token, extension, cameraId,
+ characteristicsMapNative);
}
/**
* @hide
*/
- public static void unregisterClient(Context ctx, IBinder token) {
- CameraExtensionManagerGlobal.get().unregisterClient(ctx, token);
+ public static void unregisterClient(Context ctx, IBinder token, int extension) {
+ CameraExtensionManagerGlobal.get().unregisterClient(ctx, token, extension);
}
/**
* @hide
*/
- public static void initializeSession(IInitializeSessionCallback cb) throws RemoteException {
- CameraExtensionManagerGlobal.get().initializeSession(cb);
+ public static void initializeSession(IInitializeSessionCallback cb, int extension)
+ throws RemoteException {
+ CameraExtensionManagerGlobal.get().initializeSession(cb, extension);
}
/**
* @hide
*/
- public static void releaseSession() {
- CameraExtensionManagerGlobal.get().releaseSession();
+ public static void releaseSession(int extension) {
+ CameraExtensionManagerGlobal.get().releaseSession(extension);
}
/**
* @hide
*/
- public static boolean areAdvancedExtensionsSupported() {
- return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported();
+ public static boolean areAdvancedExtensionsSupported(int extension) {
+ return CameraExtensionManagerGlobal.get().areAdvancedExtensionsSupported(extension);
}
/**
@@ -510,7 +656,7 @@
*/
public static boolean isExtensionSupported(String cameraId, int extensionType,
Map<String, CameraMetadataNative> characteristicsMap) {
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extensionType)) {
try {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extensionType);
return extender.isExtensionAvailable(cameraId, characteristicsMap);
@@ -600,24 +746,24 @@
public @NonNull List<Integer> getSupportedExtensions() {
ArrayList<Integer> ret = new ArrayList<>();
final IBinder token = new Binder(TAG + "#getSupportedExtensions:" + mCameraId);
- boolean success = registerClient(mContext, token);
- if (!success) {
- return Collections.unmodifiableList(ret);
- }
IntArray extensionList = new IntArray(EXTENSION_LIST.length);
extensionList.addAll(EXTENSION_LIST);
if (Flags.concertMode()) {
extensionList.add(EXTENSION_EYES_FREE_VIDEOGRAPHY);
}
- try {
- for (int extensionType : extensionList.toArray()) {
- if (isExtensionSupported(mCameraId, extensionType, mCharacteristicsMapNative)) {
+
+ for (int extensionType : extensionList.toArray()) {
+ try {
+ boolean success = registerClient(mContext, token, extensionType, mCameraId,
+ mCharacteristicsMapNative);
+ if (success && isExtensionSupported(mCameraId, extensionType,
+ mCharacteristicsMapNative)) {
ret.add(extensionType);
}
+ } finally {
+ unregisterClient(mContext, token, extensionType);
}
- } finally {
- unregisterClient(mContext, token);
}
return Collections.unmodifiableList(ret);
@@ -643,7 +789,8 @@
public <T> @Nullable T get(@Extension int extension,
@NonNull CameraCharacteristics.Key<T> key) {
final IBinder token = new Binder(TAG + "#get:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -653,7 +800,7 @@
throw new IllegalArgumentException("Unsupported extension");
}
- if (areAdvancedExtensionsSupported() && getKeys(extension).contains(key)) {
+ if (areAdvancedExtensionsSupported(extension) && getKeys(extension).contains(key)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
CameraMetadataNative metadata =
@@ -670,7 +817,7 @@
Log.e(TAG, "Failed to query the extension for the specified key! Extension "
+ "service does not respond!");
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
return null;
}
@@ -691,7 +838,8 @@
public @NonNull Set<CameraCharacteristics.Key> getKeys(@Extension int extension) {
final IBinder token =
new Binder(TAG + "#getKeys:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -703,7 +851,7 @@
throw new IllegalArgumentException("Unsupported extension");
}
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
CameraMetadataNative metadata =
@@ -732,7 +880,7 @@
Log.e(TAG, "Failed to query the extension for all available keys! Extension "
+ "service does not respond!");
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
return Collections.unmodifiableSet(ret);
}
@@ -755,7 +903,8 @@
*/
public boolean isPostviewAvailable(@Extension int extension) {
final IBinder token = new Binder(TAG + "#isPostviewAvailable:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -765,7 +914,7 @@
throw new IllegalArgumentException("Unsupported extension");
}
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
return extender.isPostviewAvailable();
@@ -779,7 +928,7 @@
Log.e(TAG, "Failed to query the extension for postview availability! Extension "
+ "service does not respond!");
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
return false;
@@ -813,7 +962,8 @@
public List<Size> getPostviewSupportedSizes(@Extension int extension,
@NonNull Size captureSize, int format) {
final IBinder token = new Binder(TAG + "#getPostviewSupportedSizes:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -831,7 +981,7 @@
StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
switch(format) {
case ImageFormat.YUV_420_888:
case ImageFormat.JPEG:
@@ -879,7 +1029,7 @@
+ "service does not respond!");
return Collections.emptyList();
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
}
@@ -917,7 +1067,8 @@
// ambiguity is resolved in b/169799538.
final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -929,7 +1080,7 @@
StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
return generateSupportedSizes(
@@ -948,7 +1099,7 @@
+ " not respond!");
return new ArrayList<>();
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
}
@@ -978,7 +1129,8 @@
List<Size> getExtensionSupportedSizes(@Extension int extension, int format) {
try {
final IBinder token = new Binder(TAG + "#getExtensionSupportedSizes:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -990,7 +1142,7 @@
StreamConfigurationMap streamMap = mCharacteristicsMap.get(mCameraId).get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
switch(format) {
case ImageFormat.YUV_420_888:
case ImageFormat.JPEG:
@@ -1035,7 +1187,7 @@
}
}
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
} catch (RemoteException e) {
Log.e(TAG, "Failed to query the extension supported sizes! Extension service does"
@@ -1073,7 +1225,8 @@
}
final IBinder token = new Binder(TAG + "#getEstimatedCaptureLatencyRangeMillis:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -1087,7 +1240,7 @@
new android.hardware.camera2.extension.Size();
sz.width = captureOutputSize.getWidth();
sz.height = captureOutputSize.getHeight();
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
LatencyRange latencyRange = extender.getEstimatedCaptureLatencyRange(mCameraId,
@@ -1126,7 +1279,7 @@
Log.e(TAG, "Failed to query the extension capture latency! Extension service does"
+ " not respond!");
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
return null;
@@ -1143,7 +1296,8 @@
*/
public boolean isCaptureProcessProgressAvailable(@Extension int extension) {
final IBinder token = new Binder(TAG + "#isCaptureProcessProgressAvailable:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -1153,7 +1307,7 @@
throw new IllegalArgumentException("Unsupported extension");
}
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
return extender.isCaptureProcessProgressAvailable();
@@ -1167,7 +1321,7 @@
Log.e(TAG, "Failed to query the extension progress callbacks! Extension service does"
+ " not respond!");
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
return false;
@@ -1195,7 +1349,8 @@
@NonNull
public Set<CaptureRequest.Key> getAvailableCaptureRequestKeys(@Extension int extension) {
final IBinder token = new Binder(TAG + "#getAvailableCaptureRequestKeys:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -1208,7 +1363,7 @@
}
CameraMetadataNative captureRequestMeta = null;
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
captureRequestMeta = extender.getAvailableCaptureRequestKeys(mCameraId);
@@ -1250,7 +1405,7 @@
} catch (RemoteException e) {
throw new IllegalStateException("Failed to query the available capture request keys!");
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
return Collections.unmodifiableSet(ret);
@@ -1282,7 +1437,8 @@
@NonNull
public Set<CaptureResult.Key> getAvailableCaptureResultKeys(@Extension int extension) {
final IBinder token = new Binder(TAG + "#getAvailableCaptureResultKeys:" + mCameraId);
- boolean success = registerClient(mContext, token);
+ boolean success = registerClient(mContext, token, extension, mCameraId,
+ mCharacteristicsMapNative);
if (!success) {
throw new IllegalArgumentException("Unsupported extensions");
}
@@ -1294,7 +1450,7 @@
}
CameraMetadataNative captureResultMeta = null;
- if (areAdvancedExtensionsSupported()) {
+ if (areAdvancedExtensionsSupported(extension)) {
IAdvancedExtenderImpl extender = initializeAdvancedExtension(extension);
extender.init(mCameraId, mCharacteristicsMapNative);
captureResultMeta = extender.getAvailableCaptureResultKeys(mCameraId);
@@ -1336,7 +1492,7 @@
} catch (RemoteException e) {
throw new IllegalStateException("Failed to query the available capture result keys!");
} finally {
- unregisterClient(mContext, token);
+ unregisterClient(mContext, token, extension);
}
return Collections.unmodifiableSet(ret);
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index f6c8f36..b2032fa 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -102,6 +102,8 @@
private boolean mInitialized;
private boolean mSessionClosed;
+ private int mExtensionType;
+
private final Context mContext;
@@ -205,7 +207,7 @@
CameraAdvancedExtensionSessionImpl ret = new CameraAdvancedExtensionSessionImpl(ctx,
extender, cameraDevice, characteristicsMapNative, repeatingRequestSurface,
burstCaptureSurface, postviewSurface, config.getStateCallback(),
- config.getExecutor(), sessionId, token);
+ config.getExecutor(), sessionId, token, config.getExtension());
ret.mStatsAggregator.setClientName(ctx.getOpPackageName());
ret.mStatsAggregator.setExtensionType(config.getExtension());
@@ -223,7 +225,8 @@
@Nullable Surface postviewSurface,
@NonNull StateCallback callback, @NonNull Executor executor,
int sessionId,
- @NonNull IBinder token) {
+ @NonNull IBinder token,
+ int extension) {
mContext = ctx;
mAdvancedExtender = extender;
mCameraDevice = cameraDevice;
@@ -242,6 +245,7 @@
mSessionId = sessionId;
mToken = token;
mInterfaceLock = cameraDevice.mInterfaceLock;
+ mExtensionType = extension;
mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(),
/*isAdvanced=*/true);
@@ -583,9 +587,9 @@
if (mToken != null) {
if (mInitialized || (mCaptureSession != null)) {
notifyClose = true;
- CameraExtensionCharacteristics.releaseSession();
+ CameraExtensionCharacteristics.releaseSession(mExtensionType);
}
- CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
+ CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType);
}
mInitialized = false;
mToken = null;
@@ -654,7 +658,8 @@
}
try {
- CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
+ CameraExtensionCharacteristics.initializeSession(
+ mInitializeHandler, mExtensionType);
} catch (RemoteException e) {
Log.e(TAG, "Failed to initialize session! Extension service does"
+ " not respond!");
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
index ccb24e7..f03876b 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -2559,13 +2559,16 @@
boolean initializationFailed = true;
IBinder token = new Binder(TAG + " : " + mNextSessionId++);
try {
- boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token);
+ boolean ret = CameraExtensionCharacteristics.registerClient(mContext, token,
+ extensionConfiguration.getExtension(), mCameraId,
+ CameraExtensionUtils.getCharacteristicsMapNative(characteristicsMap));
if (!ret) {
token = null;
throw new UnsupportedOperationException("Unsupported extension!");
}
- if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported()) {
+ if (CameraExtensionCharacteristics.areAdvancedExtensionsSupported(
+ extensionConfiguration.getExtension())) {
mCurrentAdvancedExtensionSession =
CameraAdvancedExtensionSessionImpl.createCameraAdvancedExtensionSession(
this, characteristicsMap, mContext, extensionConfiguration,
@@ -2580,7 +2583,8 @@
throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
} finally {
if (initializationFailed && (token != null)) {
- CameraExtensionCharacteristics.unregisterClient(mContext, token);
+ CameraExtensionCharacteristics.unregisterClient(mContext, token,
+ extensionConfiguration.getExtension());
}
}
}
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index db7055b..725b413 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -118,6 +118,7 @@
// In case the client doesn't explicitly enable repeating requests, the framework
// will do so internally.
private boolean mInternalRepeatingRequestEnabled = true;
+ private int mExtensionType;
private final Context mContext;
@@ -244,7 +245,8 @@
sessionId,
token,
extensionChars.getAvailableCaptureRequestKeys(config.getExtension()),
- extensionChars.getAvailableCaptureResultKeys(config.getExtension()));
+ extensionChars.getAvailableCaptureResultKeys(config.getExtension()),
+ config.getExtension());
session.mStatsAggregator.setClientName(ctx.getOpPackageName());
session.mStatsAggregator.setExtensionType(config.getExtension());
@@ -266,7 +268,8 @@
int sessionId,
@NonNull IBinder token,
@NonNull Set<CaptureRequest.Key> requestKeys,
- @Nullable Set<CaptureResult.Key> resultKeys) {
+ @Nullable Set<CaptureResult.Key> resultKeys,
+ int extension) {
mContext = ctx;
mImageExtender = imageExtender;
mPreviewExtender = previewExtender;
@@ -289,6 +292,7 @@
mSupportedResultKeys = resultKeys;
mCaptureResultsSupported = !resultKeys.isEmpty();
mInterfaceLock = cameraDevice.mInterfaceLock;
+ mExtensionType = extension;
mStatsAggregator = new ExtensionSessionStatsAggregator(mCameraDevice.getId(),
/*isAdvanced=*/false);
@@ -881,9 +885,9 @@
if (mToken != null) {
if (mInitialized || (mCaptureSession != null)) {
notifyClose = true;
- CameraExtensionCharacteristics.releaseSession();
+ CameraExtensionCharacteristics.releaseSession(mExtensionType);
}
- CameraExtensionCharacteristics.unregisterClient(mContext, mToken);
+ CameraExtensionCharacteristics.unregisterClient(mContext, mToken, mExtensionType);
}
mInitialized = false;
mToken = null;
@@ -1000,7 +1004,8 @@
mStatsAggregator.commit(/*isFinal*/false);
try {
finishPipelineInitialization();
- CameraExtensionCharacteristics.initializeSession(mInitializeHandler);
+ CameraExtensionCharacteristics.initializeSession(
+ mInitializeHandler, mExtensionType);
} catch (RemoteException e) {
Log.e(TAG, "Failed to initialize session! Extension service does"
+ " not respond!");
diff --git a/services/core/java/com/android/server/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
similarity index 96%
rename from services/core/java/com/android/server/devicestate/DeviceState.java
rename to core/java/android/hardware/devicestate/DeviceState.java
index 2ba59b0..5a34905 100644
--- a/services/core/java/com/android/server/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.devicestate;
+package android.hardware.devicestate;
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
@@ -22,7 +22,6 @@
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
-import android.hardware.devicestate.DeviceStateManager;
import com.android.internal.util.Preconditions;
@@ -38,9 +37,9 @@
* state of the system. This is useful for variable-state devices, like foldable or rollable
* devices, that can be configured by users into differing hardware states, which each may have a
* different expected use case.
+ * @hide
*
- * @see DeviceStateProvider
- * @see DeviceStateManagerService
+ * @see DeviceStateManager
*/
public final class DeviceState {
/**
diff --git a/core/java/android/hardware/location/ContextHubClient.java b/core/java/android/hardware/location/ContextHubClient.java
index 6ed87fff..01dccd1 100644
--- a/core/java/android/hardware/location/ContextHubClient.java
+++ b/core/java/android/hardware/location/ContextHubClient.java
@@ -15,11 +15,14 @@
*/
package android.hardware.location;
+import android.annotation.FlaggedApi;
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.PendingIntent;
+import android.chre.flags.Flags;
import android.os.RemoteException;
import android.util.Log;
@@ -185,23 +188,75 @@
@RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
@ContextHubTransaction.Result
public int sendMessageToNanoApp(@NonNull NanoAppMessage message) {
+ return doSendMessageToNanoApp(message, null);
+ }
+
+ /**
+ * Sends a reliable message to a nanoapp.
+ *
+ * This method is similar to {@link ContextHubClient#sendMessageToNanoApp} with the
+ * difference that it expects the message to be acknowledged by CHRE.
+ *
+ * The transaction succeeds after we received an ACK from CHRE without error.
+ * In all other cases the transaction will fail.
+ */
+ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB)
+ @NonNull
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public ContextHubTransaction<Void> sendReliableMessageToNanoApp(
+ @NonNull NanoAppMessage message) {
+ if (!Flags.reliableMessageImplementation()) {
+ return null;
+ }
+
+ ContextHubTransaction<Void> transaction =
+ new ContextHubTransaction<>(ContextHubTransaction.TYPE_RELIABLE_MESSAGE);
+
+ if (!mAttachedHub.supportsReliableMessages()) {
+ transaction.setResponse(new ContextHubTransaction.Response<Void>(
+ ContextHubTransaction.RESULT_FAILED_NOT_SUPPORTED, null));
+ return transaction;
+ }
+
+ IContextHubTransactionCallback callback =
+ ContextHubTransactionHelper.createTransactionCallback(transaction);
+
+ @ContextHubTransaction.Result int result = doSendMessageToNanoApp(message, callback);
+ if (result != ContextHubTransaction.RESULT_SUCCESS) {
+ transaction.setResponse(new ContextHubTransaction.Response<Void>(result, null));
+ }
+
+ return transaction;
+ }
+
+ /**
+ * Sends a message to a nanoapp.
+ *
+ * @param message The message to send.
+ * @param transactionCallback The callback to use when the message is reliable. null for regular
+ * messages.
+ * @return A {@link ContextHubTransaction.Result} error code.
+ */
+ @ContextHubTransaction.Result
+ private int doSendMessageToNanoApp(@NonNull NanoAppMessage message,
+ @Nullable IContextHubTransactionCallback transactionCallback) {
Objects.requireNonNull(message, "NanoAppMessage cannot be null");
int maxPayloadBytes = mAttachedHub.getMaxPacketLengthBytes();
+
byte[] payload = message.getMessageBody();
if (payload != null && payload.length > maxPayloadBytes) {
- Log.e(
- TAG,
- "Message ("
- + payload.length
- + " bytes) exceeds max payload length ("
- + maxPayloadBytes
- + " bytes)");
+ Log.e(TAG,
+ "Message (%d bytes) exceeds max payload length (%d bytes)".formatted(
+ payload.length, maxPayloadBytes));
return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
}
try {
- return mClientProxy.sendMessageToNanoApp(message);
+ if (transactionCallback == null) {
+ return mClientProxy.sendMessageToNanoApp(message);
+ }
+ return mClientProxy.sendReliableMessageToNanoApp(message, transactionCallback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -224,16 +279,32 @@
/** @hide */
public synchronized void callbackFinished() {
try {
- while (mClientProxy == null) {
- try {
- this.wait();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
+ waitForClientProxy();
mClientProxy.callbackFinished();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
+
+ /** @hide */
+ public synchronized void reliableMessageCallbackFinished(int messageSequenceNumber,
+ byte errorCode) {
+ try {
+ waitForClientProxy();
+ mClientProxy.reliableMessageCallbackFinished(messageSequenceNumber, errorCode);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ private void waitForClientProxy() {
+ while (mClientProxy == null) {
+ try {
+ this.wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
}
diff --git a/core/java/android/hardware/location/ContextHubInfo.java b/core/java/android/hardware/location/ContextHubInfo.java
index 51045a4..5012a79 100644
--- a/core/java/android/hardware/location/ContextHubInfo.java
+++ b/core/java/android/hardware/location/ContextHubInfo.java
@@ -15,9 +15,11 @@
*/
package android.hardware.location;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.chre.flags.Flags;
import android.hardware.contexthub.V1_0.ContextHub;
import android.os.Parcel;
import android.os.Parcelable;
@@ -41,6 +43,7 @@
private float mSleepPowerDrawMw;
private float mPeakPowerDrawMw;
private int mMaxPacketLengthBytes;
+ private boolean mSupportsReliableMessages;
private byte mChreApiMajorVersion;
private byte mChreApiMinorVersion;
private short mChrePatchVersion;
@@ -71,6 +74,7 @@
mSleepPowerDrawMw = contextHub.sleepPowerDrawMw;
mPeakPowerDrawMw = contextHub.peakPowerDrawMw;
mMaxPacketLengthBytes = contextHub.maxSupportedMsgLen;
+ mSupportsReliableMessages = false;
mChrePlatformId = contextHub.chrePlatformId;
mChreApiMajorVersion = contextHub.chreApiMajorVersion;
mChreApiMinorVersion = contextHub.chreApiMinorVersion;
@@ -94,6 +98,8 @@
mSleepPowerDrawMw = 0;
mPeakPowerDrawMw = 0;
mMaxPacketLengthBytes = contextHub.maxSupportedMessageLengthBytes;
+ mSupportsReliableMessages = Flags.reliableMessageImplementation()
+ && contextHub.supportsReliableMessages;
mChrePlatformId = contextHub.chrePlatformId;
mChreApiMajorVersion = contextHub.chreApiMajorVersion;
mChreApiMinorVersion = contextHub.chreApiMinorVersion;
@@ -104,16 +110,25 @@
}
/**
- * returns the maximum number of bytes that can be sent per message to the hub
+ * Returns the maximum number of bytes for a message to the hub.
*
- * @return int - maximum bytes that can be transmitted in a
- * single packet
+ * @return int - maximum bytes that can be transmitted in a single packet.
*/
public int getMaxPacketLengthBytes() {
return mMaxPacketLengthBytes;
}
/**
+ * Returns whether reliable messages are supported
+ *
+ * @return whether reliable messages are supported.
+ */
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public boolean supportsReliableMessages() {
+ return mSupportsReliableMessages;
+ }
+
+ /**
* get the context hub unique identifer
*
* @return int - unique system wide identifier
@@ -164,7 +179,10 @@
* @return int - platform version number
*/
public int getStaticSwVersion() {
- return (mChreApiMajorVersion << 24) | (mChreApiMinorVersion << 16) | (mChrePatchVersion);
+ // Version parts are all unsigned values.
+ return (Byte.toUnsignedInt(mChreApiMajorVersion) << 24)
+ | (Byte.toUnsignedInt(mChreApiMinorVersion) << 16)
+ | (Short.toUnsignedInt(mChrePatchVersion));
}
/**
@@ -284,12 +302,14 @@
retVal += ", Toolchain version: 0x" + Integer.toHexString(mToolchainVersion);
retVal += "\n\tPlatformVersion : 0x" + Integer.toHexString(mPlatformVersion);
retVal += ", SwVersion : "
- + mChreApiMajorVersion + "." + mChreApiMinorVersion + "." + mChrePatchVersion;
+ + Byte.toUnsignedInt(mChreApiMajorVersion) + "." + Byte.toUnsignedInt(
+ mChreApiMinorVersion) + "." + Short.toUnsignedInt(mChrePatchVersion);
retVal += ", CHRE platform ID: 0x" + Long.toHexString(mChrePlatformId);
retVal += "\n\tPeakMips : " + mPeakMips;
retVal += ", StoppedPowerDraw : " + mStoppedPowerDrawMw + " mW";
retVal += ", PeakPowerDraw : " + mPeakPowerDrawMw + " mW";
retVal += ", MaxPacketLength : " + mMaxPacketLengthBytes + " Bytes";
+ retVal += ", SupportsReliableMessage : " + mSupportsReliableMessages;
return retVal;
}
@@ -316,6 +336,8 @@
proto.write(ContextHubInfoProto.SLEEP_POWER_DRAW_MW, mSleepPowerDrawMw);
proto.write(ContextHubInfoProto.PEAK_POWER_DRAW_MW, mPeakPowerDrawMw);
proto.write(ContextHubInfoProto.MAX_PACKET_LENGTH_BYTES, mMaxPacketLengthBytes);
+ proto.write(ContextHubInfoProto.SUPPORTS_RELIABLE_MESSAGES,
+ mSupportsReliableMessages);
}
@Override
@@ -339,6 +361,8 @@
&& (other.getSleepPowerDrawMw() == mSleepPowerDrawMw)
&& (other.getPeakPowerDrawMw() == mPeakPowerDrawMw)
&& (other.getMaxPacketLengthBytes() == mMaxPacketLengthBytes)
+ && (!Flags.reliableMessage()
+ || (other.supportsReliableMessages() == mSupportsReliableMessages))
&& Arrays.equals(other.getSupportedSensors(), mSupportedSensors)
&& Arrays.equals(other.getMemoryRegions(), mMemoryRegions);
}
@@ -367,6 +391,7 @@
mSupportedSensors = new int[numSupportedSensors];
in.readIntArray(mSupportedSensors);
mMemoryRegions = in.createTypedArray(MemoryRegion.CREATOR);
+ mSupportsReliableMessages = in.readBoolean();
}
public int describeContents() {
@@ -393,6 +418,7 @@
out.writeInt(mSupportedSensors.length);
out.writeIntArray(mSupportedSensors);
out.writeTypedArray(mMemoryRegions, flags);
+ out.writeBoolean(mSupportsReliableMessages);
}
public static final @android.annotation.NonNull Parcelable.Creator<ContextHubInfo> CREATOR
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 481ec72..3a58993 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -29,15 +29,15 @@
import android.annotation.TestApi;
import android.app.ActivityThread;
import android.app.PendingIntent;
+import android.chre.flags.Flags;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.hardware.contexthub.ErrorCode;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.Looper;
import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.ServiceManager.ServiceNotFoundException;
import android.util.Log;
import java.lang.annotation.Retention;
@@ -456,32 +456,6 @@
}
}
- /**
- * Helper function to generate a stub for a non-query transaction callback.
- *
- * @param transaction the transaction to unblock when complete
- *
- * @return the callback
- *
- * @hide
- */
- private IContextHubTransactionCallback createTransactionCallback(
- ContextHubTransaction<Void> transaction) {
- return new IContextHubTransactionCallback.Stub() {
- @Override
- public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
- Log.e(TAG, "Received a query callback on a non-query request");
- transaction.setResponse(new ContextHubTransaction.Response<Void>(
- ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE, null));
- }
-
- @Override
- public void onTransactionComplete(int result) {
- transaction.setResponse(new ContextHubTransaction.Response<Void>(result, null));
- }
- };
- }
-
/**
* Helper function to generate a stub for a query transaction callback.
*
@@ -532,7 +506,8 @@
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_LOAD_NANOAPP);
- IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+ IContextHubTransactionCallback callback =
+ ContextHubTransactionHelper.createTransactionCallback(transaction);
try {
mService.loadNanoAppOnHub(hubInfo.getId(), callback, appBinary);
@@ -560,7 +535,8 @@
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_UNLOAD_NANOAPP);
- IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+ IContextHubTransactionCallback callback =
+ ContextHubTransactionHelper.createTransactionCallback(transaction);
try {
mService.unloadNanoAppFromHub(hubInfo.getId(), callback, nanoAppId);
@@ -588,7 +564,8 @@
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_ENABLE_NANOAPP);
- IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+ IContextHubTransactionCallback callback =
+ ContextHubTransactionHelper.createTransactionCallback(transaction);
try {
mService.enableNanoApp(hubInfo.getId(), callback, nanoAppId);
@@ -616,7 +593,8 @@
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_DISABLE_NANOAPP);
- IContextHubTransactionCallback callback = createTransactionCallback(transaction);
+ IContextHubTransactionCallback callback =
+ ContextHubTransactionHelper.createTransactionCallback(transaction);
try {
mService.disableNanoApp(hubInfo.getId(), callback, nanoAppId);
@@ -732,7 +710,14 @@
executor.execute(
() -> {
callback.onMessageFromNanoApp(client, message);
- client.callbackFinished();
+ if (Flags.reliableMessage()
+ && Flags.reliableMessageImplementation()
+ && message.isReliable()) {
+ client.reliableMessageCallbackFinished(
+ message.getMessageSequenceNumber(), ErrorCode.OK);
+ } else {
+ client.callbackFinished();
+ }
});
}
diff --git a/core/java/android/hardware/location/ContextHubTransaction.java b/core/java/android/hardware/location/ContextHubTransaction.java
index d11e0a9..4060f4c 100644
--- a/core/java/android/hardware/location/ContextHubTransaction.java
+++ b/core/java/android/hardware/location/ContextHubTransaction.java
@@ -16,9 +16,11 @@
package android.hardware.location;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import android.chre.flags.Flags;
import android.os.Handler;
import android.os.HandlerExecutor;
@@ -57,7 +59,8 @@
TYPE_UNLOAD_NANOAPP,
TYPE_ENABLE_NANOAPP,
TYPE_DISABLE_NANOAPP,
- TYPE_QUERY_NANOAPPS
+ TYPE_QUERY_NANOAPPS,
+ TYPE_RELIABLE_MESSAGE,
})
public @interface Type { }
@@ -66,6 +69,8 @@
public static final int TYPE_ENABLE_NANOAPP = 2;
public static final int TYPE_DISABLE_NANOAPP = 3;
public static final int TYPE_QUERY_NANOAPPS = 4;
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public static final int TYPE_RELIABLE_MESSAGE = 5;
/**
* Constants describing the result of a transaction or request through the Context Hub Service.
@@ -81,7 +86,8 @@
RESULT_FAILED_AT_HUB,
RESULT_FAILED_TIMEOUT,
RESULT_FAILED_SERVICE_INTERNAL_FAILURE,
- RESULT_FAILED_HAL_UNAVAILABLE
+ RESULT_FAILED_HAL_UNAVAILABLE,
+ RESULT_FAILED_NOT_SUPPORTED,
})
public @interface Result {}
public static final int RESULT_SUCCESS = 0;
@@ -117,6 +123,11 @@
* Failure mode when the Context Hub HAL was not available.
*/
public static final int RESULT_FAILED_HAL_UNAVAILABLE = 8;
+ /**
+ * Failure mode when the operation is not supported.
+ */
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public static final int RESULT_FAILED_NOT_SUPPORTED = 9;
/**
* A class describing the response for a ContextHubTransaction.
@@ -221,6 +232,11 @@
return upperCase ? "Disable" : "disable";
case ContextHubTransaction.TYPE_QUERY_NANOAPPS:
return upperCase ? "Query" : "query";
+ case ContextHubTransaction.TYPE_RELIABLE_MESSAGE: {
+ if (Flags.reliableMessage()) {
+ return upperCase ? "Reliable Message" : "reliable message";
+ }
+ }
default:
return upperCase ? "Unknown" : "unknown";
}
diff --git a/core/java/android/hardware/location/ContextHubTransactionHelper.java b/core/java/android/hardware/location/ContextHubTransactionHelper.java
new file mode 100644
index 0000000..66c03f4
--- /dev/null
+++ b/core/java/android/hardware/location/ContextHubTransactionHelper.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 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 android.hardware.location;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Helper class to generate {@link IContextHubTransactionCallback}.
+ *
+ * @hide
+ */
+public class ContextHubTransactionHelper {
+ private static final String TAG = "ContextHubTransactionHelper";
+
+ /**
+ * Helper to generate a stub for a query nanoapp transaction callback.
+ *
+ * @param transaction the transaction to unblock when complete
+ * @return the callback
+ * @hide
+ */
+ public static IContextHubTransactionCallback createNanoAppQueryCallback(
+ @NonNull() ContextHubTransaction<List<NanoAppState>> transaction) {
+ Objects.requireNonNull(transaction, "transaction cannot be null");
+ return new IContextHubTransactionCallback.Stub() {
+ @Override
+ public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
+ transaction.setResponse(new ContextHubTransaction.Response<List<NanoAppState>>(
+ result, nanoappList));
+ }
+
+ @Override
+ public void onTransactionComplete(int result) {
+ Log.e(TAG, "Received a non-query callback on a query request");
+ transaction.setResponse(new ContextHubTransaction.Response<List<NanoAppState>>(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE, null));
+ }
+ };
+ }
+
+ /**
+ * Helper to generate a stub for a non-query transaction callback.
+ *
+ * @param transaction the transaction to unblock when complete
+ * @return the callback
+ * @hide
+ */
+ public static IContextHubTransactionCallback createTransactionCallback(
+ @NonNull() ContextHubTransaction<Void> transaction) {
+ Objects.requireNonNull(transaction, "transaction cannot be null");
+ return new IContextHubTransactionCallback.Stub() {
+ @Override
+ public void onQueryResponse(int result, List<NanoAppState> nanoappList) {
+ Log.e(TAG, "Received a query callback on a non-query request");
+ transaction.setResponse(new ContextHubTransaction.Response<Void>(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE, null));
+ }
+
+ @Override
+ public void onTransactionComplete(int result) {
+ transaction.setResponse(new ContextHubTransaction.Response<Void>(result, null));
+ }
+ };
+ }
+
+}
diff --git a/core/java/android/hardware/location/IContextHubClient.aidl b/core/java/android/hardware/location/IContextHubClient.aidl
index 1ee342e9..ca23705 100644
--- a/core/java/android/hardware/location/IContextHubClient.aidl
+++ b/core/java/android/hardware/location/IContextHubClient.aidl
@@ -18,21 +18,35 @@
import android.app.PendingIntent;
import android.hardware.location.NanoAppMessage;
+import android.hardware.location.IContextHubTransactionCallback;
/**
* @hide
*/
interface IContextHubClient {
-
- // Sends a message to a nanoapp
+ // Sends a message to a nanoapp.
int sendMessageToNanoApp(in NanoAppMessage message);
- // Closes the connection with the Context Hub
+ // Closes the connection with the Context Hub.
void close();
// Returns the unique ID for this client.
int getId();
- // Notify direct-call message callback completed
+ // Notify the framework that a client callback has finished executing.
void callbackFinished();
+
+ // Notify the framework that a reliable message client callback has
+ // finished executing.
+ void reliableMessageCallbackFinished(int messageSequenceNumber, byte errorCode);
+
+ /**
+ * Sends a reliable message to a nanoapp.
+ *
+ * @param message The message to send.
+ * @param transactionCallback The transaction callback for reliable message.
+ */
+ int sendReliableMessageToNanoApp(
+ in NanoAppMessage message,
+ in IContextHubTransactionCallback transactionCallback);
}
diff --git a/core/java/android/hardware/location/NanoAppMessage.java b/core/java/android/hardware/location/NanoAppMessage.java
index 7ac1dd1..48aa1bd 100644
--- a/core/java/android/hardware/location/NanoAppMessage.java
+++ b/core/java/android/hardware/location/NanoAppMessage.java
@@ -15,9 +15,11 @@
*/
package android.hardware.location;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.chre.flags.Flags;
import android.os.Parcel;
import android.os.Parcelable;
@@ -39,13 +41,17 @@
private int mMessageType;
private byte[] mMessageBody;
private boolean mIsBroadcasted;
+ private boolean mIsReliable;
+ private int mMessageSequenceNumber;
- private NanoAppMessage(
- long nanoAppId, int messageType, byte[] messageBody, boolean broadcasted) {
+ private NanoAppMessage(long nanoAppId, int messageType, byte[] messageBody,
+ boolean broadcasted, boolean isReliable, int messageSequenceNumber) {
mNanoAppId = nanoAppId;
mMessageType = messageType;
mMessageBody = messageBody;
mIsBroadcasted = broadcasted;
+ mIsReliable = isReliable;
+ mMessageSequenceNumber = messageSequenceNumber;
}
/**
@@ -62,10 +68,10 @@
*
* @return the NanoAppMessage object
*/
- public static NanoAppMessage createMessageToNanoApp(
- long targetNanoAppId, int messageType, byte[] messageBody) {
- return new NanoAppMessage(
- targetNanoAppId, messageType, messageBody, false /* broadcasted */);
+ public static NanoAppMessage createMessageToNanoApp(long targetNanoAppId, int messageType,
+ byte[] messageBody) {
+ return new NanoAppMessage(targetNanoAppId, messageType, messageBody,
+ false /* broadcasted */, false /* isReliable */, 0 /* messageSequenceNumber */);
}
/**
@@ -81,9 +87,33 @@
*
* @return the NanoAppMessage object
*/
- public static NanoAppMessage createMessageFromNanoApp(
- long sourceNanoAppId, int messageType, byte[] messageBody, boolean broadcasted) {
- return new NanoAppMessage(sourceNanoAppId, messageType, messageBody, broadcasted);
+ public static NanoAppMessage createMessageFromNanoApp(long sourceNanoAppId, int messageType,
+ byte[] messageBody, boolean broadcasted) {
+ return new NanoAppMessage(sourceNanoAppId, messageType, messageBody, broadcasted,
+ false /* isReliable */, 0 /* messageSequenceNumber */);
+ }
+
+ /**
+ * Creates a NanoAppMessage object sent from a nanoapp.
+ *
+ * This factory method is intended only to be used by the Context Hub Service when delivering
+ * messages from a nanoapp to clients.
+ *
+ * @param sourceNanoAppId the ID of the nanoapp that the message was sent from
+ * @param messageType the nanoapp-dependent message type
+ * @param messageBody the byte array message contents
+ * @param broadcasted {@code true} if the message was broadcasted, {@code false} otherwise
+ * @param isReliable if the NanoAppMessage is reliable
+ * @param messageSequenceNumber the message sequence number of the NanoAppMessage
+ *
+ * @return the NanoAppMessage object
+ */
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public static @NonNull NanoAppMessage createMessageFromNanoApp(long sourceNanoAppId,
+ int messageType, @NonNull byte[] messageBody, boolean broadcasted, boolean isReliable,
+ int messageSequenceNumber) {
+ return new NanoAppMessage(sourceNanoAppId, messageType, messageBody, broadcasted,
+ isReliable, messageSequenceNumber);
}
/**
@@ -114,6 +144,40 @@
return mIsBroadcasted;
}
+ /**
+ * Returns if the message is reliable. The default value is {@code false}
+ * @return {@code true} if the message is reliable, {@code false} otherwise
+ */
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public boolean isReliable() {
+ return mIsReliable;
+ }
+
+ /**
+ * Returns the message sequence number. The default value is 0
+ * @return the message sequence number of the message
+ */
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public int getMessageSequenceNumber() {
+ return mMessageSequenceNumber;
+ }
+
+ /**
+ * Sets the isReliable field of the message
+ */
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public void setIsReliable(boolean isReliable) {
+ mIsReliable = isReliable;
+ }
+
+ /**
+ * Sets the message sequence number of the message
+ */
+ @FlaggedApi(Flags.FLAG_RELIABLE_MESSAGE)
+ public void setMessageSequenceNumber(int messageSequenceNumber) {
+ mMessageSequenceNumber = messageSequenceNumber;
+ }
+
private NanoAppMessage(Parcel in) {
mNanoAppId = in.readLong();
mIsBroadcasted = (in.readInt() == 1);
@@ -122,6 +186,9 @@
int msgSize = in.readInt();
mMessageBody = new byte[msgSize];
in.readByteArray(mMessageBody);
+
+ mIsReliable = (in.readInt() == 1);
+ mMessageSequenceNumber = in.readInt();
}
@Override
@@ -137,6 +204,9 @@
out.writeInt(mMessageBody.length);
out.writeByteArray(mMessageBody);
+
+ out.writeInt(mIsReliable ? 1 : 0);
+ out.writeInt(mMessageSequenceNumber);
}
public static final @NonNull Creator<NanoAppMessage> CREATOR =
@@ -159,7 +229,9 @@
String ret = "NanoAppMessage[type = " + mMessageType + ", length = " + mMessageBody.length
+ " bytes, " + (mIsBroadcasted ? "broadcast" : "unicast") + ", nanoapp = 0x"
- + Long.toHexString(mNanoAppId) + "](";
+ + Long.toHexString(mNanoAppId) + ", isReliable = "
+ + (mIsReliable ? "true" : "false") + ", messageSequenceNumber = "
+ + mMessageSequenceNumber + "](";
if (length > 0) {
ret += "data = 0x";
}
@@ -190,7 +262,11 @@
isEqual = (other.getNanoAppId() == mNanoAppId)
&& (other.getMessageType() == mMessageType)
&& (other.isBroadcastMessage() == mIsBroadcasted)
- && Arrays.equals(other.getMessageBody(), mMessageBody);
+ && Arrays.equals(other.getMessageBody(), mMessageBody)
+ && (!Flags.reliableMessage()
+ || (other.isReliable() == mIsReliable))
+ && (!Flags.reliableMessage()
+ || (other.getMessageSequenceNumber() == mMessageSequenceNumber));
}
return isEqual;
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index b68b94d..a6b2ed0 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -31,6 +31,7 @@
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.location.GnssSignalQuality;
+import android.net.NetworkCapabilities;
import android.os.BatteryStatsManager.WifiState;
import android.os.BatteryStatsManager.WifiSupplState;
import android.server.ServerProtoEnums;
@@ -59,6 +60,7 @@
import com.android.internal.os.CpuScalingPolicies;
import com.android.internal.os.MonotonicClock;
import com.android.internal.os.PowerStats;
+import com.android.net.module.util.NetworkCapabilitiesUtils;
import com.google.android.collect.Lists;
@@ -2734,26 +2736,28 @@
"emngcy", "other"
};
+ public static final int NUM_ALL_NETWORK_TYPES = getAllNetworkTypesCount();
public static final int DATA_CONNECTION_OUT_OF_SERVICE = 0;
- public static final int DATA_CONNECTION_EMERGENCY_SERVICE = getEmergencyNetworkConnectionType();
- public static final int DATA_CONNECTION_OTHER = DATA_CONNECTION_EMERGENCY_SERVICE + 1;
+ public static final int DATA_CONNECTION_EMERGENCY_SERVICE = NUM_ALL_NETWORK_TYPES + 1;
+ public static final int DATA_CONNECTION_OTHER = NUM_ALL_NETWORK_TYPES + 2;
@UnsupportedAppUsage
- public static final int NUM_DATA_CONNECTION_TYPES = DATA_CONNECTION_OTHER + 1;
+ public static final int NUM_DATA_CONNECTION_TYPES = NUM_ALL_NETWORK_TYPES + 3;
+
@android.ravenwood.annotation.RavenwoodReplace
- private static int getEmergencyNetworkConnectionType() {
+ public static int getAllNetworkTypesCount() {
int count = TelephonyManager.getAllNetworkTypes().length;
if (DATA_CONNECTION_NAMES.length != count + 3) { // oos, emngcy, other
throw new IllegalStateException(
"DATA_CONNECTION_NAMES length does not match network type count. "
+ "Expected: " + (count + 3) + ", actual:" + DATA_CONNECTION_NAMES.length);
}
- return count + 1;
+ return count;
}
- private static int getEmergencyNetworkConnectionType$ravenwood() {
- return DATA_CONNECTION_NAMES.length - 2;
+ public static int getAllNetworkTypesCount$ravenwood() {
+ return DATA_CONNECTION_NAMES.length - 3; // oos, emngcy, other
}
/**
@@ -9071,4 +9075,31 @@
protected static boolean isKernelStatsAvailable$ravenwood() {
return false;
}
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ protected static int getDisplayTransport(int[] transports) {
+ return NetworkCapabilitiesUtils.getDisplayTransport(transports);
+ }
+
+ // See NetworkCapabilitiesUtils
+ private static final int[] DISPLAY_TRANSPORT_PRIORITIES = new int[] {
+ NetworkCapabilities.TRANSPORT_VPN,
+ NetworkCapabilities.TRANSPORT_CELLULAR,
+ NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+ NetworkCapabilities.TRANSPORT_BLUETOOTH,
+ NetworkCapabilities.TRANSPORT_WIFI,
+ NetworkCapabilities.TRANSPORT_ETHERNET,
+ NetworkCapabilities.TRANSPORT_USB
+ };
+
+ protected static int getDisplayTransport$ravenwood(int[] transports) {
+ for (int transport : DISPLAY_TRANSPORT_PRIORITIES) {
+ for (int t : transports) {
+ if (t == transport) {
+ return transport;
+ }
+ }
+ }
+ return transports[0];
+ }
}
diff --git a/core/java/android/os/BluetoothBatteryStats.java b/core/java/android/os/BluetoothBatteryStats.java
index 3d99a08..fa8f39d 100644
--- a/core/java/android/os/BluetoothBatteryStats.java
+++ b/core/java/android/os/BluetoothBatteryStats.java
@@ -26,6 +26,7 @@
*
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class BluetoothBatteryStats implements Parcelable {
/** @hide */
diff --git a/core/java/android/os/UserBatteryConsumer.java b/core/java/android/os/UserBatteryConsumer.java
index a2ff078..23ba0c6 100644
--- a/core/java/android/os/UserBatteryConsumer.java
+++ b/core/java/android/os/UserBatteryConsumer.java
@@ -34,6 +34,7 @@
*
* {@hide}
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class UserBatteryConsumer extends BatteryConsumer {
static final int CONSUMER_TYPE_USER = 2;
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 0fbdbc4..de32423 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -180,11 +180,14 @@
/**
- * User type representing a private profile.
+ * User type representing a private profile. Private profile is a user profile that can be used
+ * as an alternative user-space to install and use sensitive apps.
+ * UI surfaces can adopt an alternative strategy to show apps belonging to this profile, in line
+ * with their sensitive nature.
* @hide
*/
@FlaggedApi(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE)
- @TestApi
+ @SystemApi
public static final String USER_TYPE_PROFILE_PRIVATE = "android.os.usertype.profile.PRIVATE";
/**
@@ -1424,8 +1427,8 @@
public static final String DISALLOW_RECORD_AUDIO = "no_record_audio";
/**
- * Specifies if a user is not allowed to run in the background and should be stopped during
- * user switch. The default value is <code>false</code>.
+ * Specifies if a user is not allowed to run in the background and should be stopped and locked
+ * during user switch. The default value is <code>false</code>.
*
* <p>This restriction can be set by device owners and profile owners.
*
diff --git a/core/java/android/os/WakeLockStats.java b/core/java/android/os/WakeLockStats.java
index 05a7313..69e70a0 100644
--- a/core/java/android/os/WakeLockStats.java
+++ b/core/java/android/os/WakeLockStats.java
@@ -25,6 +25,7 @@
* Snapshot of wake lock stats.
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class WakeLockStats implements Parcelable {
/** @hide */
diff --git a/core/java/android/provider/BlockedNumberContract.java b/core/java/android/provider/BlockedNumberContract.java
index 5d00b29..4075e90 100644
--- a/core/java/android/provider/BlockedNumberContract.java
+++ b/core/java/android/provider/BlockedNumberContract.java
@@ -15,12 +15,20 @@
*/
package android.provider;
+import android.Manifest;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.annotation.WorkerThread;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.telecom.Log;
+import android.telecom.TelecomManager;
+
+import com.android.server.telecom.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -214,6 +222,333 @@
* <p>TYPE: String</p>
*/
public static final String COLUMN_E164_NUMBER = "e164_number";
+
+ /**
+ * A protected broadcast intent action for letting components with
+ * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} know that the block suppression
+ * status as returned by {@link #getBlockSuppressionStatus(Context)} has been updated.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ACTION_BLOCK_SUPPRESSION_STATE_CHANGED =
+ "android.provider.action.BLOCK_SUPPRESSION_STATE_CHANGED";
+
+ /**
+ * Preference key of block numbers not in contacts setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED =
+ "block_numbers_not_in_contacts_setting";
+
+ /**
+ * Preference key of block private number calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_PRIVATE =
+ "block_private_number_calls_setting";
+
+ /**
+ * Preference key of block payphone calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_PAYPHONE =
+ "block_payphone_calls_setting";
+
+ /**
+ * Preference key of block unknown calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_UNKNOWN =
+ "block_unknown_calls_setting";
+
+ /**
+ * Preference key for whether should show an emergency call notification.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION =
+ "show_emergency_call_notification";
+
+ /**
+ * Preference key of block unavailable calls setting.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final String ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE =
+ "block_unavailable_calls_setting";
+
+ /**
+ * Notifies the provider that emergency services were contacted by the user.
+ * <p> This results in {@link #shouldSystemBlockNumber} returning {@code false} independent
+ * of the contents of the provider for a duration defined by
+ * {@link android.telephony.CarrierConfigManager#KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT}
+ * the provider unless {@link #endBlockSuppression(Context)} is called.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static void notifyEmergencyContact(@NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ try {
+ Log.i(LOG_TAG, "notifyEmergencyContact; caller=%s", context.getOpPackageName());
+ context.getContentResolver().call(
+ AUTHORITY_URI, SystemContract.METHOD_NOTIFY_EMERGENCY_CONTACT, null, null);
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "notifyEmergencyContact: provider not ready.");
+ }
+ }
+
+ /**
+ * Notifies the provider to disable suppressing blocking. If emergency services were not
+ * contacted recently at all, calling this method is a no-op.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static void endBlockSuppression(@NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ String caller = context.getOpPackageName();
+ Log.i(LOG_TAG, "endBlockSuppression: caller=%s", caller);
+ context.getContentResolver().call(
+ AUTHORITY_URI, SystemContract.METHOD_END_BLOCK_SUPPRESSION, null, null);
+ }
+
+ /**
+ * Returns {@code true} if {@code phoneNumber} is blocked taking
+ * {@link #notifyEmergencyContact(Context)} into consideration. If emergency services
+ * have not been contacted recently and enhanced call blocking not been enabled, this
+ * method is equivalent to {@link #isBlocked(Context, String)}.
+ *
+ * @param context the context of the caller.
+ * @param phoneNumber the number to check.
+ * @param numberPresentation the presentation code associated with the call.
+ * @param isNumberInContacts indicates if the provided number exists as a contact.
+ * @return result code indicating if the number should be blocked, and if so why.
+ * Valid values are: {@link #STATUS_NOT_BLOCKED}, {@link #STATUS_BLOCKED_IN_LIST},
+ * {@link #STATUS_BLOCKED_NOT_IN_CONTACTS}, {@link #STATUS_BLOCKED_PAYPHONE},
+ * {@link #STATUS_BLOCKED_RESTRICTED}, {@link #STATUS_BLOCKED_UNKNOWN_NUMBER}.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static int shouldSystemBlockNumber(@NonNull Context context,
+ @NonNull String phoneNumber, @TelecomManager.Presentation int numberPresentation,
+ boolean isNumberInContacts) {
+ verifyBlockedNumbersPermission(context);
+ try {
+ String caller = context.getOpPackageName();
+ Bundle extras = new Bundle();
+ extras.putInt(BlockedNumberContract.EXTRA_CALL_PRESENTATION, numberPresentation);
+ extras.putBoolean(BlockedNumberContract.EXTRA_CONTACT_EXIST, isNumberInContacts);
+ final Bundle res = context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_SHOULD_SYSTEM_BLOCK_NUMBER, phoneNumber, extras);
+ int blockResult = res != null ? res.getInt(RES_BLOCK_STATUS, STATUS_NOT_BLOCKED) :
+ BlockedNumberContract.STATUS_NOT_BLOCKED;
+ Log.d(LOG_TAG, "shouldSystemBlockNumber: number=%s, caller=%s, result=%s",
+ Log.piiHandle(phoneNumber), caller,
+ SystemContract.blockStatusToString(blockResult));
+ return blockResult;
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "shouldSystemBlockNumber: provider not ready.");
+ return BlockedNumberContract.STATUS_NOT_BLOCKED;
+ }
+ }
+
+ /**
+ * Returns the current status of block suppression.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static @NonNull BlockSuppressionStatus getBlockSuppressionStatus(
+ @NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ final Bundle res = context.getContentResolver().call(
+ AUTHORITY_URI, SystemContract.METHOD_GET_BLOCK_SUPPRESSION_STATUS, null, null);
+ BlockSuppressionStatus blockSuppressionStatus = new BlockSuppressionStatus(
+ res.getBoolean(SystemContract.RES_IS_BLOCKING_SUPPRESSED, false),
+ res.getLong(SystemContract.RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP, 0));
+ Log.d(LOG_TAG, "getBlockSuppressionStatus: caller=%s, status=%s",
+ context.getOpPackageName(), blockSuppressionStatus);
+ return blockSuppressionStatus;
+ }
+
+ /**
+ * Check whether should show the emergency call notification.
+ *
+ * @param context the context of the caller.
+ * @return {@code true} if should show emergency call notification. {@code false} otherwise.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static boolean shouldShowEmergencyCallNotification(@NonNull Context context) {
+ verifyBlockedNumbersPermission(context);
+ try {
+ final Bundle res = context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_SHOULD_SHOW_EMERGENCY_CALL_NOTIFICATION, null, null);
+ return res != null && res.getBoolean(RES_SHOW_EMERGENCY_CALL_NOTIFICATION, false);
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "shouldShowEmergencyCallNotification: provider not ready.");
+ return false;
+ }
+ }
+
+ /**
+ * Check whether the enhanced block setting is enabled.
+ *
+ * @param context the context of the caller.
+ * @param key the key of the setting to check, can be
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PRIVATE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
+ * @return {@code true} if the setting is enabled. {@code false} otherwise.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static boolean getBlockedNumberSetting(
+ @NonNull Context context, @NonNull String key) {
+ verifyBlockedNumbersPermission(context);
+ Bundle extras = new Bundle();
+ extras.putString(EXTRA_ENHANCED_SETTING_KEY, key);
+ try {
+ final Bundle res = context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_GET_ENHANCED_BLOCK_SETTING, null, extras);
+ return res != null && res.getBoolean(RES_ENHANCED_SETTING_IS_ENABLED, false);
+ } catch (NullPointerException | IllegalArgumentException ex) {
+ // The content resolver can throw an NPE or IAE; we don't want to crash Telecom if
+ // either of these happen.
+ Log.w(null, "getEnhancedBlockSetting: provider not ready.");
+ return false;
+ }
+ }
+
+ /**
+ * Set the enhanced block setting enabled status.
+ *
+ * @param context the context of the caller.
+ * @param key the key of the setting to set, can be
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNREGISTERED}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PRIVATE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
+ * {@link SystemContract#ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
+ * @param value the enabled statue of the setting to set.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(allOf = {
+ android.Manifest.permission.READ_BLOCKED_NUMBERS,
+ android.Manifest.permission.WRITE_BLOCKED_NUMBERS
+ })
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static void setBlockedNumberSetting(@NonNull Context context,
+ @NonNull String key, boolean value) {
+ verifyBlockedNumbersPermission(context);
+ Bundle extras = new Bundle();
+ extras.putString(EXTRA_ENHANCED_SETTING_KEY, key);
+ extras.putBoolean(EXTRA_ENHANCED_SETTING_VALUE, value);
+ context.getContentResolver().call(AUTHORITY_URI,
+ SystemContract.METHOD_SET_ENHANCED_BLOCK_SETTING, null, extras);
+ }
+
+ /**
+ * Represents the current status of
+ * {@link #shouldSystemBlockNumber(Context, String, int, boolean)}. If emergency services
+ * have been contacted recently, {@link #mIsSuppressed} is {@code true}, and blocking
+ * is disabled until the timestamp {@link #mUntilTimestampMillis}.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
+ public static final class BlockSuppressionStatus {
+ private boolean mIsSuppressed;
+
+ /**
+ * Timestamp in milliseconds from epoch.
+ */
+ private long mUntilTimestampMillis;
+
+ public BlockSuppressionStatus(boolean isSuppressed, long untilTimestampMillis) {
+ this.mIsSuppressed = isSuppressed;
+ this.mUntilTimestampMillis = untilTimestampMillis;
+ }
+
+ @Override
+ public String toString() {
+ return "[BlockSuppressionStatus; isSuppressed=" + mIsSuppressed + ", until="
+ + mUntilTimestampMillis + "]";
+ }
+
+ public boolean getIsSuppressed() {
+ return mIsSuppressed;
+ }
+
+ public long getUntilTimestampMillis() {
+ return mUntilTimestampMillis;
+ }
+ }
+
+ /**
+ * Verifies that the caller holds both the
+ * {@link android.Manifest.permission#READ_BLOCKED_NUMBERS} permission and the
+ * {@link android.Manifest.permission#WRITE_BLOCKED_NUMBERS} permission.
+ *
+ * @param context
+ * @throws SecurityException if the caller is missing the necessary permissions
+ */
+ private static void verifyBlockedNumbersPermission(Context context) {
+ context.enforceCallingOrSelfPermission(Manifest.permission.READ_BLOCKED_NUMBERS,
+ "Caller does not have the android.permission.READ_BLOCKED_NUMBERS permission");
+ context.enforceCallingOrSelfPermission(Manifest.permission.WRITE_BLOCKED_NUMBERS,
+ "Caller does not have the android.permission.WRITE_BLOCKED_NUMBERS permission");
+ }
}
/** @hide */
@@ -558,7 +893,7 @@
* {@link #ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
- * {@link #ENHANCED_SETTING_KEY_EMERGENCY_CALL_NOTIFICATION_SHOWING}
+ * {@link #ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
* @return {@code true} if the setting is enabled. {@code false} otherwise.
*/
public static boolean getEnhancedBlockSetting(Context context, String key) {
@@ -586,7 +921,7 @@
* {@link #ENHANCED_SETTING_KEY_BLOCK_PAYPHONE}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNKNOWN}
* {@link #ENHANCED_SETTING_KEY_BLOCK_UNAVAILABLE}
- * {@link #ENHANCED_SETTING_KEY_EMERGENCY_CALL_NOTIFICATION_SHOWING}
+ * {@link #ENHANCED_SETTING_KEY_SHOW_EMERGENCY_CALL_NOTIFICATION}
* @param value the enabled statue of the setting to set.
*/
public static void setEnhancedBlockSetting(Context context, String key, boolean value) {
diff --git a/core/java/android/provider/CallLog.java b/core/java/android/provider/CallLog.java
index 7d127ad..c13dd36 100644
--- a/core/java/android/provider/CallLog.java
+++ b/core/java/android/provider/CallLog.java
@@ -19,6 +19,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.LongDef;
import android.annotation.NonNull;
@@ -55,6 +56,8 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.server.telecom.flags.Flags;
+
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
@@ -427,6 +430,8 @@
private double mLongitude = Double.NaN;
private Uri mPictureUri;
private int mIsPhoneAccountMigrationPending;
+ private boolean mIsBusinessCall;
+ private String mBusinessName;
/**
* @param callerInfo the CallerInfo object to get the target contact from.
@@ -645,15 +650,44 @@
}
/**
+ * @param isBusinessCall should be set if the caller is a business call
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public @NonNull AddCallParametersBuilder setIsBusinessCall(boolean isBusinessCall) {
+ mIsBusinessCall = isBusinessCall;
+ return this;
+ }
+
+ /**
+ * @param businessName should be set if the caller is a business call
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public @NonNull AddCallParametersBuilder setBusinessName(String businessName) {
+ mBusinessName = businessName;
+ return this;
+ }
+
+ /**
* Builds the object
*/
public @NonNull AddCallParams build() {
- return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
- mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
- mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead, mCallBlockReason,
- mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
- mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
- mIsPhoneAccountMigrationPending);
+ if (Flags.businessCallComposer()) {
+ return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
+ mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
+ mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead,
+ mCallBlockReason,
+ mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
+ mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
+ mIsPhoneAccountMigrationPending, mIsBusinessCall, mBusinessName);
+ } else {
+ return new AddCallParams(mCallerInfo, mNumber, mPostDialDigits, mViaNumber,
+ mPresentation, mCallType, mFeatures, mAccountHandle, mStart, mDuration,
+ mDataUsage, mAddForAllUsers, mUserToBeInsertedTo, mIsRead,
+ mCallBlockReason,
+ mCallScreeningAppName, mCallScreeningComponentName, mMissedReason,
+ mPriority, mSubject, mLatitude, mLongitude, mPictureUri,
+ mIsPhoneAccountMigrationPending);
+ }
}
}
@@ -681,6 +715,8 @@
private double mLongitude = Double.NaN;
private Uri mPictureUri;
private int mIsPhoneAccountMigrationPending;
+ private boolean mIsBusinessCall;
+ private String mBusinessName;
private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits,
String viaNumber, int presentation, int callType, int features,
@@ -717,6 +753,43 @@
mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending;
}
+ private AddCallParams(CallerInfo callerInfo, String number, String postDialDigits,
+ String viaNumber, int presentation, int callType, int features,
+ PhoneAccountHandle accountHandle, long start, int duration, long dataUsage,
+ boolean addForAllUsers, UserHandle userToBeInsertedTo, boolean isRead,
+ int callBlockReason,
+ CharSequence callScreeningAppName, String callScreeningComponentName,
+ long missedReason,
+ int priority, String subject, double latitude, double longitude, Uri pictureUri,
+ int isPhoneAccountMigrationPending, boolean isBusinessCall, String businessName) {
+ mCallerInfo = callerInfo;
+ mNumber = number;
+ mPostDialDigits = postDialDigits;
+ mViaNumber = viaNumber;
+ mPresentation = presentation;
+ mCallType = callType;
+ mFeatures = features;
+ mAccountHandle = accountHandle;
+ mStart = start;
+ mDuration = duration;
+ mDataUsage = dataUsage;
+ mAddForAllUsers = addForAllUsers;
+ mUserToBeInsertedTo = userToBeInsertedTo;
+ mIsRead = isRead;
+ mCallBlockReason = callBlockReason;
+ mCallScreeningAppName = callScreeningAppName;
+ mCallScreeningComponentName = callScreeningComponentName;
+ mMissedReason = missedReason;
+ mPriority = priority;
+ mSubject = subject;
+ mLatitude = latitude;
+ mLongitude = longitude;
+ mPictureUri = pictureUri;
+ mIsPhoneAccountMigrationPending = isPhoneAccountMigrationPending;
+ mIsBusinessCall = isBusinessCall;
+ mBusinessName = businessName;
+ }
+
}
/**
@@ -915,6 +988,19 @@
*/
public static final String NUMBER = "number";
+
+ /**
+ * Boolean indicating whether the call is a business call.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String IS_BUSINESS_CALL = "is_business_call";
+
+ /**
+ * String that stores the asserted display name associated with business call.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String ASSERTED_DISPLAY_NAME = "asserted_display_name";
+
/**
* The number presenting rules set by the network.
*
@@ -1713,7 +1799,6 @@
}
ContentValues values = new ContentValues(14);
-
values.put(NUMBER, params.mNumber);
values.put(POST_DIAL_DIGITS, params.mPostDialDigits);
values.put(VIA_NUMBER, params.mViaNumber);
@@ -1746,7 +1831,10 @@
values.put(COMPOSER_PHOTO_URI, params.mPictureUri.toString());
}
values.put(IS_PHONE_ACCOUNT_MIGRATION_PENDING, params.mIsPhoneAccountMigrationPending);
-
+ if (Flags.businessCallComposer()) {
+ values.put(IS_BUSINESS_CALL, Integer.valueOf(params.mIsBusinessCall ? 1 : 0));
+ values.put(ASSERTED_DISPLAY_NAME, params.mBusinessName);
+ }
if ((params.mCallerInfo != null) && (params.mCallerInfo.getContactId() > 0)) {
// Update usage information for the number associated with the contact ID.
// We need to use both the number and the ID for obtaining a data ID since other
diff --git a/core/java/android/provider/ContactKeysManager.java b/core/java/android/provider/ContactKeysManager.java
index bef6456..01aaa3d 100644
--- a/core/java/android/provider/ContactKeysManager.java
+++ b/core/java/android/provider/ContactKeysManager.java
@@ -39,18 +39,19 @@
import java.util.Objects;
/**
- * ContactKeysManager provides the access to the E2EE contact keys provider.
- * It manages two types of keys - {@link ContactKey} of other users' and the owner's keys -
- * {@link SelfKey}.
+ * ContactKeysManager provides access to the provider of end-to-end encryption contact keys.
+ * It manages two types of keys - {@link ContactKey} and {@link SelfKey}.
* <ul>
* <li>
- * For {@link ContactKey} this API allows the insert/update, removal, changing of the
- * verification state, retrieving the keys (either created by or visible to the caller app)
- * operations.
+ * A {@link ContactKey} is a public key associated with a contact. It's used to end-to-end
+ * encrypt the communications between a user and the contact. This API allows operations on
+ * {@link ContactKey}s to insert/update, remove, change the verification state, and retrieving
+ * keys (either created by or visible to the caller app).
* </li>
* <li>
- * For {@link SelfKey} this API allows the insert/update, removal, retrieving the self keys
- * (either created by or visible to the caller app) operations.
+ * A {@link SelfKey} is a key for this device, so the key represents the owner of the device.
+ * This API allows operations on {@link SelfKey}s to insert/update, remove, and retrieving
+ * self keys (either created by or visible to the caller app).
* </li>
* </ul>
* Keys are uniquely identified by:
@@ -71,7 +72,7 @@
* ContactsProvider.
*/
@FlaggedApi(Flags.FLAG_USER_KEYS)
-public class ContactKeysManager {
+public final class ContactKeysManager {
/**
* The authority for the contact keys provider.
* @hide
@@ -354,9 +355,9 @@
private static void validateVerificationState(int verificationState) {
- if (verificationState != UNVERIFIED
- && verificationState != VERIFICATION_FAILED
- && verificationState != VERIFIED) {
+ if (verificationState != VERIFICATION_STATE_UNVERIFIED
+ && verificationState != VERIFICATION_STATE_VERIFICATION_FAILED
+ && verificationState != VERIFICATION_STATE_VERIFIED) {
throw new IllegalArgumentException("Verification state value "
+ verificationState + " is not supported");
}
@@ -600,25 +601,25 @@
* @hide
*/
@IntDef(prefix = {"VERIFICATION_STATE_"}, value = {
- UNVERIFIED,
- VERIFICATION_FAILED,
- VERIFIED
+ VERIFICATION_STATE_UNVERIFIED,
+ VERIFICATION_STATE_VERIFICATION_FAILED,
+ VERIFICATION_STATE_VERIFIED
})
@Retention(RetentionPolicy.SOURCE)
public @interface VerificationState {}
/**
- * Unverified state of a contact E2EE key.
+ * Unverified state of a contact end to end encrypted key.
*/
- public static final int UNVERIFIED = 0;
+ public static final int VERIFICATION_STATE_UNVERIFIED = 0;
/**
- * Failed verification state of a contact E2EE key.
+ * Failed verification state of a contact end to end encrypted key.
*/
- public static final int VERIFICATION_FAILED = 1;
+ public static final int VERIFICATION_STATE_VERIFICATION_FAILED = 1;
/**
- * Verified state of a contact E2EE key.
+ * Verified state of a contact end to end encrypted key.
*/
- public static final int VERIFIED = 2;
+ public static final int VERIFICATION_STATE_VERIFIED = 2;
/** @hide */
public static final class ContactKeys {
@@ -791,7 +792,7 @@
}
/**
- * A parcelable class encapsulating other users' E2EE contact key.
+ * A parcelable class encapsulating other users' end to end encrypted contact key.
*/
public static final class ContactKey implements Parcelable {
/**
@@ -1056,7 +1057,7 @@
}
/**
- * A parcelable class encapsulating self E2EE contact key.
+ * A parcelable class encapsulating self end to end encrypted contact key.
*/
public static final class SelfKey implements Parcelable {
/**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index be9915c..b026ce9 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -668,6 +668,23 @@
"android.settings.MANAGE_APP_LONG_RUNNING_JOBS";
/**
+ * Activity Action: Show settings to allow configuration of
+ * {@link Manifest.permission#RUN_BACKUP_JOBS} permission.
+ *
+ * Input: Optionally, the Intent's data URI can specify the application package name to
+ * directly invoke the management GUI specific to the package name. For example
+ * "package:com.my.app".
+ * <p>
+ * Output: When a package data uri is passed as input, the activity result is set to
+ * {@link android.app.Activity#RESULT_OK} if the permission was granted to the app. Otherwise,
+ * the result is set to {@link android.app.Activity#RESULT_CANCELED}.
+ */
+ @FlaggedApi(Flags.FLAG_BACKUP_TASKS_SETTINGS_SCREEN)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_REQUEST_RUN_BACKUP_JOBS =
+ "android.settings.REQUEST_RUN_BACKUP_JOBS";
+
+ /**
* Activity Action: Show settings to allow configuration of cross-profile access for apps
*
* Input: Optionally, the Intent's data URI can specify the application package name to
@@ -11830,6 +11847,13 @@
public static final String MEDIA_CONTROLS_LOCK_SCREEN = "media_controls_lock_screen";
/**
+ * Whether to enable camera extensions software fallback.
+ * @hide
+ */
+ @Readable
+ public static final String CAMERA_EXTENSIONS_FALLBACK = "camera_extensions_fallback";
+
+ /**
* Controls whether contextual suggestions can be shown in the media controls.
* @hide
*/
diff --git a/core/java/android/provider/flags.aconfig b/core/java/android/provider/flags.aconfig
index 0f12b13..ea1ac27 100644
--- a/core/java/android/provider/flags.aconfig
+++ b/core/java/android/provider/flags.aconfig
@@ -13,3 +13,10 @@
description: "This flag controls new E2EE contact keys API"
bug: "290696572"
}
+
+flag {
+ name: "backup_tasks_settings_screen"
+ namespace: "backstage_power"
+ description: "Add a new settings page for the RUN_BACKUP_JOBS permission."
+ bug: "320563660"
+}
diff --git a/core/java/android/service/appprediction/AppPredictionService.java b/core/java/android/service/appprediction/AppPredictionService.java
index a2ffa5d..2402cfd 100644
--- a/core/java/android/service/appprediction/AppPredictionService.java
+++ b/core/java/android/service/appprediction/AppPredictionService.java
@@ -18,6 +18,7 @@
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.annotation.CallSuper;
+import android.annotation.FlaggedApi;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -31,12 +32,15 @@
import android.app.prediction.IPredictionCallback;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.Looper;
import android.os.RemoteException;
import android.service.appprediction.IPredictionService.Stub;
+import android.service.appprediction.flags.Flags;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
@@ -134,6 +138,16 @@
obtainMessage(AppPredictionService::doDestroyPredictionSession,
AppPredictionService.this, sessionId));
}
+
+ @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API)
+ @Override
+ public void requestServiceFeatures(AppPredictionSessionId sessionId,
+ IRemoteCallback callback) {
+ mHandler.sendMessage(
+ obtainMessage(AppPredictionService::onRequestServiceFeatures,
+ AppPredictionService.this, sessionId,
+ new RemoteCallbackWrapper(callback, null)));
+ }
};
@CallSuper
@@ -277,6 +291,18 @@
public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {}
/**
+ * Called by the client app to request {@link AppPredictionService} features info.
+ *
+ * @param sessionId the session's Id. It is @NonNull.
+ * @param callback the callback to return the Bundle which includes service features info. It
+ * is @NonNull.
+ */
+ @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API)
+ @MainThread
+ public void onRequestServiceFeatures(@NonNull AppPredictionSessionId sessionId,
+ @NonNull Consumer<Bundle> callback) {}
+
+ /**
* Used by the prediction factory to send back results the client app. The can be called
* in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as
* a result of changes in predictions.
@@ -357,4 +383,50 @@
}
}
}
+
+ private static final class RemoteCallbackWrapper implements Consumer<Bundle>,
+ IBinder.DeathRecipient {
+
+ private IRemoteCallback mCallback;
+ private final Consumer<RemoteCallbackWrapper> mOnBinderDied;
+
+ RemoteCallbackWrapper(IRemoteCallback callback,
+ @Nullable Consumer<RemoteCallbackWrapper> onBinderDied) {
+ mCallback = callback;
+ mOnBinderDied = onBinderDied;
+ if (mOnBinderDied != null) {
+ try {
+ mCallback.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to link to death: " + e);
+ }
+ }
+ }
+
+ public void destroy() {
+ if (mCallback != null && mOnBinderDied != null) {
+ mCallback.asBinder().unlinkToDeath(this, 0);
+ }
+ }
+
+ @Override
+ public void accept(Bundle bundle) {
+ try {
+ if (mCallback != null) {
+ mCallback.sendResult(bundle);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error sending result:" + e);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ destroy();
+ mCallback = null;
+ if (mOnBinderDied != null) {
+ mOnBinderDied.accept(this);
+ }
+ }
+ }
}
diff --git a/core/java/android/service/appprediction/IPredictionService.aidl b/core/java/android/service/appprediction/IPredictionService.aidl
index 0f3df85..e144dfa 100644
--- a/core/java/android/service/appprediction/IPredictionService.aidl
+++ b/core/java/android/service/appprediction/IPredictionService.aidl
@@ -22,6 +22,7 @@
import android.app.prediction.AppPredictionSessionId;
import android.app.prediction.IPredictionCallback;
import android.content.pm.ParceledListSlice;
+import android.os.IRemoteCallback;
/**
* Interface from the system to a prediction service.
@@ -50,4 +51,6 @@
void requestPredictionUpdate(in AppPredictionSessionId sessionId);
void onDestroyPredictionSession(in AppPredictionSessionId sessionId);
+
+ void requestServiceFeatures(in AppPredictionSessionId sessionId, in IRemoteCallback callback);
}
diff --git a/core/java/android/service/appprediction/flags/flags.aconfig b/core/java/android/service/appprediction/flags/flags.aconfig
new file mode 100644
index 0000000..c7e47d4
--- /dev/null
+++ b/core/java/android/service/appprediction/flags/flags.aconfig
@@ -0,0 +1,8 @@
+package: "android.service.appprediction.flags"
+
+flag {
+ name: "service_features_api"
+ namespace: "systemui"
+ description: "Guards the new requestServiceFeatures api"
+ bug: "292565550"
+}
\ No newline at end of file
diff --git a/core/java/android/service/chooser/ChooserResult.java b/core/java/android/service/chooser/ChooserResult.java
new file mode 100644
index 0000000..4603be1
--- /dev/null
+++ b/core/java/android/service/chooser/ChooserResult.java
@@ -0,0 +1,173 @@
+/*
+ * 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 android.service.chooser;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
+import android.compat.annotation.Overridable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * An event reported to a supplied [IntentSender] by the system chooser when an activity is selected
+ * or other actions are taken to complete the session.
+ *
+ * @see Intent#EXTRA_CHOOSER_RESULT_INTENT_SENDER
+ */
+@FlaggedApi(android.service.chooser.Flags.FLAG_ENABLE_CHOOSER_RESULT)
+public final class ChooserResult implements Parcelable {
+
+ /**
+ * Controls whether to send ChooserResult to the optional IntentSender supplied to the Chooser.
+ * <p>
+ * When enabled, ChooserResult is added to the provided Intent as
+ * {@link Intent#EXTRA_CHOOSER_RESULT}, and sent for actions such as copy and edit, in addition
+ * to activity selection. When disabled, only the selected component
+ * is provided in {@link Intent#EXTRA_CHOSEN_COMPONENT}.
+ * <p>
+ * See: {@link Intent#createChooser(Intent, CharSequence, IntentSender)}
+ *
+ * @hide
+ */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ @Overridable
+ public static final long SEND_CHOOSER_RESULT = 263474465L;
+
+ /** @hide */
+ @IntDef({
+ CHOOSER_RESULT_UNKNOWN,
+ CHOOSER_RESULT_SELECTED_COMPONENT,
+ CHOOSER_RESULT_COPY,
+ CHOOSER_RESULT_EDIT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ResultType { }
+
+ /** An unknown action was taken to complete the session. */
+ public static final int CHOOSER_RESULT_UNKNOWN = -1;
+ /** The session was completed by selecting an activity to launch. */
+ public static final int CHOOSER_RESULT_SELECTED_COMPONENT = 0;
+ /** The session was completed by invoking the copy action. */
+ public static final int CHOOSER_RESULT_COPY = 1;
+ /** The session was completed by invoking the edit action. */
+ public static final int CHOOSER_RESULT_EDIT = 2;
+
+ @ResultType
+ private final int mType;
+ private final ComponentName mSelectedComponent;
+ private final boolean mIsShortcut;
+
+ private ChooserResult(@NonNull Parcel source) {
+ mType = source.readInt();
+ mSelectedComponent = ComponentName.readFromParcel(source);
+ mIsShortcut = source.readBoolean();
+ }
+
+ /** @hide */
+ public ChooserResult(@ResultType int type, @Nullable ComponentName componentName,
+ boolean isShortcut) {
+ mType = type;
+ mSelectedComponent = componentName;
+ mIsShortcut = isShortcut;
+ }
+
+ /**
+ * The type of the result.
+ *
+ * @return the type of the result
+ */
+ @ResultType
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Provides the component of the Activity selected for results with type
+ * when type is {@link ChooserResult#CHOOSER_RESULT_SELECTED_COMPONENT}.
+ * <p>
+ * For all other types, this value is null.
+ *
+ * @return the component name selected
+ */
+ @Nullable
+ public ComponentName getSelectedComponent() {
+ return mSelectedComponent;
+ }
+
+ /**
+ * Whether the selected component was provided by the app from as a shortcut.
+ *
+ * @return true if the selected component is a shortcut, false otherwise
+ */
+ public boolean isShortcut() {
+ return mIsShortcut;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<ChooserResult> CREATOR =
+ new Creator<>() {
+ @Override
+ public ChooserResult createFromParcel(Parcel source) {
+ return new ChooserResult(source);
+ }
+
+ @Override
+ public ChooserResult[] newArray(int size) {
+ return new ChooserResult[0];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mType);
+ ComponentName.writeToParcel(mSelectedComponent, dest);
+ dest.writeBoolean(mIsShortcut);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ChooserResult that = (ChooserResult) o;
+ return mType == that.mType
+ && mIsShortcut == that.mIsShortcut
+ && Objects.equals(mSelectedComponent, that.mSelectedComponent);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mType, mSelectedComponent, mIsShortcut);
+ }
+}
diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig
index 3cc7f5a..add575b 100644
--- a/core/java/android/service/chooser/flags.aconfig
+++ b/core/java/android/service/chooser/flags.aconfig
@@ -8,6 +8,13 @@
}
flag {
+ name: "enable_sharesheet_metadata_extra"
+ namespace: "intentresolver"
+ description: "This flag enables sharesheet metadata to be displayed to users."
+ bug: "318942069"
+}
+
+flag {
name: "support_nfc_resolver"
namespace: "systemui"
description: "This flag controls the new NFC 'resolver' activity"
@@ -20,3 +27,10 @@
description: "This flag controls content toggling in Chooser"
bug: "302691505"
}
+
+flag {
+ name: "enable_chooser_result"
+ namespace: "intentresolver"
+ description: "Provides additional callbacks with information about user actions in ChooserResult"
+ bug: "263474465"
+}
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index a6d3bb4..2028c40 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -585,9 +585,7 @@
}
if (ClientFlags.fixLineHeightForLocale()) {
- if (minimumFontMetrics == null) {
- paint.getFontMetricsIntForLocale(fm);
- } else {
+ if (minimumFontMetrics != null) {
fm.set(minimumFontMetrics);
// Because the font metrics is provided by public APIs, adjust the top/bottom with
// ascent/descent: top must be smaller than ascent, bottom must be larger than
@@ -713,18 +711,21 @@
public void draw(Canvas c, Path highlight, Paint highlightpaint,
int cursorOffset) {
if (mDirect != null && highlight == null) {
+ float leftShift = 0;
if (getUseBoundsForWidth()) {
- c.save();
RectF drawingRect = computeDrawingBoundingBox();
if (drawingRect.left < 0) {
- c.translate(-drawingRect.left, 0);
+ leftShift = -drawingRect.left;
+ c.translate(leftShift, 0);
}
}
c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
- if (getUseBoundsForWidth()) {
- c.restore();
+ if (leftShift != 0) {
+ // Manually translate back to the original position because of b/324498002, using
+ // save/restore disappears the toggle switch drawables.
+ c.translate(-leftShift, 0);
}
} else {
super.draw(c, highlight, highlightpaint, cursorOffset);
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 8ddb42d..e5d199a 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -464,11 +464,12 @@
@Nullable Path selectionPath,
@Nullable Paint selectionPaint,
int cursorOffsetVertical) {
+ float leftShift = 0;
if (mUseBoundsForWidth) {
- canvas.save();
RectF drawingRect = computeDrawingBoundingBox();
if (drawingRect.left < 0) {
- canvas.translate(-drawingRect.left, 0);
+ leftShift = -drawingRect.left;
+ canvas.translate(leftShift, 0);
}
}
final long lineRange = getLineRangeForDraw(canvas);
@@ -479,8 +480,10 @@
drawWithoutText(canvas, highlightPaths, highlightPaints, selectionPath, selectionPaint,
cursorOffsetVertical, firstLine, lastLine);
drawText(canvas, firstLine, lastLine);
- if (mUseBoundsForWidth) {
- canvas.restore();
+ if (leftShift != 0) {
+ // Manually translate back to the original position because of b/324498002, using
+ // save/restore disappears the toggle switch drawables.
+ canvas.translate(-leftShift, 0);
}
}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 99bd2ff..5986238 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -767,22 +767,14 @@
}
int defaultTop;
- int defaultAscent;
- int defaultDescent;
+ final int defaultAscent;
+ final int defaultDescent;
int defaultBottom;
- if (ClientFlags.fixLineHeightForLocale()) {
- if (b.mMinimumFontMetrics != null) {
- defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
- defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
- defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
- defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
- } else {
- paint.getFontMetricsIntForLocale(fm);
- defaultTop = fm.top;
- defaultAscent = fm.ascent;
- defaultDescent = fm.descent;
- defaultBottom = fm.bottom;
- }
+ if (ClientFlags.fixLineHeightForLocale() && b.mMinimumFontMetrics != null) {
+ defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
+ defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
+ defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
+ defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
// Because the font metrics is provided by public APIs, adjust the top/bottom with
// ascent/descent: top must be smaller than ascent, bottom must be larger than descent.
@@ -1043,10 +1035,10 @@
if (endPos < spanEnd) {
// preserve metrics for current span
- fmTop = fm.top;
- fmBottom = fm.bottom;
- fmAscent = fm.ascent;
- fmDescent = fm.descent;
+ fmTop = Math.min(defaultTop, fm.top);
+ fmBottom = Math.max(defaultBottom, fm.bottom);
+ fmAscent = Math.min(defaultAscent, fm.ascent);
+ fmDescent = Math.max(defaultDescent, fm.descent);
} else {
fmTop = fmBottom = fmAscent = fmDescent = 0;
}
@@ -1069,7 +1061,7 @@
&& mLineCount < mMaximumVisibleLineCount) {
final MeasuredParagraph measuredPara =
MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
- if (ClientFlags.fixLineHeightForLocale()) {
+ if (defaultAscent != 0 && defaultDescent != 0) {
fm.top = defaultTop;
fm.ascent = defaultAscent;
fm.descent = defaultDescent;
diff --git a/core/java/android/tracing/perfetto/TracingContext.java b/core/java/android/tracing/perfetto/TracingContext.java
index 0bce26e..c1a61a7 100644
--- a/core/java/android/tracing/perfetto/TracingContext.java
+++ b/core/java/android/tracing/perfetto/TracingContext.java
@@ -105,6 +105,5 @@
return res;
}
- // private static native void nativeFlush(long nativeDataSourcePointer);
private static native void nativeFlush(TracingContext thiz, long ctxPointer);
}
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index ec6e90b..50d419f 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -53,9 +53,12 @@
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import javax.xml.parsers.SAXParserFactory;
+
/**
* XML utility methods.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class Xml {
private Xml() {}
@@ -73,8 +76,33 @@
*
* @hide
*/
- public static final boolean ENABLE_BINARY_DEFAULT = SystemProperties
- .getBoolean("persist.sys.binary_xml", true);
+ public static final boolean ENABLE_BINARY_DEFAULT = shouldEnableBinaryDefault();
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static boolean shouldEnableBinaryDefault() {
+ return SystemProperties.getBoolean("persist.sys.binary_xml", true);
+ }
+
+ private static boolean shouldEnableBinaryDefault$ravenwood() {
+ return true;
+ }
+
+ /**
+ * Feature flag: when set, {@link #resolvePullParser(InputStream)}} will attempt to sniff
+ * using {@code pread} optimization.
+ *
+ * @hide
+ */
+ public static final boolean ENABLE_RESOLVE_OPTIMIZATIONS = shouldEnableResolveOptimizations();
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static boolean shouldEnableResolveOptimizations() {
+ return true;
+ }
+
+ private static boolean shouldEnableResolveOptimizations$ravenwood() {
+ return false;
+ }
/**
* Parses the given xml string and fires events on the given SAX handler.
@@ -82,7 +110,7 @@
public static void parse(String xml, ContentHandler contentHandler)
throws SAXException {
try {
- XMLReader reader = XmlObjectFactory.newXMLReader();
+ XMLReader reader = newXMLReader();
reader.setContentHandler(contentHandler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
@@ -96,7 +124,7 @@
*/
public static void parse(Reader in, ContentHandler contentHandler)
throws IOException, SAXException {
- XMLReader reader = XmlObjectFactory.newXMLReader();
+ XMLReader reader = newXMLReader();
reader.setContentHandler(contentHandler);
reader.parse(new InputSource(in));
}
@@ -107,7 +135,7 @@
*/
public static void parse(InputStream in, Encoding encoding,
ContentHandler contentHandler) throws IOException, SAXException {
- XMLReader reader = XmlObjectFactory.newXMLReader();
+ XMLReader reader = newXMLReader();
reader.setContentHandler(contentHandler);
InputSource source = new InputSource(in);
source.setEncoding(encoding.expatName);
@@ -120,19 +148,26 @@
@android.ravenwood.annotation.RavenwoodReplace
public static XmlPullParser newPullParser() {
try {
- XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
+ XmlPullParser parser = newXmlPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
return parser;
} catch (XmlPullParserException e) {
- throw new AssertionError();
+ throw new AssertionError(e);
}
}
/** @hide */
public static XmlPullParser newPullParser$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlPullParser();
+ try {
+ // Prebuilt kxml2-android does not support FEATURE_PROCESS_DOCDECL, so omit here;
+ // it's quite rare and all tests are passing
+ XmlPullParser parser = newXmlPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ return parser;
+ } catch (XmlPullParserException e) {
+ throw new AssertionError(e);
+ }
}
/**
@@ -145,17 +180,10 @@
* @hide
*/
@SuppressWarnings("AndroidFrameworkEfficientXml")
- @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlPullParser newFastPullParser() {
return XmlUtils.makeTyped(newPullParser());
}
- /** @hide */
- public static TypedXmlPullParser newFastPullParser$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlPullParser();
- }
-
/**
* Creates a new {@link XmlPullParser} that reads XML documents using a
* custom binary wire protocol which benchmarking has shown to be 8.5x
@@ -189,11 +217,10 @@
*
* @hide
*/
- @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
throws IOException {
final byte[] magic = new byte[4];
- if (in instanceof FileInputStream) {
+ if (ENABLE_RESOLVE_OPTIMIZATIONS && in instanceof FileInputStream) {
try {
Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
} catch (ErrnoException e) {
@@ -222,31 +249,11 @@
return xml;
}
- /** @hide */
- public static @NonNull TypedXmlPullParser resolvePullParser$ravenwood(@NonNull InputStream in)
- throws IOException {
- // TODO: remove once we're linking against libcore
- final TypedXmlPullParser xml = new BinaryXmlPullParser();
- try {
- xml.setInput(in, StandardCharsets.UTF_8.name());
- } catch (XmlPullParserException e) {
- throw new IOException(e);
- }
- return xml;
- }
-
/**
* Creates a new xml serializer.
*/
- @android.ravenwood.annotation.RavenwoodReplace
public static XmlSerializer newSerializer() {
- return XmlObjectFactory.newXmlSerializer();
- }
-
- /** @hide */
- public static XmlSerializer newSerializer$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlSerializer();
+ return newXmlSerializer();
}
/**
@@ -259,17 +266,10 @@
* @hide
*/
@SuppressWarnings("AndroidFrameworkEfficientXml")
- @android.ravenwood.annotation.RavenwoodReplace
public static @NonNull TypedXmlSerializer newFastSerializer() {
return XmlUtils.makeTyped(new FastXmlSerializer());
}
- /** @hide */
- public static @NonNull TypedXmlSerializer newFastSerializer$ravenwood() {
- // TODO: remove once we're linking against libcore
- return new BinaryXmlSerializer();
- }
-
/**
* Creates a new {@link XmlSerializer} that writes XML documents using a
* custom binary wire protocol which benchmarking has shown to be 4.4x
@@ -334,7 +334,6 @@
*
* @hide
*/
- @android.ravenwood.annotation.RavenwoodKeep
public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
throws XmlPullParserException, IOException {
// Some parsers may have already consumed the event that starts the
@@ -394,7 +393,6 @@
* unsupported, which can confuse serializers. This method normalizes empty
* strings to be {@code null}.
*/
- @android.ravenwood.annotation.RavenwoodKeep
private static @Nullable String normalizeNamespace(@Nullable String namespace) {
if (namespace == null || namespace.isEmpty()) {
return null;
@@ -457,4 +455,45 @@
? (AttributeSet) parser
: new XmlPullAttributes(parser);
}
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static @NonNull XmlSerializer newXmlSerializer() {
+ return XmlObjectFactory.newXmlSerializer();
+ }
+
+ private static @NonNull XmlSerializer newXmlSerializer$ravenwood() {
+ try {
+ return XmlPullParserFactory.newInstance().newSerializer();
+ } catch (XmlPullParserException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static @NonNull XmlPullParser newXmlPullParser() {
+ return XmlObjectFactory.newXmlPullParser();
+ }
+
+ private static @NonNull XmlPullParser newXmlPullParser$ravenwood() {
+ try {
+ return XmlPullParserFactory.newInstance().newPullParser();
+ } catch (XmlPullParserException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ }
+
+ @android.ravenwood.annotation.RavenwoodReplace
+ private static @NonNull XMLReader newXMLReader() {
+ return XmlObjectFactory.newXMLReader();
+ }
+
+ private static @NonNull XMLReader newXMLReader$ravenwood() {
+ try {
+ final SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ return factory.newSAXParser().getXMLReader();
+ } catch (Exception e) {
+ throw new UnsupportedOperationException(e);
+ }
+ }
}
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index fbadef3..0006139 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -86,6 +86,7 @@
* that are currently attached and whether mirroring has been enabled.
* </p>
*/
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
public final class Display {
private static final String TAG = "Display";
private static final boolean DEBUG = false;
@@ -1998,6 +1999,7 @@
* display power state. In SUSPEND states, updates are absolutely forbidden.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isSuspendedState(int state) {
return state == STATE_OFF || state == STATE_DOZE_SUSPEND || state == STATE_ON_SUSPEND;
}
@@ -2007,6 +2009,7 @@
* specified display power state.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isDozeState(int state) {
return state == STATE_DOZE || state == STATE_DOZE_SUSPEND;
}
@@ -2016,6 +2019,7 @@
* or {@link #STATE_VR}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isActiveState(int state) {
return state == STATE_ON || state == STATE_VR;
}
@@ -2024,6 +2028,7 @@
* Returns true if the display is in an off state such as {@link #STATE_OFF}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isOffState(int state) {
return state == STATE_OFF;
}
@@ -2033,6 +2038,7 @@
* or {@link #STATE_VR} or {@link #STATE_ON_SUSPEND}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isOnState(int state) {
return state == STATE_ON || state == STATE_VR || state == STATE_ON_SUSPEND;
}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 0ce1d47..eb28920 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -23,8 +23,10 @@
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
+import android.view.inputmethod.Flags;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
+import android.widget.Editor;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
@@ -80,6 +82,16 @@
* connections and only set mConnectedView to null when mConnectionCount is zero.
*/
private int mConnectionCount = 0;
+
+ /**
+ * The reference to the View that currently has focus.
+ * This replaces mConnecteView when {@code Flags#intitiationWithoutInputConnection()} is
+ * enabled.
+ */
+ @Nullable
+ @VisibleForTesting
+ public WeakReference<View> mFocusedView = null;
+
private final InputMethodManager mImm;
private final int[] mTempLocation = new int[2];
@@ -112,9 +124,15 @@
*
* If the stylus is hovering on an unconnected editor that supports handwriting, we always show
* the hover icon.
+ * TODO(b/308827131): Rename to FocusedView after Flag is flipped.
*/
private boolean mShowHoverIconForConnectedView = true;
+ /** When flag is enabled, touched editors don't wait for InputConnection for initiation.
+ * However, delegation still waits for InputConnection.
+ */
+ private final boolean mInitiateWithoutConnection = Flags.initiationWithoutInputConnection();
+
@VisibleForTesting
public HandwritingInitiator(@NonNull ViewConfiguration viewConfiguration,
@NonNull InputMethodManager inputMethodManager) {
@@ -201,8 +219,8 @@
View candidateView = findBestCandidateView(mState.mStylusDownX,
mState.mStylusDownY, /* isHover */ false);
if (candidateView != null) {
- if (candidateView == getConnectedView()) {
- if (!candidateView.hasFocus()) {
+ if (candidateView == getConnectedOrFocusedView()) {
+ if (!mInitiateWithoutConnection && !candidateView.hasFocus()) {
requestFocusWithoutReveal(candidateView);
}
startHandwriting(candidateView);
@@ -217,8 +235,17 @@
candidateView.getHandwritingDelegatorCallback().run();
mState.mHasPreparedHandwritingDelegation = true;
} else {
- mState.mPendingConnectedView = new WeakReference<>(candidateView);
- requestFocusWithoutReveal(candidateView);
+ if (!mInitiateWithoutConnection) {
+ mState.mPendingConnectedView = new WeakReference<>(candidateView);
+ }
+ if (!candidateView.hasFocus()) {
+ requestFocusWithoutReveal(candidateView);
+ }
+ if (mInitiateWithoutConnection
+ && updateFocusedView(candidateView,
+ /* fromTouchEvent */ true)) {
+ startHandwriting(candidateView);
+ }
}
}
}
@@ -244,11 +271,7 @@
*/
public void onDelegateViewFocused(@NonNull View view) {
if (view == getConnectedView()) {
- if (tryAcceptStylusHandwritingDelegation(view)) {
- // A handwriting delegate view is accepted and handwriting starts; hide the
- // hover icon.
- mShowHoverIconForConnectedView = false;
- }
+ tryAcceptStylusHandwritingDelegation(view);
}
}
@@ -260,6 +283,10 @@
* @see #onInputConnectionClosed(View)
*/
public void onInputConnectionCreated(@NonNull View view) {
+ if (mInitiateWithoutConnection && !view.isHandwritingDelegate()) {
+ // When flag is enabled, only delegation continues to wait for InputConnection.
+ return;
+ }
if (!view.isAutoHandwritingEnabled()) {
clearConnectedView();
return;
@@ -274,12 +301,15 @@
// A new view just gain focus. By default, we should show hover icon for it.
mShowHoverIconForConnectedView = true;
if (view.isHandwritingDelegate() && tryAcceptStylusHandwritingDelegation(view)) {
- // A handwriting delegate view is accepted and handwriting starts; hide the
- // hover icon.
+ // tryAcceptStylusHandwritingDelegation should set boolean below, however, we
+ // cannot mock IMM to return true for acceptStylusDelegation().
+ // TODO(b/324670412): we should move any dependent tests to integration and remove
+ // the assignment below.
mShowHoverIconForConnectedView = false;
return;
}
- if (mState != null && mState.mPendingConnectedView != null
+ if (!mInitiateWithoutConnection && mState != null
+ && mState.mPendingConnectedView != null
&& mState.mPendingConnectedView.get() == view) {
startHandwriting(view);
}
@@ -293,6 +323,9 @@
* @param view the view that closed the InputConnection.
*/
public void onInputConnectionClosed(@NonNull View view) {
+ if (mInitiateWithoutConnection && !view.isHandwritingDelegate()) {
+ return;
+ }
final View connectedView = getConnectedView();
if (connectedView == null) return;
if (connectedView == view) {
@@ -306,6 +339,48 @@
}
}
+ @Nullable
+ private View getFocusedView() {
+ if (mFocusedView == null) return null;
+ return mFocusedView.get();
+ }
+
+ /**
+ * Clear the tracked focused view tracked for handwriting initiation.
+ * @param view the focused view.
+ */
+ public void clearFocusedView(View view) {
+ if (view == null || mFocusedView == null) {
+ return;
+ }
+ if (mFocusedView.get() == view) {
+ mFocusedView = null;
+ }
+ }
+
+ /**
+ * Called when new {@link Editor} is focused.
+ * @return {@code true} if handwriting can initiate for given view.
+ */
+ @VisibleForTesting
+ public boolean updateFocusedView(@NonNull View view, boolean fromTouchEvent) {
+ if (!view.shouldInitiateHandwriting()) {
+ mFocusedView = null;
+ return false;
+ }
+
+ final View focusedView = getFocusedView();
+ if (focusedView != view) {
+ mFocusedView = new WeakReference<>(view);
+ // A new view just gain focus. By default, we should show hover icon for it.
+ mShowHoverIconForConnectedView = true;
+ }
+ if (!fromTouchEvent) {
+ tryAcceptStylusHandwritingDelegation(view);
+ }
+ return true;
+ }
+
/** Starts a stylus handwriting session for the view. */
@VisibleForTesting
public void startHandwriting(@NonNull View view) {
@@ -324,6 +399,9 @@
*/
@VisibleForTesting
public boolean tryAcceptStylusHandwritingDelegation(@NonNull View view) {
+ if (!view.isHandwritingDelegate() || (mState != null && mState.mHasInitiatedHandwriting)) {
+ return false;
+ }
String delegatorPackageName =
view.getAllowedHandwritingDelegatorPackageName();
if (delegatorPackageName == null) {
@@ -337,6 +415,9 @@
if (view instanceof TextView) {
((TextView) view).hideHint();
}
+ // A handwriting delegate view is accepted and handwriting starts; hide the
+ // hover icon.
+ mShowHoverIconForConnectedView = false;
return true;
}
return false;
@@ -377,16 +458,25 @@
return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING);
}
- if (hoverView != getConnectedView()) {
+ if (hoverView != getConnectedOrFocusedView()) {
// The stylus is hovering on another view that supports handwriting. We should show
- // hover icon. Also reset the mShowHoverIconForConnectedView so that hover
- // icon is displayed again next time when the stylus hovers on connected view.
+ // hover icon. Also reset the mShowHoverIconForFocusedView so that hover
+ // icon is displayed again next time when the stylus hovers on focused view.
mShowHoverIconForConnectedView = true;
return PointerIcon.getSystemIcon(context, PointerIcon.TYPE_HANDWRITING);
}
return null;
}
+ // TODO(b/308827131): Remove once Flag is flipped.
+ private View getConnectedOrFocusedView() {
+ if (mInitiateWithoutConnection) {
+ return mFocusedView == null ? null : mFocusedView.get();
+ } else {
+ return mConnectedView == null ? null : mConnectedView.get();
+ }
+ }
+
private View getCachedHoverTarget() {
if (mCachedHoverTarget == null) {
return null;
@@ -458,20 +548,21 @@
*/
@Nullable
private View findBestCandidateView(float x, float y, boolean isHover) {
+ // TODO(b/308827131): Rename to FocusedView after Flag is flipped.
// If the connectedView is not null and do not set any handwriting area, it will check
// whether the connectedView's boundary contains the initial stylus position. If true,
// directly return the connectedView.
- final View connectedView = getConnectedView();
- if (connectedView != null) {
+ final View connectedOrFocusedView = getConnectedOrFocusedView();
+ if (connectedOrFocusedView != null) {
Rect handwritingArea = mTempRect;
- if (getViewHandwritingArea(connectedView, handwritingArea)
- && isInHandwritingArea(handwritingArea, x, y, connectedView, isHover)
- && shouldTriggerStylusHandwritingForView(connectedView)) {
+ if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea)
+ && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover)
+ && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) {
if (!isHover && mState != null) {
mState.mStylusDownWithinEditorBounds =
contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
}
- return connectedView;
+ return connectedOrFocusedView;
}
}
diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl
index b5b81d1..29cc859 100644
--- a/core/java/android/view/IWindowManager.aidl
+++ b/core/java/android/view/IWindowManager.aidl
@@ -73,6 +73,7 @@
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
import android.window.ITrustedPresentationListener;
+import android.window.IUnhandledDragListener;
import android.window.InputTransferToken;
import android.window.ScreenCapture;
import android.window.TrustedPresentationThresholds;
@@ -1091,4 +1092,10 @@
@EnforcePermission("DETECT_SCREEN_RECORDING")
void unregisterScreenRecordingCallback(IScreenRecordingCallback callback);
+
+ /**
+ * Sets the listener to be called back when a cross-window drag and drop operation is unhandled
+ * (ie. not handled by any window which can handle the drag).
+ */
+ void setUnhandledDragListener(IUnhandledDragListener listener);
}
diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java
index 83bdb08..fe98fab 100644
--- a/core/java/android/view/InsetsFrameProvider.java
+++ b/core/java/android/view/InsetsFrameProvider.java
@@ -18,6 +18,7 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.IBinder;
@@ -110,6 +111,12 @@
private Insets mMinimalInsetsSizeInDisplayCutoutSafe = null;
/**
+ * Indicates the bounding rectangles within the provided insets frame, in relative coordinates
+ * to the source frame.
+ */
+ private Rect[] mBoundingRects = null;
+
+ /**
* Creates an InsetsFrameProvider which describes what frame an insets source should have.
*
* @param owner the owner of this provider. We might have multiple sources with the same type on
@@ -205,6 +212,22 @@
return mMinimalInsetsSizeInDisplayCutoutSafe;
}
+ /**
+ * Sets the bounding rectangles within and relative to the source frame.
+ */
+ public InsetsFrameProvider setBoundingRects(@Nullable Rect[] boundingRects) {
+ mBoundingRects = boundingRects == null ? null : boundingRects.clone();
+ return this;
+ }
+
+ /**
+ * Returns the arbitrary bounding rects, or null if none were set.
+ */
+ @Nullable
+ public Rect[] getBoundingRects() {
+ return mBoundingRects;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -231,6 +254,9 @@
sb.append(", mMinimalInsetsSizeInDisplayCutoutSafe=")
.append(mMinimalInsetsSizeInDisplayCutoutSafe);
}
+ if (mBoundingRects != null) {
+ sb.append(", mBoundingRects=").append(Arrays.toString(mBoundingRects));
+ }
sb.append("}");
return sb.toString();
}
@@ -257,6 +283,7 @@
mInsetsSizeOverrides = in.createTypedArray(InsetsSizeOverride.CREATOR);
mArbitraryRectangle = in.readTypedObject(Rect.CREATOR);
mMinimalInsetsSizeInDisplayCutoutSafe = in.readTypedObject(Insets.CREATOR);
+ mBoundingRects = in.createTypedArray(Rect.CREATOR);
}
@Override
@@ -268,6 +295,7 @@
out.writeTypedArray(mInsetsSizeOverrides, flags);
out.writeTypedObject(mArbitraryRectangle, flags);
out.writeTypedObject(mMinimalInsetsSizeInDisplayCutoutSafe, flags);
+ out.writeTypedArray(mBoundingRects, flags);
}
public boolean idEquals(InsetsFrameProvider o) {
@@ -288,14 +316,15 @@
&& Arrays.equals(mInsetsSizeOverrides, other.mInsetsSizeOverrides)
&& Objects.equals(mArbitraryRectangle, other.mArbitraryRectangle)
&& Objects.equals(mMinimalInsetsSizeInDisplayCutoutSafe,
- other.mMinimalInsetsSizeInDisplayCutoutSafe);
+ other.mMinimalInsetsSizeInDisplayCutoutSafe)
+ && Arrays.equals(mBoundingRects, other.mBoundingRects);
}
@Override
public int hashCode() {
return Objects.hash(mId, mSource, mFlags, mInsetsSize,
Arrays.hashCode(mInsetsSizeOverrides), mArbitraryRectangle,
- mMinimalInsetsSizeInDisplayCutoutSafe);
+ mMinimalInsetsSizeInDisplayCutoutSafe, Arrays.hashCode(mBoundingRects));
}
public static final @NonNull Parcelable.Creator<InsetsFrameProvider> CREATOR =
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index bc33d5e..f9eba29 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -38,6 +38,8 @@
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Objects;
import java.util.StringJoiner;
@@ -105,6 +107,12 @@
})
public @interface Flags {}
+ /**
+ * Used when there are no bounding rects to describe an inset, which is only possible when the
+ * insets itself is {@link Insets#NONE}.
+ */
+ private static final Rect[] NO_BOUNDING_RECTS = new Rect[0];
+
private @Flags int mFlags;
/**
@@ -117,6 +125,7 @@
/** Frame of the source in screen coordinate space */
private final Rect mFrame;
private @Nullable Rect mVisibleFrame;
+ private @Nullable Rect[] mBoundingRects;
private boolean mVisible;
@@ -127,6 +136,7 @@
private @InternalInsetsSide int mSideHint = SIDE_NONE;
private final Rect mTmpFrame = new Rect();
+ private final Rect mTmpBoundingRect = new Rect();
public InsetsSource(int id, @InsetsType int type) {
mId = id;
@@ -145,6 +155,9 @@
: null;
mFlags = other.mFlags;
mSideHint = other.mSideHint;
+ mBoundingRects = other.mBoundingRects != null
+ ? other.mBoundingRects.clone()
+ : null;
}
public void set(InsetsSource other) {
@@ -155,6 +168,9 @@
: null;
mFlags = other.mFlags;
mSideHint = other.mSideHint;
+ mBoundingRects = other.mBoundingRects != null
+ ? other.mBoundingRects.clone()
+ : null;
}
public InsetsSource setFrame(int left, int top, int right, int bottom) {
@@ -199,6 +215,15 @@
return this;
}
+ /**
+ * Set the bounding rectangles of this source. They are expected to be relative to the source
+ * frame.
+ */
+ public InsetsSource setBoundingRects(@Nullable Rect[] rects) {
+ mBoundingRects = rects != null ? rects.clone() : null;
+ return this;
+ }
+
public int getId() {
return mId;
}
@@ -228,6 +253,13 @@
}
/**
+ * Returns the bounding rectangles of this source.
+ */
+ public @Nullable Rect[] getBoundingRects() {
+ return mBoundingRects;
+ }
+
+ /**
* Calculates the insets this source will cause to a client window.
*
* @param relativeFrame The frame to calculate the insets relative to.
@@ -313,6 +345,82 @@
}
/**
+ * Calculates the bounding rects the source will cause to a client window.
+ */
+ public @NonNull Rect[] calculateBoundingRects(Rect relativeFrame, boolean ignoreVisibility) {
+ if (!ignoreVisibility && !mVisible) {
+ return NO_BOUNDING_RECTS;
+ }
+
+ final Rect frame = getFrame();
+ if (mBoundingRects == null) {
+ // No bounding rects set, make a single bounding rect that covers the intersection of
+ // the |frame| and the |relativeFrame|.
+ return mTmpBoundingRect.setIntersect(frame, relativeFrame)
+ ? new Rect[]{ new Rect(mTmpBoundingRect) }
+ : NO_BOUNDING_RECTS;
+
+ }
+
+ // Special treatment for captionBar inset type. During drag-resizing, the |frame| and
+ // |boundingRects| may not get updated as quickly as |relativeFrame|, so just assume the
+ // |frame| will always be either at the top or bottom of |relativeFrame|. This means some
+ // calculations to make |boundingRects| relative to |relativeFrame| can be skipped or
+ // simplified.
+ // TODO(b/254128050): remove special treatment.
+ if (getType() == WindowInsets.Type.captionBar()) {
+ final ArrayList<Rect> validBoundingRects = new ArrayList<>();
+ for (final Rect boundingRect : mBoundingRects) {
+ // Assume that the caption |frame| and |relativeFrame| perfectly align at the top
+ // or bottom, meaning that the provided |boundingRect|, which is relative to the
+ // |frame| either is already relative to |relativeFrame| (for top captionBar()), or
+ // just needs to be made relative to |relativeFrame| for bottom bars.
+ final int frameHeight = frame.height();
+ mTmpBoundingRect.set(boundingRect);
+ if (getId() == ID_IME_CAPTION_BAR) {
+ mTmpBoundingRect.offset(0, relativeFrame.height() - frameHeight);
+ }
+ validBoundingRects.add(new Rect(mTmpBoundingRect));
+ }
+ return validBoundingRects.toArray(new Rect[validBoundingRects.size()]);
+ }
+
+ // Regular treatment for non-captionBar inset types.
+ final ArrayList<Rect> validBoundingRects = new ArrayList<>();
+ for (final Rect boundingRect : mBoundingRects) {
+ // |boundingRect| was provided relative to |frame|. Make it absolute to be in the same
+ // coordinate system as |frame|.
+ final Rect absBoundingRect = new Rect(
+ boundingRect.left + frame.left,
+ boundingRect.top + frame.top,
+ boundingRect.right + frame.left,
+ boundingRect.bottom + frame.top
+ );
+ // Now find the intersection of that |absBoundingRect| with |relativeFrame|. In other
+ // words, whichever part of the bounding rect is inside the window frame.
+ if (!mTmpBoundingRect.setIntersect(absBoundingRect, relativeFrame)) {
+ // It's possible for this to be empty if the frame and bounding rects were larger
+ // than the |relativeFrame|, such as when a system window is wider than the app
+ // window width. Just ignore that rect since it will have no effect on the
+ // window insets.
+ continue;
+ }
+ // At this point, |mTmpBoundingRect| is a valid bounding rect located fully inside the
+ // window, convert it to be relative to the window so that apps don't need to know the
+ // location of the window to understand bounding rects.
+ validBoundingRects.add(new Rect(
+ mTmpBoundingRect.left - relativeFrame.left,
+ mTmpBoundingRect.top - relativeFrame.top,
+ mTmpBoundingRect.right - relativeFrame.left,
+ mTmpBoundingRect.bottom - relativeFrame.top));
+ }
+ if (validBoundingRects.isEmpty()) {
+ return NO_BOUNDING_RECTS;
+ }
+ return validBoundingRects.toArray(new Rect[validBoundingRects.size()]);
+ }
+
+ /**
* Outputs the intersection of two rectangles. The shared edges will also be counted in the
* intersection.
*
@@ -467,6 +575,7 @@
pw.print(" visible="); pw.print(mVisible);
pw.print(" flags="); pw.print(flagsToString(mFlags));
pw.print(" sideHint="); pw.print(sideToString(mSideHint));
+ pw.print(" boundingRects="); pw.print(Arrays.toString(mBoundingRects));
pw.println();
}
@@ -492,12 +601,14 @@
if (mSideHint != that.mSideHint) return false;
if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true;
if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false;
- return mFrame.equals(that.mFrame);
+ if (!mFrame.equals(that.mFrame)) return false;
+ return Arrays.equals(mBoundingRects, that.mBoundingRects);
}
@Override
public int hashCode() {
- return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint);
+ return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint,
+ Arrays.hashCode(mBoundingRects));
}
public InsetsSource(Parcel in) {
@@ -512,6 +623,7 @@
mVisible = in.readBoolean();
mFlags = in.readInt();
mSideHint = in.readInt();
+ mBoundingRects = in.createTypedArray(Rect.CREATOR);
}
@Override
@@ -533,6 +645,7 @@
dest.writeBoolean(mVisible);
dest.writeInt(mFlags);
dest.writeInt(mSideHint);
+ dest.writeTypedArray(mBoundingRects, flags);
}
@Override
@@ -543,6 +656,7 @@
+ " mVisible=" + mVisible
+ " mFlags=" + flagsToString(mFlags)
+ " mSideHint=" + sideToString(mSideHint)
+ + " mBoundingRects=" + Arrays.toString(mBoundingRects)
+ "}";
}
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index c88da9e..21eec67 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -128,6 +128,8 @@
final Rect relativeFrameMax = new Rect(frame);
@InsetsType int forceConsumingTypes = 0;
@InsetsType int suppressScrimTypes = 0;
+ final Rect[][] typeBoundingRectsMap = new Rect[Type.SIZE][];
+ final Rect[][] typeMaxBoundingRectsMap = new Rect[Type.SIZE][];
for (int i = mSources.size() - 1; i >= 0; i--) {
final InsetsSource source = mSources.valueAt(i);
final @InsetsType int type = source.getType();
@@ -141,7 +143,7 @@
}
processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
- idSideMap, typeVisibilityMap);
+ idSideMap, typeVisibilityMap, typeBoundingRectsMap);
// IME won't be reported in max insets as the size depends on the EditorInfo of the IME
// target.
@@ -154,7 +156,7 @@
}
processSource(ignoringVisibilitySource, relativeFrameMax,
true /* ignoreVisibility */, typeMaxInsetsMap, null /* idSideMap */,
- null /* typeVisibilityMap */);
+ null /* typeVisibilityMap */, typeMaxBoundingRectsMap);
}
}
final int softInputAdjustMode = legacySoftInputMode & SOFT_INPUT_MASK_ADJUST;
@@ -175,7 +177,8 @@
calculateRelativeRoundedCorners(frame),
calculateRelativePrivacyIndicatorBounds(frame),
calculateRelativeDisplayShape(frame),
- compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0);
+ compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0,
+ typeBoundingRectsMap, typeMaxBoundingRectsMap, frame.width(), frame.height());
}
private DisplayCutout calculateRelativeCutout(Rect frame) {
@@ -328,12 +331,13 @@
private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility,
Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap,
- @Nullable boolean[] typeVisibilityMap) {
+ @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap) {
Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
+ final Rect[] boundingRects = source.calculateBoundingRects(relativeFrame, ignoreVisibility);
final int type = source.getType();
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, type);
+ typeBoundingRectsMap, insets, boundingRects, type);
if (type == Type.MANDATORY_SYSTEM_GESTURES) {
// Mandatory system gestures are also system gestures.
@@ -342,24 +346,25 @@
// ability to set systemGestureInsets() independently from
// mandatorySystemGestureInsets() in the Builder.
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.SYSTEM_GESTURES);
+ typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES);
}
if (type == Type.CAPTION_BAR) {
// Caption should also be gesture and tappable elements. This should not be needed when
// the caption is added from the shell, as the shell can add other types at the same
// time.
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.SYSTEM_GESTURES);
+ typeBoundingRectsMap, insets, boundingRects, Type.SYSTEM_GESTURES);
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.MANDATORY_SYSTEM_GESTURES);
+ typeBoundingRectsMap, insets, boundingRects, Type.MANDATORY_SYSTEM_GESTURES);
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
- insets, Type.TAPPABLE_ELEMENT);
+ typeBoundingRectsMap, insets, boundingRects, Type.TAPPABLE_ELEMENT);
}
}
private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap,
@InternalInsetsSide @Nullable SparseIntArray idSideMap,
- @Nullable boolean[] typeVisibilityMap, Insets insets, int type) {
+ @Nullable boolean[] typeVisibilityMap, Rect[][] typeBoundingRectsMap,
+ Insets insets, Rect[] boundingRects, int type) {
int index = indexOf(type);
// Don't put Insets.NONE into typeInsetsMap. Otherwise, two WindowInsets can be considered
@@ -384,6 +389,22 @@
idSideMap.put(source.getId(), insetSide);
}
}
+
+ if (typeBoundingRectsMap != null && boundingRects.length > 0) {
+ final Rect[] existing = typeBoundingRectsMap[index];
+ if (existing == null) {
+ typeBoundingRectsMap[index] = boundingRects;
+ } else {
+ typeBoundingRectsMap[index] = concatenate(existing, boundingRects);
+ }
+ }
+ }
+
+ private static Rect[] concatenate(Rect[] a, Rect[] b) {
+ final Rect[] c = new Rect[a.length + b.length];
+ System.arraycopy(a, 0, c, 0, a.length);
+ System.arraycopy(b, 0, c, a.length, b.length);
+ return c;
}
/**
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a9f1897..3dfd68a 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30,6 +30,7 @@
import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_UNKNOWN;
import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH;
import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE;
+import static android.view.flags.Flags.FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API;
import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout;
@@ -42,6 +43,7 @@
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__SINGLE_TAP;
import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION;
+import static com.android.window.flags.Flags.FLAG_DELEGATE_UNHANDLED_DRAGS;
import static java.lang.Math.max;
@@ -67,6 +69,7 @@
import android.annotation.TestApi;
import android.annotation.UiContext;
import android.annotation.UiThread;
+import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.AutofillOptions;
import android.content.ClipData;
@@ -1946,6 +1949,41 @@
static final int TOOLTIP = 0x40000000;
/** @hide */
+ @IntDef(prefix = { "CONTENT_SENSITIVITY_" }, value = {
+ CONTENT_SENSITIVITY_AUTO,
+ CONTENT_SENSITIVITY_SENSITIVE,
+ CONTENT_SENSITIVITY_NOT_SENSITIVE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ContentSensitivity {}
+
+ /**
+ * Automatically determine whether a view displays sensitive content. For example, available
+ * autofill hints (or some other signal) can be used to determine if this view
+ * displays sensitive content.
+ *
+ * @see #getContentSensitivity()
+ */
+ @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+ public static final int CONTENT_SENSITIVITY_AUTO = 0x0;
+
+ /**
+ * The view displays sensitive content.
+ *
+ * @see #getContentSensitivity()
+ */
+ @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+ public static final int CONTENT_SENSITIVITY_SENSITIVE = 0x1;
+
+ /**
+ * The view doesn't display sensitive content.
+ *
+ * @see #getContentSensitivity()
+ */
+ @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+ public static final int CONTENT_SENSITIVITY_NOT_SENSITIVE = 0x2;
+
+ /** @hide */
@IntDef(flag = true, prefix = { "FOCUSABLES_" }, value = {
FOCUSABLES_ALL,
FOCUSABLES_TOUCH_MODE
@@ -3646,6 +3684,7 @@
* 1 PFLAG4_ROTARY_HAPTICS_ENABLED
* 1 PFLAG4_ROTARY_HAPTICS_SCROLL_SINCE_LAST_ROTARY_INPUT
* 1 PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT
+ * 11 PFLAG4_CONTENT_SENSITIVITY_MASK
* |-------|-------|-------|-------|
*/
@@ -3762,6 +3801,15 @@
*/
private static final int PFLAG4_ROTARY_HAPTICS_WAITING_FOR_SCROLL_EVENT = 0x800000;
+ private static final int PFLAG4_CONTENT_SENSITIVITY_SHIFT = 24;
+
+ /**
+ * Mask for obtaining the bits which specify how to determine whether a view
+ * displays sensitive content or not.
+ */
+ private static final int PFLAG4_CONTENT_SENSITIVITY_MASK =
+ (CONTENT_SENSITIVITY_AUTO | CONTENT_SENSITIVITY_SENSITIVE
+ | CONTENT_SENSITIVITY_NOT_SENSITIVE) << PFLAG4_CONTENT_SENSITIVITY_SHIFT;
/* End of masks for mPrivateFlags4 */
/** @hide */
@@ -5283,6 +5331,34 @@
public static final int DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION = 1 << 11;
/**
+ * Flag indicating that a drag can cross window boundaries (within the same application). When
+ * {@link #startDragAndDrop(ClipData, DragShadowBuilder, Object, int)} is called
+ * with this flag set, only visible windows belonging to the same application (ie. share the
+ * same UID) with targetSdkVersion >= {@link android.os.Build.VERSION_CODES#N API 24} will be
+ * able to participate in the drag operation and receive the dragged content.
+ *
+ * If both DRAG_FLAG_GLOBAL_SAME_APPLICATION and DRAG_FLAG_GLOBAL are set, then
+ * DRAG_FLAG_GLOBAL_SAME_APPLICATION takes precedence and the drag will only go to visible
+ * windows from the same application.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ public static final int DRAG_FLAG_GLOBAL_SAME_APPLICATION = 1 << 12;
+
+ /**
+ * Flag indicating that an unhandled drag should be delegated to the system to be started if no
+ * visible window wishes to handle the drop. When using this flag, the caller must provide
+ * ClipData with an Item that contains an immutable PendingIntent to an activity to be launched
+ * (not a broadcast, service, etc). See
+ * {@link ClipData.Item.Builder#setPendingIntent(PendingIntent)}.
+ *
+ * The system can decide to launch the intent or not based on factors like the current screen
+ * size or windowing mode. If the system does not launch the intent, it will be canceled via the
+ * normal drag and drop flow.
+ */
+ @FlaggedApi(FLAG_DELEGATE_UNHANDLED_DRAGS)
+ public static final int DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG = 1 << 13;
+
+ /**
* Vertical scroll factor cached by {@link #getVerticalScrollFactor}.
*/
private float mVerticalScrollFactor;
@@ -10150,6 +10226,54 @@
}
/**
+ * Sets content sensitivity mode to determine whether this view displays sensitive content.
+ *
+ * @param mode {@link #CONTENT_SENSITIVITY_AUTO}, {@link #CONTENT_SENSITIVITY_NOT_SENSITIVE}
+ * or {@link #CONTENT_SENSITIVITY_SENSITIVE}
+ */
+ @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+ public final void setContentSensitivity(@ContentSensitivity int mode) {
+ mPrivateFlags4 &= ~PFLAG4_CONTENT_SENSITIVITY_MASK;
+ mPrivateFlags4 |= ((mode << PFLAG4_CONTENT_SENSITIVITY_SHIFT)
+ & PFLAG4_CONTENT_SENSITIVITY_MASK);
+ }
+
+ /**
+ * Gets content sensitivity mode to determine whether this view displays sensitive content.
+ *
+ * <p>See {@link #setContentSensitivity(int)} and
+ * {@link #isContentSensitive()} for more info about this mode.
+ *
+ * @return {@link #CONTENT_SENSITIVITY_AUTO} by default, or value passed to
+ * {@link #setContentSensitivity(int)}.
+ */
+ @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+ public @ContentSensitivity
+ final int getContentSensitivity() {
+ return (mPrivateFlags4 & PFLAG4_CONTENT_SENSITIVITY_MASK)
+ >> PFLAG4_CONTENT_SENSITIVITY_SHIFT;
+ }
+
+ /**
+ * Returns whether this view displays sensitive content, based
+ * on the value explicitly set by {@link #setContentSensitivity(int)}.
+ *
+ * @return whether the view displays sensitive content.
+ *
+ * @see #setContentSensitivity(int)
+ * @see #CONTENT_SENSITIVITY_AUTO
+ * @see #CONTENT_SENSITIVITY_SENSITIVE
+ * @see #CONTENT_SENSITIVITY_NOT_SENSITIVE
+ */
+ @FlaggedApi(FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API)
+ public final boolean isContentSensitive() {
+ if (getContentSensitivity() == CONTENT_SENSITIVITY_SENSITIVE) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
* Gets the mode for determining whether this view is important for content capture.
*
* <p>See {@link #setImportantForContentCapture(int)} and
@@ -22175,6 +22299,9 @@
* Retrieve a unique token identifying the window this view is attached to.
* @return Return the window's token for use in
* {@link WindowManager.LayoutParams#token WindowManager.LayoutParams.token}.
+ * This token maybe null if this view is not attached to a window.
+ * @see #isAttachedToWindow() for current window attach state
+ * @see OnAttachStateChangeListener to listen to window attach/detach state changes
*/
public IBinder getWindowToken() {
return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
@@ -28402,9 +28529,29 @@
Log.w(VIEW_LOG_TAG, "startDragAndDrop called with an invalid surface.");
return false;
}
+ if ((flags & DRAG_FLAG_GLOBAL) != 0 && ((flags & DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0)) {
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called with both DRAG_FLAG_GLOBAL "
+ + "and DRAG_FLAG_GLOBAL_SAME_APPLICATION, the drag will default to "
+ + "DRAG_FLAG_GLOBAL_SAME_APPLICATION");
+ flags &= ~DRAG_FLAG_GLOBAL;
+ }
if (data != null) {
- data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
+ if (com.android.window.flags.Flags.delegateUnhandledDrags()) {
+ data.prepareToLeaveProcess(
+ (flags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) != 0);
+ if ((flags & DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG) != 0) {
+ if (!data.hasActivityPendingIntents()) {
+ // Reset the flag if there is no launchable activity intent
+ flags &= ~DRAG_FLAG_START_PENDING_INTENT_ON_UNHANDLED_DRAG;
+ Log.w(VIEW_LOG_TAG, "startDragAndDrop called with "
+ + "DRAG_FLAG_START_INTENT_ON_UNHANDLED_DRAG but the clip data "
+ + "contains non-activity PendingIntents");
+ }
+ }
+ } else {
+ data.prepareToLeaveProcess((flags & DRAG_FLAG_GLOBAL) != 0);
+ }
}
Rect bounds = new Rect();
@@ -28430,6 +28577,7 @@
if (token != null) {
root.setLocalDragState(myLocalState);
mAttachInfo.mDragToken = token;
+ mAttachInfo.mDragData = data;
mAttachInfo.mViewRootImpl.setDragStartedViewForAccessibility(this);
setAccessibilityDragStarted(true);
}
@@ -28507,8 +28655,12 @@
if (mAttachInfo.mDragSurface != null) {
mAttachInfo.mDragSurface.release();
}
+ if (mAttachInfo.mDragData != null) {
+ mAttachInfo.mDragData.cleanUpPendingIntents();
+ }
mAttachInfo.mDragSurface = surface;
mAttachInfo.mDragToken = token;
+ mAttachInfo.mDragData = data;
// Cache the local state object for delivery with DragEvents
root.setLocalDragState(myLocalState);
if (a11yEnabled) {
@@ -31422,11 +31574,15 @@
IBinder mDragToken;
/**
+ * Used to track the data of the current drag operation for cleanup later.
+ */
+ ClipData mDragData;
+
+ /**
* The drag shadow surface for the current drag operation.
*/
public Surface mDragSurface;
-
/**
* The view that currently has a tooltip displayed.
*/
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 07c9795..28a7334 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8599,6 +8599,10 @@
mAttachInfo.mDragSurface.release();
mAttachInfo.mDragSurface = null;
}
+ if (mAttachInfo.mDragData != null) {
+ mAttachInfo.mDragData.cleanUpPendingIntents();
+ mAttachInfo.mDragData = null;
+ }
}
}
}
diff --git a/core/java/android/view/WindowInsets.java b/core/java/android/view/WindowInsets.java
index 921afaa..fbebe1e 100644
--- a/core/java/android/view/WindowInsets.java
+++ b/core/java/android/view/WindowInsets.java
@@ -34,6 +34,7 @@
import static android.view.WindowInsets.Type.indexOf;
import static android.view.WindowInsets.Type.systemBars;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -44,8 +45,10 @@
import android.content.Intent;
import android.graphics.Insets;
import android.graphics.Rect;
+import android.util.Size;
import android.view.View.OnApplyWindowInsetsListener;
import android.view.WindowInsets.Type.InsetsType;
+import android.view.flags.Flags;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethod;
@@ -54,7 +57,10 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
import java.util.Objects;
/**
@@ -78,6 +84,8 @@
private final Insets[] mTypeInsetsMap;
private final Insets[] mTypeMaxInsetsMap;
private final boolean[] mTypeVisibilityMap;
+ private final Rect[][] mTypeBoundingRectsMap;
+ private final Rect[][] mTypeMaxBoundingRectsMap;
@Nullable private Rect mTempRect;
private final boolean mIsRound;
@@ -85,6 +93,8 @@
@Nullable private final RoundedCorners mRoundedCorners;
@Nullable private final PrivacyIndicatorBounds mPrivacyIndicatorBounds;
@Nullable private final DisplayShape mDisplayShape;
+ private final int mFrameWidth;
+ private final int mFrameHeight;
private final @InsetsType int mForceConsumingTypes;
private final @InsetsType int mSuppressScrimTypes;
@@ -114,7 +124,7 @@
static {
CONSUMED = new WindowInsets(createCompatTypeMap(null), createCompatTypeMap(null),
createCompatVisibilityMap(createCompatTypeMap(null)), false, 0, 0, null,
- null, null, null, systemBars(), false);
+ null, null, null, systemBars(), false, null, null, 0, 0);
}
/**
@@ -139,7 +149,10 @@
RoundedCorners roundedCorners,
PrivacyIndicatorBounds privacyIndicatorBounds,
DisplayShape displayShape,
- @InsetsType int compatInsetsTypes, boolean compatIgnoreVisibility) {
+ @InsetsType int compatInsetsTypes, boolean compatIgnoreVisibility,
+ Rect[][] typeBoundingRectsMap,
+ Rect[][] typeMaxBoundingRectsMap,
+ int frameWidth, int frameHeight) {
mSystemWindowInsetsConsumed = typeInsetsMap == null;
mTypeInsetsMap = mSystemWindowInsetsConsumed
? new Insets[SIZE]
@@ -164,6 +177,14 @@
mRoundedCorners = roundedCorners;
mPrivacyIndicatorBounds = privacyIndicatorBounds;
mDisplayShape = displayShape;
+ mTypeBoundingRectsMap = (mSystemWindowInsetsConsumed || typeBoundingRectsMap == null)
+ ? new Rect[SIZE][]
+ : typeBoundingRectsMap.clone();
+ mTypeMaxBoundingRectsMap = (mStableInsetsConsumed || typeMaxBoundingRectsMap == null)
+ ? new Rect[SIZE][]
+ : typeMaxBoundingRectsMap.clone();
+ mFrameWidth = frameWidth;
+ mFrameHeight = frameHeight;
}
/**
@@ -181,7 +202,11 @@
src.mPrivacyIndicatorBounds,
src.mDisplayShape,
src.mCompatInsetsTypes,
- src.mCompatIgnoreVisibility);
+ src.mCompatIgnoreVisibility,
+ src.mSystemWindowInsetsConsumed ? null : src.mTypeBoundingRectsMap,
+ src.mStableInsetsConsumed ? null : src.mTypeMaxBoundingRectsMap,
+ src.mFrameWidth,
+ src.mFrameHeight);
}
private static DisplayCutout displayCutoutCopyConstructorArgument(WindowInsets w) {
@@ -233,7 +258,8 @@
@UnsupportedAppUsage
public WindowInsets(Rect systemWindowInsets) {
this(createCompatTypeMap(systemWindowInsets), null, new boolean[SIZE], false, 0, 0,
- null, null, null, null, systemBars(), false /* compatIgnoreVisibility */);
+ null, null, null, null, systemBars(), false /* compatIgnoreVisibility */,
+ new Rect[SIZE][], null, 0, 0);
}
/**
@@ -475,6 +501,111 @@
}
/**
+ * Returns a list of {@link Rect}s, each of which is the bounding rectangle for an area
+ * that is being partially or fully obscured inside the window.
+ *
+ * <p>
+ * May be used with or instead of {@link Insets} for finer avoidance of regions that may be
+ * partially obscuring the window but may be smaller than those provided by
+ * {@link #getInsets(int)}.
+ * </p>
+ *
+ * <p>
+ * The {@link Rect}s returned are always cropped to the bounds of the window frame and their
+ * coordinate values are relative to the {@link #getFrame()}, regardless of the window's
+ * position on screen.
+ * </p>
+ *
+ * <p>
+ * If inset by {@link #inset(Insets)}, bounding rects that intersect with the provided insets
+ * will be resized to only include the intersection with the remaining frame. Bounding rects
+ * may be completely removed if they no longer intersect with the new instance.
+ * </p>
+ *
+ * @param typeMask the insets type for which to obtain the bounding rectangles
+ * @return the bounding rectangles
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public List<Rect> getBoundingRects(@InsetsType int typeMask) {
+ Rect[] allRects = null;
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ final Rect[] rects = mTypeBoundingRectsMap[indexOf(i)];
+ if (rects == null) {
+ continue;
+ }
+ if (allRects == null) {
+ allRects = rects;
+ } else {
+ final Rect[] concat = new Rect[allRects.length + rects.length];
+ System.arraycopy(allRects, 0, concat, 0, allRects.length);
+ System.arraycopy(rects, 0, concat, allRects.length, rects.length);
+ allRects = concat;
+ }
+ }
+ if (allRects == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.asList(allRects);
+ }
+
+ /**
+ * Returns a list of {@link Rect}s, each of which is the bounding rectangle for an area that
+ * can be partially or fully obscured inside the window, regardless of whether
+ * that type is currently visible or not.
+ *
+ * <p> The bounding rects represent areas of a window that <b>may</b> be partially or fully
+ * obscured by the {@code type}. This value does not change based on the visibility state of
+ * those elements. For example, if the status bar is normally shown, but temporarily hidden,
+ * the bounding rects returned here will provide the rects associated with the status bar being
+ * shown.</p>
+ *
+ * <p>
+ * May be used with or instead of {@link Insets} for finer avoidance of regions that may be
+ * partially obscuring the window but may be smaller than those provided by
+ * {@link #getInsetsIgnoringVisibility(int)}.
+ * </p>
+ *
+ * <p>
+ * The {@link Rect}s returned are always cropped to the bounds of the window frame and their
+ * coordinate values are relative to the {@link #getFrame()}, regardless of the window's
+ * position on screen.
+ * </p>
+ *
+ * @param typeMask the insets type for which to obtain the bounding rectangles
+ * @return the bounding rectangles
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public List<Rect> getBoundingRectsIgnoringVisibility(@InsetsType int typeMask) {
+ Rect[] allRects = null;
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ final Rect[] rects = mTypeMaxBoundingRectsMap[indexOf(i)];
+ if (rects == null) {
+ continue;
+ }
+ if (allRects == null) {
+ allRects = rects;
+ } else {
+ final Rect[] concat = new Rect[allRects.length + rects.length];
+ System.arraycopy(allRects, 0, concat, 0, allRects.length);
+ System.arraycopy(rects, 0, concat, allRects.length, rects.length);
+ allRects = concat;
+ }
+ }
+ if (allRects == null) {
+ return Collections.emptyList();
+ }
+ return Arrays.asList(allRects);
+ }
+
+ /**
* Returns the display cutout if there is one.
*
* <p>Note: the display cutout will already be {@link #consumeDisplayCutout consumed} during
@@ -555,7 +686,10 @@
mTypeVisibilityMap,
mIsRound, mForceConsumingTypes, mSuppressScrimTypes,
null /* displayCutout */, mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape,
- mCompatInsetsTypes, mCompatIgnoreVisibility);
+ mCompatInsetsTypes, mCompatIgnoreVisibility,
+ mSystemWindowInsetsConsumed ? null : mTypeBoundingRectsMap,
+ mStableInsetsConsumed ? null : mTypeMaxBoundingRectsMap,
+ mFrameWidth, mFrameHeight);
}
@@ -610,7 +744,7 @@
(mCompatInsetsTypes & displayCutout()) != 0
? null : displayCutoutCopyConstructorArgument(this),
mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, mCompatInsetsTypes,
- mCompatIgnoreVisibility);
+ mCompatIgnoreVisibility, null, null, mFrameWidth, mFrameHeight);
}
// TODO(b/119190588): replace @code with @link below
@@ -914,6 +1048,10 @@
result.append(Type.toString(1 << i)).append("=").append(insets)
.append(" max=").append(maxInsets)
.append(" vis=").append(visible)
+ .append(" boundingRects=")
+ .append(Arrays.toString(mTypeBoundingRectsMap[i]))
+ .append(" maxBoundingRects=")
+ .append(Arrays.toString(mTypeMaxBoundingRectsMap[i]))
.append("\n ");
}
}
@@ -942,6 +1080,10 @@
result.append("displayCutoutConsumed=" + mDisplayCutoutConsumed);
result.append("\n ");
result.append(isRound() ? "round" : "");
+ result.append("\n ");
+ result.append("frameWidth=" + mFrameWidth);
+ result.append("\n ");
+ result.append("frameHeight=" + mFrameHeight);
result.append("}");
return result.toString();
}
@@ -1013,6 +1155,27 @@
}
/**
+ * Returns the assumed size of the window, relative to which the {@link #getInsets} and
+ * {@link #getBoundingRects} have been calculated.
+ *
+ * <p> May be used with {@link #getBoundingRects} to better understand their position within
+ * the window, such as the area between the edge of a bounding rect and the edge of the window.
+ *
+ * <p>Note: the size may not match the actual size of the window, which is determined during
+ * the layout pass - as {@link WindowInsets} are dispatched before layout.
+ *
+ * <p>Caution: using this value in determining the actual window size may make the result of
+ * layout passes unstable and should be avoided.
+ *
+ * @return the assumed size of the window during the inset calculation
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Size getFrame() {
+ return new Size(mFrameWidth, mFrameHeight);
+ }
+
+ /**
* @see #inset(int, int, int, int)
* @hide
*/
@@ -1039,7 +1202,17 @@
? null
: mPrivacyIndicatorBounds.inset(left, top, right, bottom),
mDisplayShape,
- mCompatInsetsTypes, mCompatIgnoreVisibility);
+ mCompatInsetsTypes, mCompatIgnoreVisibility,
+ mSystemWindowInsetsConsumed
+ ? null
+ : insetBoundingRects(mTypeBoundingRectsMap, left, top, right, bottom,
+ mFrameWidth, mFrameHeight),
+ mStableInsetsConsumed
+ ? null
+ : insetBoundingRects(mTypeMaxBoundingRectsMap, left, top, right, bottom,
+ mFrameWidth, mFrameHeight),
+ Math.max(0, mFrameWidth - left - right),
+ Math.max(0, mFrameHeight - top - bottom));
}
@Override
@@ -1060,7 +1233,11 @@
&& Objects.equals(mDisplayCutout, that.mDisplayCutout)
&& Objects.equals(mRoundedCorners, that.mRoundedCorners)
&& Objects.equals(mPrivacyIndicatorBounds, that.mPrivacyIndicatorBounds)
- && Objects.equals(mDisplayShape, that.mDisplayShape);
+ && Objects.equals(mDisplayShape, that.mDisplayShape)
+ && Arrays.deepEquals(mTypeBoundingRectsMap, that.mTypeBoundingRectsMap)
+ && Arrays.deepEquals(mTypeMaxBoundingRectsMap, that.mTypeMaxBoundingRectsMap)
+ && mFrameWidth == that.mFrameWidth
+ && mFrameHeight == that.mFrameHeight;
}
@Override
@@ -1069,7 +1246,8 @@
Arrays.hashCode(mTypeVisibilityMap), mIsRound, mDisplayCutout, mRoundedCorners,
mForceConsumingTypes, mSuppressScrimTypes, mSystemWindowInsetsConsumed,
mStableInsetsConsumed, mDisplayCutoutConsumed, mPrivacyIndicatorBounds,
- mDisplayShape);
+ mDisplayShape, Arrays.deepHashCode(mTypeBoundingRectsMap),
+ Arrays.deepHashCode(mTypeMaxBoundingRectsMap), mFrameWidth, mFrameHeight);
}
@@ -1110,6 +1288,68 @@
return Insets.of(newLeft, newTop, newRight, newBottom);
}
+ static Rect[][] insetBoundingRects(Rect[][] typeBoundingRectsMap,
+ int insetLeft, int insetTop, int insetRight, int insetBottom, int frameWidth,
+ int frameHeight) {
+ if (insetLeft == 0 && insetTop == 0 && insetRight == 0 && insetBottom == 0) {
+ return typeBoundingRectsMap;
+ }
+ boolean cloned = false;
+ for (int i = 0; i < SIZE; i++) {
+ final Rect[] boundingRects = typeBoundingRectsMap[i];
+ if (boundingRects == null) {
+ continue;
+ }
+ final Rect[] insetBoundingRects = insetBoundingRects(boundingRects,
+ insetLeft, insetTop, insetRight, insetBottom, frameWidth, frameHeight);
+ if (!Arrays.equals(insetBoundingRects, boundingRects)) {
+ if (!cloned) {
+ typeBoundingRectsMap = typeBoundingRectsMap.clone();
+ cloned = true;
+ }
+ typeBoundingRectsMap[i] = insetBoundingRects;
+ }
+ }
+ return typeBoundingRectsMap;
+ }
+
+ static Rect[] insetBoundingRects(Rect[] boundingRects,
+ int left, int top, int right, int bottom, int frameWidth, int frameHeight) {
+ final List<Rect> insetBoundingRectsList = new ArrayList<>();
+ for (int i = 0; i < boundingRects.length; i++) {
+ final Rect insetRect = insetRect(boundingRects[i], left, top, right, bottom,
+ frameWidth, frameHeight);
+ if (insetRect != null) {
+ insetBoundingRectsList.add(insetRect);
+ }
+ }
+ return insetBoundingRectsList.toArray(new Rect[0]);
+ }
+
+ private static Rect insetRect(Rect orig, int insetLeft, int insetTop, int insetRight,
+ int insetBottom, int frameWidth, int frameHeight) {
+ if (orig == null) {
+ return null;
+ }
+
+ // Calculate the inset frame, and leave it in that coordinate space for easier comparison
+ // against the |orig| rect.
+ final Rect insetFrame = new Rect(insetLeft, insetTop, frameWidth - insetRight,
+ frameHeight - insetBottom);
+ // Then the intersecting portion of |orig| with the inset |insetFrame|.
+ final Rect insetRect = new Rect();
+ if (insetRect.setIntersect(insetFrame, orig)) {
+ // The intersection is the inset rect, but its position must be shifted to be relative
+ // to the frame. Since the new frame will start at left=|insetLeft| and top=|insetTop|,
+ // just offset that much back in the direction of the origin of the frame.
+ insetRect.offset(-insetLeft, -insetTop);
+ return insetRect;
+ } else {
+ // The |orig| rect does not intersect with the new frame at all, so don't report it.
+ return null;
+ }
+ }
+
/**
* @return whether system window insets have been consumed.
*/
@@ -1125,6 +1365,8 @@
private final Insets[] mTypeInsetsMap;
private final Insets[] mTypeMaxInsetsMap;
private final boolean[] mTypeVisibilityMap;
+ private final Rect[][] mTypeBoundingRectsMap;
+ private final Rect[][] mTypeMaxBoundingRectsMap;
private boolean mSystemInsetsConsumed = true;
private boolean mStableInsetsConsumed = true;
@@ -1137,6 +1379,8 @@
private @InsetsType int mSuppressScrimTypes;
private PrivacyIndicatorBounds mPrivacyIndicatorBounds = new PrivacyIndicatorBounds();
+ private int mFrameWidth;
+ private int mFrameHeight;
/**
* Creates a builder where all insets are initially consumed.
@@ -1145,6 +1389,8 @@
mTypeInsetsMap = new Insets[SIZE];
mTypeMaxInsetsMap = new Insets[SIZE];
mTypeVisibilityMap = new boolean[SIZE];
+ mTypeBoundingRectsMap = new Rect[SIZE][];
+ mTypeMaxBoundingRectsMap = new Rect[SIZE][];
}
/**
@@ -1165,6 +1411,10 @@
mSuppressScrimTypes = insets.mSuppressScrimTypes;
mPrivacyIndicatorBounds = insets.mPrivacyIndicatorBounds;
mDisplayShape = insets.mDisplayShape;
+ mTypeBoundingRectsMap = insets.mTypeBoundingRectsMap.clone();
+ mTypeMaxBoundingRectsMap = insets.mTypeMaxBoundingRectsMap.clone();
+ mFrameWidth = insets.mFrameWidth;
+ mFrameHeight = insets.mFrameHeight;
}
/**
@@ -1452,6 +1702,68 @@
}
/**
+ * Sets the bounding rects.
+ *
+ * @param typeMask the inset types to which these rects apply.
+ * @param rects the bounding rects.
+ * @return itself.
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Builder setBoundingRects(@InsetsType int typeMask, @NonNull List<Rect> rects) {
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ mTypeBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]);
+ }
+ return this;
+ }
+
+ /**
+ * Sets the bounding rects while ignoring their visibility state.
+ *
+ * @param typeMask the inset types to which these rects apply.
+ * @param rects the bounding rects.
+ * @return itself.
+ *
+ * @throws IllegalArgumentException If {@code typeMask} contains {@link Type#ime()}.
+ * Maximum bounding rects are not available for this type as the height of the IME is
+ * dynamic depending on the {@link EditorInfo} of the currently focused view, as well as
+ * the UI state of the IME.
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Builder setBoundingRectsIgnoringVisibility(@InsetsType int typeMask,
+ @NonNull List<Rect> rects) {
+ if (typeMask == IME) {
+ throw new IllegalArgumentException("Maximum bounding rects not available for IME");
+ }
+ for (int i = FIRST; i <= LAST; i = i << 1) {
+ if ((typeMask & i) == 0) {
+ continue;
+ }
+ mTypeMaxBoundingRectsMap[indexOf(i)] = rects.toArray(new Rect[0]);
+ }
+ return this;
+ }
+
+ /**
+ * Set the frame size.
+ *
+ * @param width the width of the frame.
+ * @param height the height of the frame.
+ * @return itself.
+ */
+ @FlaggedApi(Flags.FLAG_CUSTOMIZABLE_WINDOW_HEADERS)
+ @NonNull
+ public Builder setFrame(int width, int height) {
+ mFrameWidth = width;
+ mFrameHeight = height;
+ return this;
+ }
+
+ /**
* Builds a {@link WindowInsets} instance.
*
* @return the {@link WindowInsets} instance.
@@ -1462,7 +1774,10 @@
mStableInsetsConsumed ? null : mTypeMaxInsetsMap, mTypeVisibilityMap,
mIsRound, mForceConsumingTypes, mSuppressScrimTypes, mDisplayCutout,
mRoundedCorners, mPrivacyIndicatorBounds, mDisplayShape, systemBars(),
- false /* compatIgnoreVisibility */);
+ false /* compatIgnoreVisibility */,
+ mSystemInsetsConsumed ? null : mTypeBoundingRectsMap,
+ mStableInsetsConsumed ? null : mTypeMaxBoundingRectsMap,
+ mFrameWidth, mFrameHeight);
}
}
diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java
index ae00b70..2fb5213 100644
--- a/core/java/android/view/WindowManagerImpl.java
+++ b/core/java/android/view/WindowManagerImpl.java
@@ -520,11 +520,16 @@
public void registerTrustedPresentationListener(@NonNull IBinder window,
@NonNull TrustedPresentationThresholds thresholds, @NonNull Executor executor,
@NonNull Consumer<Boolean> listener) {
+ Objects.requireNonNull(window, "window must not be null");
+ Objects.requireNonNull(thresholds, "thresholds must not be null");
+ Objects.requireNonNull(executor, "executor must not be null");
+ Objects.requireNonNull(listener, "listener must not be null");
mGlobal.registerTrustedPresentationListener(window, thresholds, executor, listener);
}
@Override
public void unregisterTrustedPresentationListener(@NonNull Consumer<Boolean> listener) {
+ Objects.requireNonNull(listener, "listener must not be null");
mGlobal.unregisterTrustedPresentationListener(listener);
}
diff --git a/core/java/android/view/inputmethod/InputMethodInfo.java b/core/java/android/view/inputmethod/InputMethodInfo.java
index d38a95e..b60efc1 100644
--- a/core/java/android/view/inputmethod/InputMethodInfo.java
+++ b/core/java/android/view/inputmethod/InputMethodInfo.java
@@ -81,12 +81,21 @@
* {@link Intent#getAction() Intent action} for IME that
* {@link #supportsStylusHandwriting() supports stylus handwriting}.
*
- * @see #createStylusHandwritingSettingsActivityIntent().
+ * @see #createStylusHandwritingSettingsActivityIntent()
*/
public static final String ACTION_STYLUS_HANDWRITING_SETTINGS =
"android.view.inputmethod.action.STYLUS_HANDWRITING_SETTINGS";
/**
+ * {@link Intent#getAction() Intent action} for the IME language settings.
+ *
+ * @see #createImeLanguageSettingsActivityIntent()
+ */
+ @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP)
+ public static final String ACTION_IME_LANGUAGE_SETTINGS =
+ "android.view.inputmethod.action.IME_LANGUAGE_SETTINGS";
+
+ /**
* Maximal length of a component name
* @hide
*/
@@ -132,6 +141,13 @@
final String mSettingsActivityName;
/**
+ * The input method language settings activity's name, used to
+ * launch the language settings activity of this input method.
+ */
+ @Nullable
+ private final String mLanguageSettingsActivityName;
+
+ /**
* The resource in the input method's .apk that holds a boolean indicating
* whether it should be considered the default input method for this
* system. This is a resource ID instead of the final value so that it
@@ -244,6 +260,7 @@
PackageManager pm = context.getPackageManager();
String settingsActivityComponent = null;
+ String languageSettingsActivityComponent = null;
String stylusHandwritingSettingsActivity = null;
boolean isVrOnly;
boolean isVirtualDeviceOnly;
@@ -277,9 +294,17 @@
com.android.internal.R.styleable.InputMethod);
settingsActivityComponent = sa.getString(
com.android.internal.R.styleable.InputMethod_settingsActivity);
- if ((si.name != null && si.name.length() > COMPONENT_NAME_MAX_LENGTH) || (
- settingsActivityComponent != null
- && settingsActivityComponent.length() > COMPONENT_NAME_MAX_LENGTH)) {
+ if (Flags.imeSwitcherRevamp()) {
+ languageSettingsActivityComponent = sa.getString(
+ com.android.internal.R.styleable.InputMethod_languageSettingsActivity);
+ }
+ if ((si.name != null && si.name.length() > COMPONENT_NAME_MAX_LENGTH)
+ || (settingsActivityComponent != null
+ && settingsActivityComponent.length()
+ > COMPONENT_NAME_MAX_LENGTH)
+ || (languageSettingsActivityComponent != null
+ && languageSettingsActivityComponent.length()
+ > COMPONENT_NAME_MAX_LENGTH)) {
throw new XmlPullParserException(
"Activity name exceeds maximum of 1000 characters");
}
@@ -382,6 +407,7 @@
}
mSubtypes = new InputMethodSubtypeArray(subtypes);
mSettingsActivityName = settingsActivityComponent;
+ mLanguageSettingsActivityName = languageSettingsActivityComponent;
mStylusHandwritingSettingsActivityAttr = stylusHandwritingSettingsActivity;
mIsDefaultResId = isDefaultResId;
mIsAuxIme = isAuxIme;
@@ -401,6 +427,7 @@
public InputMethodInfo(InputMethodInfo source) {
mId = source.mId;
mSettingsActivityName = source.mSettingsActivityName;
+ mLanguageSettingsActivityName = source.mLanguageSettingsActivityName;
mIsDefaultResId = source.mIsDefaultResId;
mIsAuxIme = source.mIsAuxIme;
mSupportsSwitchingToNextInputMethod = source.mSupportsSwitchingToNextInputMethod;
@@ -422,6 +449,7 @@
InputMethodInfo(Parcel source) {
mId = source.readString();
mSettingsActivityName = source.readString();
+ mLanguageSettingsActivityName = source.readString8();
mIsDefaultResId = source.readInt();
mIsAuxIme = source.readInt() == 1;
mSupportsSwitchingToNextInputMethod = source.readInt() == 1;
@@ -445,8 +473,9 @@
public InputMethodInfo(String packageName, String className,
CharSequence label, String settingsActivity) {
this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */,
- settingsActivity, null /* subtypes */, 0 /* isDefaultResId */,
- false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */,
+ settingsActivity, null /* languageSettingsActivity */, null /* subtypes */,
+ 0 /* isDefaultResId */, false /* forceDefault */,
+ true /* supportsSwitchingToNextInputMethod */,
false /* inlineSuggestionsEnabled */, false /* isVrOnly */,
false /* isVirtualDeviceOnly */, 0 /* handledConfigChanges */,
false /* supportsStylusHandwriting */,
@@ -461,11 +490,12 @@
@TestApi
public InputMethodInfo(@NonNull String packageName, @NonNull String className,
@NonNull CharSequence label, @NonNull String settingsActivity,
- boolean supportStylusHandwriting,
+ @NonNull String languageSettingsActivity, boolean supportStylusHandwriting,
@NonNull String stylusHandwritingSettingsActivityAttr) {
this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */,
- settingsActivity, null /* subtypes */, 0 /* isDefaultResId */,
- false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */,
+ settingsActivity, languageSettingsActivity, null /* subtypes */,
+ 0 /* isDefaultResId */, false /* forceDefault */,
+ true /* supportsSwitchingToNextInputMethod */,
false /* inlineSuggestionsEnabled */, false /* isVrOnly */,
false /* isVirtualDeviceOnly */, 0 /* handledConfigChanges */,
supportStylusHandwriting, stylusHandwritingSettingsActivityAttr,
@@ -481,8 +511,9 @@
@NonNull CharSequence label, @NonNull String settingsActivity,
int handledConfigChanges) {
this(buildFakeResolveInfo(packageName, className, label), false /* isAuxIme */,
- settingsActivity, null /* subtypes */, 0 /* isDefaultResId */,
- false /* forceDefault */, true /* supportsSwitchingToNextInputMethod */,
+ settingsActivity, null /* languageSettingsActivity */, null /* subtypes */,
+ 0 /* isDefaultResId */, false /* forceDefault */,
+ true /* supportsSwitchingToNextInputMethod */,
false /* inlineSuggestionsEnabled */, false /* isVrOnly */,
false /* isVirtualDeviceOnly */, handledConfigChanges,
false /* supportsStylusHandwriting */,
@@ -497,7 +528,8 @@
public InputMethodInfo(ResolveInfo ri, boolean isAuxIme,
String settingsActivity, List<InputMethodSubtype> subtypes, int isDefaultResId,
boolean forceDefault) {
- this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault,
+ this(ri, isAuxIme, settingsActivity, null /* languageSettingsActivity */, subtypes,
+ isDefaultResId, forceDefault,
true /* supportsSwitchingToNextInputMethod */, false /* inlineSuggestionsEnabled */,
false /* isVrOnly */, false /* isVirtualDeviceOnly */, 0 /* handledconfigChanges */,
false /* supportsStylusHandwriting */,
@@ -512,7 +544,8 @@
public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity,
List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault,
boolean supportsSwitchingToNextInputMethod, boolean isVrOnly) {
- this(ri, isAuxIme, settingsActivity, subtypes, isDefaultResId, forceDefault,
+ this(ri, isAuxIme, settingsActivity, null /* languageSettingsActivity */, subtypes,
+ isDefaultResId, forceDefault,
supportsSwitchingToNextInputMethod, false /* inlineSuggestionsEnabled */, isVrOnly,
false /* isVirtualDeviceOnly */,
0 /* handledConfigChanges */, false /* supportsStylusHandwriting */,
@@ -525,7 +558,8 @@
* @hide
*/
public InputMethodInfo(ResolveInfo ri, boolean isAuxIme, String settingsActivity,
- List<InputMethodSubtype> subtypes, int isDefaultResId, boolean forceDefault,
+ @Nullable String languageSettingsActivity, List<InputMethodSubtype> subtypes,
+ int isDefaultResId, boolean forceDefault,
boolean supportsSwitchingToNextInputMethod, boolean inlineSuggestionsEnabled,
boolean isVrOnly, boolean isVirtualDeviceOnly, int handledConfigChanges,
boolean supportsStylusHandwriting, String stylusHandwritingSettingsActivityAttr,
@@ -534,6 +568,7 @@
mService = ri;
mId = new ComponentName(si.packageName, si.name).flattenToShortString();
mSettingsActivityName = settingsActivity;
+ mLanguageSettingsActivityName = languageSettingsActivity;
mIsDefaultResId = isDefaultResId;
mIsAuxIme = isAuxIme;
mSubtypes = new InputMethodSubtypeArray(subtypes);
@@ -756,9 +791,34 @@
mStylusHandwritingSettingsActivityAttr));
}
+ /**
+ * Returns {@link Intent} for IME language settings activity with
+ * {@link Intent#getAction() Intent action} {@link #ACTION_IME_LANGUAGE_SETTINGS},
+ * else <code>null</code> if
+ * {@link android.R.styleable#InputMethod_languageSettingsActivity} is not defined.
+ *
+ * <p>To launch IME language settings, use this method to get the {@link Intent} to launch
+ * the IME language settings activity.</p>
+ * <p>e.g.<pre><code>startActivity(createImeLanguageSettingsActivityIntent());</code></pre></p>
+ *
+ * @attr ref R.styleable#InputMethod_languageSettingsActivity
+ */
+ @FlaggedApi(android.view.inputmethod.Flags.FLAG_IME_SWITCHER_REVAMP)
+ @Nullable
+ public Intent createImeLanguageSettingsActivityIntent() {
+ if (TextUtils.isEmpty(mLanguageSettingsActivityName)) {
+ return null;
+ }
+ return new Intent(ACTION_IME_LANGUAGE_SETTINGS).setComponent(
+ new ComponentName(getServiceInfo().packageName,
+ mLanguageSettingsActivityName)
+ );
+ }
+
public void dump(Printer pw, String prefix) {
pw.println(prefix + "mId=" + mId
+ " mSettingsActivityName=" + mSettingsActivityName
+ + " mLanguageSettingsActivityName=" + mLanguageSettingsActivityName
+ " mIsVrOnly=" + mIsVrOnly
+ " mIsVirtualDeviceOnly=" + mIsVirtualDeviceOnly
+ " mSupportsSwitchingToNextInputMethod=" + mSupportsSwitchingToNextInputMethod
@@ -779,8 +839,9 @@
@Override
public String toString() {
return "InputMethodInfo{" + mId
- + ", settings: "
- + mSettingsActivityName + "}";
+ + ", settings: " + mSettingsActivityName
+ + ", languageSettings: " + mLanguageSettingsActivityName
+ + "}";
}
/**
@@ -872,6 +933,7 @@
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mId);
dest.writeString(mSettingsActivityName);
+ dest.writeString8(mLanguageSettingsActivityName);
dest.writeInt(mIsDefaultResId);
dest.writeInt(mIsAuxIme ? 1 : 0);
dest.writeInt(mSupportsSwitchingToNextInputMethod ? 1 : 0);
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 3d70c5b..3b07f27 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -18,6 +18,7 @@
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR;
+import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_IMMEDIATE;
import static android.view.inputmethod.InputConnection.CURSOR_UPDATE_MONITOR;
import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.DISPLAY_ID;
@@ -1950,6 +1951,10 @@
if (mServedView != null) {
clearedView = mServedView;
mServedView = null;
+ if (initiationWithoutInputConnection() && clearedView.getViewRootImpl() != null) {
+ clearedView.getViewRootImpl().getHandwritingInitiator()
+ .clearFocusedView(clearedView);
+ }
}
if (clearedView != null) {
if (DEBUG) {
@@ -2932,6 +2937,10 @@
switch (res.result) {
case InputBindResult.ResultCode.ERROR_NOT_IME_TARGET_WINDOW:
mRestartOnNextWindowFocus = true;
+ if (initiationWithoutInputConnection()) {
+ mServedView.getViewRootImpl().getHandwritingInitiator().clearFocusedView(
+ mServedView);
+ }
mServedView = null;
break;
}
@@ -3094,6 +3103,11 @@
return false;
}
mServedView = mNextServedView;
+ if (initiationWithoutInputConnection() && mServedView.onCheckIsTextEditor()
+ && mServedView.isHandwritingDelegate()) {
+ mServedView.getViewRootImpl().getHandwritingInitiator().onDelegateViewFocused(
+ mServedView);
+ }
if (mServedInputConnection != null) {
mServedInputConnection.finishComposingTextFromImm();
}
diff --git a/core/java/android/view/inputmethod/flags.aconfig b/core/java/android/view/inputmethod/flags.aconfig
index ccc5dbb..55986e7 100644
--- a/core/java/android/view/inputmethod/flags.aconfig
+++ b/core/java/android/view/inputmethod/flags.aconfig
@@ -54,3 +54,27 @@
bug: "293640003"
is_fixed_read_only: true
}
+
+flag {
+ name: "ime_switcher_revamp"
+ namespace: "input_method"
+ description: "Feature flag for revamping the Input Method Switcher menu"
+ bug: "311791923"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "initiation_without_input_connection"
+ namespace: "input_method"
+ description: "Feature flag for initiating handwriting without InputConnection"
+ bug: "308827131"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "connectionless_handwriting"
+ namespace: "input_method"
+ description: "Feature flag for connectionless stylus handwriting APIs"
+ bug: "300979854"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
index aa2474d7..3e0161a 100644
--- a/core/java/android/widget/EditText.java
+++ b/core/java/android/widget/EditText.java
@@ -16,9 +16,13 @@
package android.widget;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.os.Build;
import android.text.Editable;
import android.text.Selection;
import android.text.Spannable;
@@ -29,6 +33,8 @@
import android.util.AttributeSet;
import android.view.KeyEvent;
+import com.android.internal.R;
+
/*
* This is supposed to be a *very* thin veneer over TextView.
* Do not make any changes here that do anything that a TextView
@@ -85,6 +91,11 @@
private static final int ID_ITALIC = android.R.id.italic;
private static final int ID_UNDERLINE = android.R.id.underline;
+ /** @hide */
+ @ChangeId
+ @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ public static final long LINE_HEIGHT_FOR_LOCALE = 303326708L;
+
public EditText(Context context) {
this(context, null);
}
@@ -104,15 +115,39 @@
final TypedArray a = theme.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes);
- final int n = a.getIndexCount();
- for (int i = 0; i < n; ++i) {
- int attr = a.getIndex(i);
- switch (attr) {
- case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
- mStyleShortcutsEnabled = a.getBoolean(attr, false);
- break;
+ try {
+ final int n = a.getIndexCount();
+ for (int i = 0; i < n; ++i) {
+ int attr = a.getIndex(i);
+ switch (attr) {
+ case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
+ mStyleShortcutsEnabled = a.getBoolean(attr, false);
+ break;
+ }
}
+ } finally {
+ a.recycle();
}
+
+ boolean hasUseLocalePreferredLineHeightForMinimumInt = false;
+ boolean useLocalePreferredLineHeightForMinimumInt = false;
+ TypedArray tvArray = theme.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+ try {
+ hasUseLocalePreferredLineHeightForMinimumInt =
+ tvArray.hasValue(R.styleable.TextView_useLocalePreferredLineHeightForMinimum);
+ if (hasUseLocalePreferredLineHeightForMinimumInt) {
+ useLocalePreferredLineHeightForMinimumInt = tvArray.getBoolean(
+ R.styleable.TextView_useLocalePreferredLineHeightForMinimum, false);
+ }
+ } finally {
+ tvArray.recycle();
+ }
+ if (!hasUseLocalePreferredLineHeightForMinimumInt) {
+ useLocalePreferredLineHeightForMinimumInt =
+ CompatChanges.isChangeEnabled(LINE_HEIGHT_FOR_LOCALE);
+ }
+ setLocalePreferredLineHeightForMinimumUsed(useLocalePreferredLineHeightForMinimumInt);
}
@Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 9a4106d9..57e4e6a 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -867,6 +867,8 @@
private boolean mUseBoundsForWidth;
@Nullable private Paint.FontMetrics mMinimumFontMetrics;
+ @Nullable private Paint.FontMetrics mLocalePreferredFontMetrics;
+ private boolean mUseLocalePreferredLineHeightForMinimum;
@ViewDebug.ExportedProperty(category = "text")
@UnsupportedAppUsage
@@ -1617,6 +1619,11 @@
case com.android.internal.R.styleable.TextView_useBoundsForWidth:
mUseBoundsForWidth = a.getBoolean(attr, false);
hasUseBoundForWidthValue = true;
+ break;
+ case com.android.internal.R.styleable
+ .TextView_useLocalePreferredLineHeightForMinimum:
+ mUseLocalePreferredLineHeightForMinimum = a.getBoolean(attr, false);
+ break;
}
}
@@ -4992,6 +4999,41 @@
}
/**
+ * Returns true if the locale preferred line height is used for the minimum line height.
+ *
+ * @return true if using locale preferred line height for the minimum line height. Otherwise
+ * false.
+ *
+ * @see #setLocalePreferredLineHeightForMinimumUsed(boolean)
+ * @see #setMinimumFontMetrics(Paint.FontMetrics)
+ * @see #getMinimumFontMetrics()
+ */
+ @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+ public boolean isLocalePreferredLineHeightForMinimumUsed() {
+ return mUseLocalePreferredLineHeightForMinimum;
+ }
+
+ /**
+ * Set true if the locale preferred line height is used for the minimum line height.
+ *
+ * By setting this flag to true is equivalenet to call
+ * {@link #setMinimumFontMetrics(Paint.FontMetrics)} with the one obtained by
+ * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}.
+ *
+ * If custom minimum line height was specified by
+ * {@link #setMinimumFontMetrics(Paint.FontMetrics)}, this flag will be ignored.
+ *
+ * @param flag true for using locale preferred line height for the minimum line height.
+ * @see #isLocalePreferredLineHeightForMinimumUsed()
+ * @see #setMinimumFontMetrics(Paint.FontMetrics)
+ * @see #getMinimumFontMetrics()
+ */
+ @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+ public void setLocalePreferredLineHeightForMinimumUsed(boolean flag) {
+ mUseLocalePreferredLineHeightForMinimum = flag;
+ }
+
+ /**
* @return whether fallback line spacing is enabled, {@code true} by default
*
* @see #setFallbackLineSpacing(boolean)
@@ -10728,6 +10770,21 @@
return alignment;
}
+ private Paint.FontMetrics getResolvedMinimumFontMetrics() {
+ if (mMinimumFontMetrics != null) {
+ return mMinimumFontMetrics;
+ }
+ if (!mUseLocalePreferredLineHeightForMinimum) {
+ return null;
+ }
+
+ if (mLocalePreferredFontMetrics == null) {
+ mLocalePreferredFontMetrics = new Paint.FontMetrics();
+ }
+ mTextPaint.getFontMetricsForLocale(mLocalePreferredFontMetrics);
+ return mLocalePreferredFontMetrics;
+ }
+
/**
* The width passed in is now the desired layout width,
* not the full view width with padding.
@@ -10792,7 +10849,8 @@
if (hintBoring == UNKNOWN_BORING) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
isFallbackLineSpacingForBoringLayout(),
- mMinimumFontMetrics, mHintBoring);
+ getResolvedMinimumFontMetrics(), mHintBoring);
+
if (hintBoring != null) {
mHintBoring = hintBoring;
}
@@ -10842,7 +10900,8 @@
.setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
mLineBreakStyle, mLineBreakWordStyle))
.setUseBoundsForWidth(mUseBoundsForWidth)
- .setMinimumFontMetrics(mMinimumFontMetrics);
+ .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
+
if (shouldEllipsize) {
builder.setEllipsize(mEllipsize)
.setEllipsizedWidth(ellipsisWidth);
@@ -10907,12 +10966,13 @@
.setUseBoundsForWidth(mUseBoundsForWidth)
.setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
.setEllipsizedWidth(ellipsisWidth)
- .setMinimumFontMetrics(mMinimumFontMetrics);
+ .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
result = builder.build();
} else {
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
- isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+ isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+ mBoring);
if (boring != null) {
mBoring = boring;
}
@@ -10926,7 +10986,7 @@
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, null, wantWidth,
isFallbackLineSpacingForBoringLayout(),
- mUseBoundsForWidth, mMinimumFontMetrics);
+ mUseBoundsForWidth, getResolvedMinimumFontMetrics());
} else {
result = new BoringLayout(
mTransformed,
@@ -10941,7 +11001,7 @@
null,
boring,
mUseBoundsForWidth,
- mMinimumFontMetrics);
+ getResolvedMinimumFontMetrics());
}
if (useSaved) {
@@ -10953,7 +11013,7 @@
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth, isFallbackLineSpacingForBoringLayout(),
- mUseBoundsForWidth, mMinimumFontMetrics);
+ mUseBoundsForWidth, getResolvedMinimumFontMetrics());
} else {
result = new BoringLayout(
mTransformed,
@@ -10968,7 +11028,7 @@
effectiveEllipsize,
boring,
mUseBoundsForWidth,
- mMinimumFontMetrics);
+ getResolvedMinimumFontMetrics());
}
}
}
@@ -10988,7 +11048,7 @@
.setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
mLineBreakStyle, mLineBreakWordStyle))
.setUseBoundsForWidth(mUseBoundsForWidth)
- .setMinimumFontMetrics(mMinimumFontMetrics);
+ .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth);
@@ -11116,7 +11176,8 @@
if (des < 0) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
- isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+ isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+ mBoring);
if (boring != null) {
mBoring = boring;
}
@@ -11156,7 +11217,7 @@
if (hintDes < 0) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
- isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics,
+ isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
@@ -11370,7 +11431,7 @@
.setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
mLineBreakStyle, mLineBreakWordStyle))
.setUseBoundsForWidth(mUseBoundsForWidth)
- .setMinimumFontMetrics(mMinimumFontMetrics);
+ .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
final StaticLayout layout = layoutBuilder.build();
diff --git a/core/java/android/widget/flags/notification_widget_flags.aconfig b/core/java/android/widget/flags/notification_widget_flags.aconfig
index 9f0b7c3..bfe3d05 100644
--- a/core/java/android/widget/flags/notification_widget_flags.aconfig
+++ b/core/java/android/widget/flags/notification_widget_flags.aconfig
@@ -5,4 +5,14 @@
namespace: "systemui"
description: "Enables notification specific LinearLayout optimization"
bug: "316110233"
+}
+
+flag {
+ name: "call_style_set_data_async"
+ namespace: "systemui"
+ description: "Offloads caller icon drawable loading to the background thread"
+ bug: "293961072"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/core/java/android/window/IUnhandledDragCallback.aidl b/core/java/android/window/IUnhandledDragCallback.aidl
new file mode 100644
index 0000000..7806b1f
--- /dev/null
+++ b/core/java/android/window/IUnhandledDragCallback.aidl
@@ -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 android.window;
+
+import android.view.DragEvent;
+
+/**
+ * A callback for notifying the system when the unhandled drop is complete.
+ * {@hide}
+ */
+oneway interface IUnhandledDragCallback {
+ /**
+ * Called when the IUnhandledDropListener has fully handled the drop, and the drag can be
+ * cleaned up. If handled is `true`, then cleanup of the drag and drag surface will be
+ * immediate, otherwise, the system will treat the drag as a cancel back to the start of the
+ * drag.
+ */
+ void notifyUnhandledDropComplete(boolean handled);
+}
diff --git a/core/java/android/window/IUnhandledDragListener.aidl b/core/java/android/window/IUnhandledDragListener.aidl
new file mode 100644
index 0000000..52e9895
--- /dev/null
+++ b/core/java/android/window/IUnhandledDragListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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 android.window;
+
+import android.view.DragEvent;
+import android.window.IUnhandledDragCallback;
+
+/**
+ * An interface to a handler for global drags that are not consumed (ie. not handled by any window).
+ * {@hide}
+ */
+oneway interface IUnhandledDragListener {
+ /**
+ * Called when the user finishes the drag gesture but no windows have reported handling the
+ * drop. The DragEvent is populated with the drag surface for the listener to animate. The
+ * listener *MUST* call the provided callback exactly once when it has finished handling the
+ * drop. If the listener calls the callback with `true` then it is responsible for removing
+ * and releasing the drag surface passed through the DragEvent.
+ */
+ void onUnhandledDrop(in DragEvent event, in IUnhandledDragCallback callback);
+}
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index feae173..15b9b78 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -159,8 +159,11 @@
*/
public static final int FLAG_SYNC = 1 << 21;
+ /** This change represents its start configuration for the duration of the animation. */
+ public static final int FLAG_CONFIG_AT_END = 1 << 22;
+
/** The first unused bit. This can be used by remotes to attach custom flags to this change. */
- public static final int FLAG_FIRST_CUSTOM = 1 << 22;
+ public static final int FLAG_FIRST_CUSTOM = 1 << 23;
/** The change belongs to a window that won't contain activities. */
public static final int FLAGS_IS_NON_APP_WINDOW =
@@ -193,6 +196,7 @@
FLAG_TASK_LAUNCHING_BEHIND,
FLAG_MOVED_TO_TOP,
FLAG_SYNC,
+ FLAG_CONFIG_AT_END,
FLAG_FIRST_CUSTOM
})
public @interface ChangeFlags {}
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index d9b5b2d..76a34ae 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -676,18 +676,20 @@
* This identifies them.
* @param type The {@link InsetsType} of the insets source.
* @param frame The rectangle area of the insets source.
+ * @param boundingRects The bounding rects within this inset, relative to the |frame|.
* @hide
*/
@NonNull
public WindowContainerTransaction addInsetsSource(
@NonNull WindowContainerToken receiver,
- IBinder owner, int index, @InsetsType int type, Rect frame) {
+ IBinder owner, int index, @InsetsType int type, Rect frame, Rect[] boundingRects) {
final HierarchyOp hierarchyOp =
new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER)
.setContainer(receiver.asBinder())
.setInsetsFrameProvider(new InsetsFrameProvider(owner, index, type)
.setSource(InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE)
- .setArbitraryRectangle(frame))
+ .setArbitraryRectangle(frame)
+ .setBoundingRects(boundingRects))
.setInsetsFrameOwner(owner)
.build();
mHierarchyOps.add(hierarchyOp);
@@ -914,6 +916,23 @@
}
/**
+ * Defers client-facing configuration changes for activities in `container` until the end of
+ * the transition animation. The configuration will still be applied to the WMCore hierarchy
+ * at the normal time (beginning); so, special consideration must be made for this in the
+ * animation.
+ *
+ * @param container WindowContainerToken who's children should defer config notification.
+ * @hide
+ */
+ @NonNull
+ public WindowContainerTransaction deferConfigToTransitionEnd(
+ @NonNull WindowContainerToken container) {
+ final Change change = getOrCreateChange(container.asBinder());
+ change.mConfigAtTransitionEnd = true;
+ return this;
+ }
+
+ /**
* Merges another WCT into this one.
* @param transfer When true, this will transfer everything from other potentially leaving
* other in an unusable state. When false, other is left alone, but
@@ -1050,6 +1069,7 @@
private Rect mBoundsChangeSurfaceBounds = null;
@Nullable
private Rect mRelativeBounds = null;
+ private boolean mConfigAtTransitionEnd = false;
private int mActivityWindowingMode = -1;
private int mWindowingMode = -1;
@@ -1082,6 +1102,7 @@
mRelativeBounds = new Rect();
mRelativeBounds.readFromParcel(in);
}
+ mConfigAtTransitionEnd = in.readBoolean();
mWindowingMode = in.readInt();
mActivityWindowingMode = in.readInt();
@@ -1134,6 +1155,8 @@
? other.mRelativeBounds
: new Rect(other.mRelativeBounds);
}
+ mConfigAtTransitionEnd = mConfigAtTransitionEnd
+ || other.mConfigAtTransitionEnd;
}
public int getWindowingMode() {
@@ -1191,6 +1214,11 @@
return mDragResizing;
}
+ /** Gets whether the config should be sent to the client at the end of the transition. */
+ public boolean getConfigAtTransitionEnd() {
+ return mConfigAtTransitionEnd;
+ }
+
public int getChangeMask() {
return mChangeMask;
}
@@ -1269,6 +1297,9 @@
if ((mChangeMask & CHANGE_RELATIVE_BOUNDS) != 0) {
sb.append("relativeBounds:").append(mRelativeBounds).append(",");
}
+ if (mConfigAtTransitionEnd) {
+ sb.append("configAtTransitionEnd").append(",");
+ }
sb.append("}");
return sb.toString();
}
@@ -1297,6 +1328,7 @@
if (mRelativeBounds != null) {
mRelativeBounds.writeToParcel(dest, flags);
}
+ dest.writeBoolean(mConfigAtTransitionEnd);
dest.writeInt(mWindowingMode);
dest.writeInt(mActivityWindowingMode);
diff --git a/core/java/com/android/internal/os/BackgroundThread.java b/core/java/com/android/internal/os/BackgroundThread.java
index 72da819..b75daed 100644
--- a/core/java/com/android/internal/os/BackgroundThread.java
+++ b/core/java/com/android/internal/os/BackgroundThread.java
@@ -27,6 +27,7 @@
/**
* Shared singleton background thread for each process.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class BackgroundThread extends HandlerThread {
private static final long SLOW_DISPATCH_THRESHOLD_MS = 10_000;
private static final long SLOW_DELIVERY_THRESHOLD_MS = 30_000;
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index aa60cc9..0b7593a 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -297,9 +297,20 @@
}
}
+ public static class EventLogger {
+ /**
+ * Records a statsd event when the batterystats config file is written to disk.
+ */
+ public void writeCommitSysConfigFile(long startTimeMs) {
+ com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
+ "batterystats", SystemClock.uptimeMillis() - startTimeMs);
+ }
+ }
+
private TraceDelegate mTracer;
private int mTraceLastState = 0;
private int mTraceLastState2 = 0;
+ private final EventLogger mEventLogger;
/**
* Constructor
@@ -311,8 +322,16 @@
public BatteryStatsHistory(File systemDir, int maxHistoryFiles, int maxHistoryBufferSize,
HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
MonotonicClock monotonicClock) {
+ this(systemDir, maxHistoryFiles, maxHistoryBufferSize,
+ stepDetailsCalculator, clock, monotonicClock, new TraceDelegate(),
+ new EventLogger());
+ }
+
+ public BatteryStatsHistory(File systemDir, int maxHistoryFiles, int maxHistoryBufferSize,
+ HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
+ MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) {
this(Parcel.obtain(), systemDir, maxHistoryFiles, maxHistoryBufferSize,
- stepDetailsCalculator, clock, monotonicClock, new TraceDelegate());
+ stepDetailsCalculator, clock, monotonicClock, tracer, eventLogger);
initHistoryBuffer();
}
@@ -320,15 +339,15 @@
public BatteryStatsHistory(Parcel historyBuffer, File systemDir,
int maxHistoryFiles, int maxHistoryBufferSize,
HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
- MonotonicClock monotonicClock, TraceDelegate tracer) {
+ MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger) {
this(historyBuffer, systemDir, maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator,
- clock, monotonicClock, tracer, null);
+ clock, monotonicClock, tracer, eventLogger, null);
}
private BatteryStatsHistory(Parcel historyBuffer, File systemDir,
int maxHistoryFiles, int maxHistoryBufferSize,
HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
- MonotonicClock monotonicClock, TraceDelegate tracer,
+ MonotonicClock monotonicClock, TraceDelegate tracer, EventLogger eventLogger,
BatteryStatsHistory writableHistory) {
mHistoryBuffer = historyBuffer;
mSystemDir = systemDir;
@@ -338,6 +357,7 @@
mTracer = tracer;
mClock = clock;
mMonotonicClock = monotonicClock;
+ mEventLogger = eventLogger;
mWritableHistory = writableHistory;
if (mWritableHistory != null) {
mMutable = false;
@@ -394,19 +414,21 @@
HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
MonotonicClock monotonicClock) {
this(maxHistoryFiles, maxHistoryBufferSize, stepDetailsCalculator, clock, monotonicClock,
- new TraceDelegate());
+ new TraceDelegate(), new EventLogger());
}
@VisibleForTesting
public BatteryStatsHistory(int maxHistoryFiles, int maxHistoryBufferSize,
HistoryStepDetailsCalculator stepDetailsCalculator, Clock clock,
- MonotonicClock monotonicClock, TraceDelegate traceDelegate) {
+ MonotonicClock monotonicClock, TraceDelegate traceDelegate,
+ EventLogger eventLogger) {
mMaxHistoryFiles = maxHistoryFiles;
mMaxHistoryBufferSize = maxHistoryBufferSize;
mStepDetailsCalculator = stepDetailsCalculator;
mTracer = traceDelegate;
mClock = clock;
mMonotonicClock = monotonicClock;
+ mEventLogger = eventLogger;
mHistoryBuffer = Parcel.obtain();
mSystemDir = null;
@@ -425,6 +447,7 @@
mSystemDir = null;
mHistoryDir = null;
mStepDetailsCalculator = null;
+ mEventLogger = new EventLogger();
mWritableHistory = null;
mMutable = false;
@@ -482,7 +505,7 @@
historyBufferCopy.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize());
return new BatteryStatsHistory(historyBufferCopy, mSystemDir, 0, 0, null, null, null,
- null, this);
+ null, mEventLogger, this);
}
}
@@ -2154,8 +2177,7 @@
+ " duration ms:" + (SystemClock.uptimeMillis() - startTimeMs)
+ " bytes:" + p.dataSize());
}
- com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
- "batterystats", SystemClock.uptimeMillis() - startTimeMs);
+ mEventLogger.writeCommitSysConfigFile(startTimeMs);
} catch (IOException e) {
Slog.w(TAG, "Error writing battery statistics", e);
file.failWrite(fos);
@@ -2164,6 +2186,7 @@
}
}
+
/**
* Returns the total number of history tags in the tag pool.
*/
diff --git a/core/java/com/android/internal/os/LongMultiStateCounter.java b/core/java/com/android/internal/os/LongMultiStateCounter.java
index 33a9d54..064609f 100644
--- a/core/java/com/android/internal/os/LongMultiStateCounter.java
+++ b/core/java/com/android/internal/os/LongMultiStateCounter.java
@@ -55,11 +55,12 @@
*
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+@android.ravenwood.annotation.RavenwoodNativeSubstitutionClass(
+ "com.android.hoststubgen.nativesubstitution.LongMultiStateCounter_host")
public final class LongMultiStateCounter implements Parcelable {
- private static final NativeAllocationRegistry sRegistry =
- NativeAllocationRegistry.createMalloced(
- LongMultiStateCounter.class.getClassLoader(), native_getReleaseFunc());
+ private static NativeAllocationRegistry sRegistry;
private final int mStateCount;
@@ -71,16 +72,33 @@
Preconditions.checkArgumentPositive(stateCount, "stateCount must be greater than 0");
mStateCount = stateCount;
mNativeObject = native_init(stateCount);
- sRegistry.registerNativeAllocation(this, mNativeObject);
+ registerNativeAllocation();
}
private LongMultiStateCounter(Parcel in) {
mNativeObject = native_initFromParcel(in);
- sRegistry.registerNativeAllocation(this, mNativeObject);
+ registerNativeAllocation();
mStateCount = native_getStateCount(mNativeObject);
}
+ @android.ravenwood.annotation.RavenwoodReplace
+ private void registerNativeAllocation() {
+ if (sRegistry == null) {
+ synchronized (LongMultiStateCounter.class) {
+ if (sRegistry == null) {
+ sRegistry = NativeAllocationRegistry.createMalloced(
+ LongMultiStateCounter.class.getClassLoader(), native_getReleaseFunc());
+ }
+ }
+ }
+ sRegistry.registerNativeAllocation(this, mNativeObject);
+ }
+
+ private void registerNativeAllocation$ravenwood() {
+ // No-op under ravenwood
+ }
+
public int getStateCount() {
return mStateCount;
}
@@ -221,10 +239,10 @@
private static native long native_getCount(long nativeObject, int state);
@FastNative
- private native String native_toString(long nativeObject);
+ private static native String native_toString(long nativeObject);
@FastNative
- private native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
+ private static native void native_writeToParcel(long nativeObject, Parcel dest, int flags);
@FastNative
private static native long native_initFromParcel(Parcel parcel);
diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java
index e9a8d4b..1f4abc1 100644
--- a/core/java/com/android/internal/os/TimeoutRecord.java
+++ b/core/java/com/android/internal/os/TimeoutRecord.java
@@ -45,6 +45,7 @@
TimeoutKind.APP_REGISTERED,
TimeoutKind.SHORT_FGS_TIMEOUT,
TimeoutKind.JOB_SERVICE,
+ TimeoutKind.FGS_TIMEOUT,
})
@Retention(RetentionPolicy.SOURCE)
@@ -59,6 +60,7 @@
int SHORT_FGS_TIMEOUT = 8;
int JOB_SERVICE = 9;
int APP_START = 10;
+ int FGS_TIMEOUT = 11;
}
/** Kind of timeout, e.g. BROADCAST_RECEIVER, etc. */
@@ -186,6 +188,12 @@
return TimeoutRecord.endingNow(TimeoutKind.SHORT_FGS_TIMEOUT, reason);
}
+ /** Record for a "foreground service" timeout. */
+ @NonNull
+ public static TimeoutRecord forFgsTimeout(String reason) {
+ return TimeoutRecord.endingNow(TimeoutKind.FGS_TIMEOUT, reason);
+ }
+
/** Record for a job related timeout. */
@NonNull
public static TimeoutRecord forJobService(String reason) {
diff --git a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
index f62ff38..e11067d 100644
--- a/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
+++ b/core/java/com/android/internal/os/anr/AnrLatencyTracker.java
@@ -22,6 +22,7 @@
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__BROADCAST_OF_INTENT;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__CONTENT_PROVIDER_NOT_RESPONDING;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__EXECUTING_SERVICE;
+import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__FGS_TIMEOUT;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__INPUT_DISPATCHING_TIMEOUT_NO_FOCUSED_WINDOW;
import static com.android.internal.util.FrameworkStatsLog.ANRLATENCY_REPORTED__ANR_TYPE__JOB_SERVICE;
@@ -548,6 +549,8 @@
return ANRLATENCY_REPORTED__ANR_TYPE__SHORT_FGS_TIMEOUT;
case TimeoutKind.JOB_SERVICE:
return ANRLATENCY_REPORTED__ANR_TYPE__JOB_SERVICE;
+ case TimeoutKind.FGS_TIMEOUT:
+ return ANRLATENCY_REPORTED__ANR_TYPE__FGS_TIMEOUT;
default:
return ANRLATENCY_REPORTED__ANR_TYPE__UNKNOWN_ANR_TYPE;
}
diff --git a/core/java/com/android/internal/power/EnergyConsumerStats.java b/core/java/com/android/internal/power/EnergyConsumerStats.java
index e2098dd..764908d 100644
--- a/core/java/com/android/internal/power/EnergyConsumerStats.java
+++ b/core/java/com/android/internal/power/EnergyConsumerStats.java
@@ -44,6 +44,7 @@
* This class doesn't use a TimeBase, and instead requires manual decisions about when to
* accumulate since it is trivial. However, in the future, a TimeBase could be used instead.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class EnergyConsumerStats {
private static final String TAG = "MeasuredEnergyStats";
diff --git a/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
index b5e9b8f..0ceba25 100644
--- a/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
+++ b/core/java/com/android/internal/widget/NotificationOptimizedLinearLayout.java
@@ -53,6 +53,7 @@
* - LinearLayout doesn't have <code>weightSum</code>.
* - Horizontal LinearLayout's width should be measured EXACTLY.
* - Horizontal LinearLayout shouldn't need baseLineAlignment.
+ * - Horizontal LinearLayout shouldn't have any child that has negative left or right margin.
* - Vertical LinearLayout shouldn't have MATCH_PARENT children when it is not measured EXACTLY.
*
* @hide
@@ -88,7 +89,7 @@
final View weightedChildView = getSingleWeightedChild();
mShouldUseOptimizedLayout =
isUseOptimizedLinearLayoutFlagEnabled() && weightedChildView != null
- && isLinearLayoutUsable(widthMeasureSpec, heightMeasureSpec);
+ && isOptimizationPossible(widthMeasureSpec, heightMeasureSpec);
if (mShouldUseOptimizedLayout) {
onMeasureOptimized(weightedChildView, widthMeasureSpec, heightMeasureSpec);
@@ -118,7 +119,7 @@
* @param heightMeasureSpec The height measurement specification.
* @return `true` if optimization is possible, `false` otherwise.
*/
- private boolean isLinearLayoutUsable(int widthMeasureSpec, int heightMeasureSpec) {
+ private boolean isOptimizationPossible(int widthMeasureSpec, int heightMeasureSpec) {
final boolean hasWeightSum = getWeightSum() > 0.0f;
if (hasWeightSum) {
logSkipOptimizedOnMeasure("Has weightSum.");
@@ -142,10 +143,36 @@
logSkipOptimizedOnMeasure("Need to apply baseline.");
return false;
}
+
+ if (requiresNegativeMarginHandlingForHorizontalLinearLayout()) {
+ logSkipOptimizedOnMeasure("Need to handle negative margins.");
+ return false;
+ }
return true;
}
/**
+ * @return if the horizontal linearlayout requires to handle negative margins in its children.
+ * In that case, we can't use excessSpace because LinearLayout negative margin handling for
+ * excess space and WRAP_CONTENT is different.
+ */
+ private boolean requiresNegativeMarginHandlingForHorizontalLinearLayout() {
+ if (getOrientation() == VERTICAL) {
+ return false;
+ }
+
+ final List<View> activeChildren = getActiveChildren();
+ for (int i = 0; i < activeChildren.size(); i++) {
+ final View child = activeChildren.get(i);
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+ if (lp.leftMargin < 0 || lp.rightMargin < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* @return if the vertical linearlayout requires match_parent children remeasure
*/
private boolean requiresMatchParentRemeasureForVerticalLinearLayout(int widthMeasureSpec) {
@@ -337,94 +364,81 @@
*/
private void measureVerticalOptimized(@NonNull View weightedChildView, int widthMeasureSpec,
int heightMeasureSpec) {
- final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
- final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int totalLength = 0;
int maxWidth = 0;
- int usedHeight = 0;
- final List<View> activeChildren = getActiveChildren();
- final int activeChildCount = activeChildren.size();
+ final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
- final boolean isContentFirstItem = !activeChildren.isEmpty() && activeChildren.get(0)
- == weightedChildView;
-
- final boolean isContentLastItem = !activeChildren.isEmpty() && activeChildren.get(
- activeChildCount - 1) == weightedChildView;
-
- final int horizontalPaddings = getPaddingLeft() + getPaddingRight();
-
- // 1. Measure other child views.
- for (int i = 0; i < activeChildCount; i++) {
- final View child = activeChildren.get(i);
- if (child == weightedChildView) {
+ // 1. Measure all unweighted children
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child == null || child.getVisibility() == GONE) {
continue;
}
+
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
- int requiredVerticalPadding = lp.topMargin + lp.bottomMargin;
- if (!isContentFirstItem && i == 0) {
- requiredVerticalPadding += getPaddingTop();
- }
- if (!isContentLastItem && i == activeChildCount - 1) {
- requiredVerticalPadding += getPaddingBottom();
+ if (child == weightedChildView) {
+ // In excessMode, LinearLayout add weighted child top and bottom margins to
+ // totalLength when their sum is positive.
+ if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) {
+ totalLength = Math.max(totalLength, totalLength + lp.topMargin
+ + lp.bottomMargin);
+ }
+ continue;
}
- child.measure(ViewGroup.getChildMeasureSpec(widthMeasureSpec,
- horizontalPaddings + lp.leftMargin + lp.rightMargin,
- child.getLayoutParams().width),
- ViewGroup.getChildMeasureSpec(heightMeasureSpec, requiredVerticalPadding,
- lp.height));
+ measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+ // LinearLayout only adds measured children heights and its top and bottom margins
+ // to totalLength when their sum is positive.
+ totalLength = Math.max(totalLength,
+ totalLength + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
- usedHeight += child.getMeasuredHeight() + requiredVerticalPadding;
}
- // measure content
+ // Add padding to totalLength that we are going to use for remaining space.
+ totalLength += mPaddingTop + mPaddingBottom;
+
+ // 2. generate measure spec for weightedChildView.
final MarginLayoutParams lp = (MarginLayoutParams) weightedChildView.getLayoutParams();
+ // height should be AT_MOST for non EXACT cases.
+ final int childHeightMeasureMode =
+ heightMode == MeasureSpec.EXACTLY ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST;
+ final int childHeightMeasureSpec;
- int usedSpace = usedHeight + lp.topMargin + lp.bottomMargin;
- if (isContentFirstItem) {
- usedSpace += getPaddingTop();
+ // In excess mode, LinearLayout measures weighted children with remaining space. Otherwise,
+ // it is measured with remaining space just like other children.
+ if (lp.height == 0 && heightMode == MeasureSpec.EXACTLY) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, availableHeight - totalLength), childHeightMeasureMode);
+ } else {
+ final int usedHeight = lp.topMargin + lp.bottomMargin + totalLength;
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+ Math.max(0, availableHeight - usedHeight), childHeightMeasureMode);
}
- if (isContentLastItem) {
- usedSpace += getPaddingBottom();
- }
+ final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin, lp.width);
- final int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
- final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
-
- final int childWidthMeasureSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
- horizontalPaddings + lp.leftMargin + lp.rightMargin, lp.width);
-
- // 2. Calculate remaining height for weightedChildView.
- final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
- Math.max(0, availableHeight - usedSpace), MeasureSpec.AT_MOST);
-
- // 3. Measure weightedChildView with the remaining remaining space.
+ // 3. Measure weightedChildView with the remaining space.
weightedChildView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ totalLength = Math.max(totalLength,
+ totalLength + weightedChildView.getMeasuredHeight() + lp.topMargin
+ + lp.bottomMargin);
+
maxWidth = Math.max(maxWidth,
weightedChildView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
- final int totalUsedHeight = usedSpace + weightedChildView.getMeasuredHeight();
+ // Add padding to width
+ maxWidth += getPaddingLeft() + getPaddingRight();
- final int measuredWidth;
- if (widthMode == MeasureSpec.EXACTLY) {
- measuredWidth = availableWidth;
- } else {
- measuredWidth = maxWidth + getPaddingStart() + getPaddingEnd();
- }
-
- final int measuredHeight;
- if (heightMode == MeasureSpec.EXACTLY) {
- measuredHeight = availableHeight;
- } else {
- measuredHeight = totalUsedHeight;
- }
-
- // 4. Set the container size
- setMeasuredDimension(
- resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
- widthMeasureSpec),
- Math.max(getSuggestedMinimumHeight(), measuredHeight));
+ // Resolve final dimensions
+ final int finalWidth = resolveSizeAndState(Math.max(maxWidth, getSuggestedMinimumWidth()),
+ widthMeasureSpec, 0);
+ final int finalHeight = resolveSizeAndState(
+ Math.max(totalLength, getSuggestedMinimumHeight()), heightMeasureSpec, 0);
+ setMeasuredDimension(finalWidth, finalHeight);
}
@NonNull
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 3ee15ab..3d0ab4e 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -314,7 +314,8 @@
}
static void NativeSetApkAssets(JNIEnv* env, jclass /*clazz*/, jlong ptr,
- jobjectArray apk_assets_array, jboolean invalidate_caches) {
+ jobjectArray apk_assets_array, jboolean invalidate_caches,
+ jboolean preset) {
ATRACE_NAME("AssetManager::SetApkAssets");
const jsize apk_assets_len = env->GetArrayLength(apk_assets_array);
@@ -343,7 +344,11 @@
}
auto assetmanager = LockAndStartAssetManager(ptr);
- assetmanager->SetApkAssets(apk_assets, invalidate_caches);
+ if (preset) {
+ assetmanager->PresetApkAssets(apk_assets);
+ } else {
+ assetmanager->SetApkAssets(apk_assets, invalidate_caches);
+ }
}
static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint mnc,
@@ -353,7 +358,7 @@
jint screen_height, jint smallest_screen_width_dp,
jint screen_width_dp, jint screen_height_dp, jint screen_layout,
jint ui_mode, jint color_mode, jint grammatical_gender,
- jint major_version) {
+ jint major_version, jboolean force_refresh) {
ATRACE_NAME("AssetManager::SetConfiguration");
const jsize locale_count = (locales == NULL) ? 0 : env->GetArrayLength(locales);
@@ -413,7 +418,7 @@
}
auto assetmanager = LockAndStartAssetManager(ptr);
- assetmanager->SetConfigurations(configs);
+ assetmanager->SetConfigurations(std::move(configs), force_refresh != JNI_FALSE);
assetmanager->SetDefaultLocale(default_locale_int);
}
@@ -1522,8 +1527,8 @@
// AssetManager setup methods.
{"nativeCreate", "()J", (void*)NativeCreate},
{"nativeDestroy", "(J)V", (void*)NativeDestroy},
- {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;Z)V", (void*)NativeSetApkAssets},
- {"nativeSetConfiguration", "(JIILjava/lang/String;[Ljava/lang/String;IIIIIIIIIIIIIIII)V",
+ {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;ZZ)V", (void*)NativeSetApkAssets},
+ {"nativeSetConfiguration", "(JIILjava/lang/String;[Ljava/lang/String;IIIIIIIIIIIIIIIIZ)V",
(void*)NativeSetConfiguration},
{"nativeGetAssignedPackageIdentifiers", "(JZZ)Landroid/util/SparseArray;",
(void*)NativeGetAssignedPackageIdentifiers},
diff --git a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
index ddf7a67..56d3fbb 100644
--- a/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
+++ b/core/jni/com_android_internal_os_LongMultiStateCounter.cpp
@@ -100,7 +100,7 @@
return asLongMultiStateCounter(nativePtr)->getCount(state);
}
-static jobject native_toString(JNIEnv *env, jobject self, jlong nativePtr) {
+static jobject native_toString(JNIEnv *env, jclass, jlong nativePtr) {
return env->NewStringUTF(asLongMultiStateCounter(nativePtr)->toString().c_str());
}
@@ -118,7 +118,7 @@
} \
}
-static void native_writeToParcel(JNIEnv *env, jobject self, jlong nativePtr, jobject jParcel,
+static void native_writeToParcel(JNIEnv *env, jclass, jlong nativePtr, jobject jParcel,
jint flags) {
battery::LongMultiStateCounter *counter = asLongMultiStateCounter(nativePtr);
ndk::ScopedAParcel parcel(AParcel_fromJavaParcel(env, jParcel));
diff --git a/core/proto/android/hardware/location/context_hub_info.proto b/core/proto/android/hardware/location/context_hub_info.proto
index de5cd55..95b5a1a 100644
--- a/core/proto/android/hardware/location/context_hub_info.proto
+++ b/core/proto/android/hardware/location/context_hub_info.proto
@@ -46,4 +46,6 @@
optional float peak_power_draw_mw = 12;
// The maximum number of bytes that can be sent per message to the hub
optional int32 max_packet_length_bytes = 13;
+ // Whether reliable messages are supported
+ optional int32 supports_reliable_messages = 14;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index a425bb0..a2ce212 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3829,6 +3829,7 @@
@hide This is not a third-party API (intended for OEMs and system apps). -->
<permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES"
android:protectionLevel="signature|installer" />
+ <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" />
<!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
<permission android:name="android.permission.PROVISION_DEMO_DEVICE"
@@ -7033,12 +7034,16 @@
<!-- Allows the holder to read blocked numbers. See
{@link android.provider.BlockedNumberContract}.
+ @SystemApi
+ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies")
@hide -->
<permission android:name="android.permission.READ_BLOCKED_NUMBERS"
android:protectionLevel="signature" />
<!-- Allows the holder to write blocked numbers. See
{@link android.provider.BlockedNumberContract}.
+ @SystemApi
+ @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies")
@hide -->
<permission android:name="android.permission.WRITE_BLOCKED_NUMBERS"
android:protectionLevel="signature" />
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 908eeeb..41bc825 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -3893,6 +3893,10 @@
<!-- Component name of an activity that allows the user to modify
the settings for this service. -->
<attr name="settingsActivity" format="string" />
+ <!-- Component name of an activity that allows the user to modify
+ on-screen keyboards variants (e.g. different language or layout) for this service. -->
+ <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") -->
+ <attr name="languageSettingsActivity" format="string"/>
<!-- Set to true in all of the configurations for which this input
method should be considered an option as the default. -->
<attr name="isDefault" format="boolean" />
@@ -5853,6 +5857,23 @@
use glyph bound's as a source of text width. -->
<!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") -->
<attr name="useBoundsForWidth" format="boolean" />
+ <!-- Whether to use the locale preferred line height for the minimum line height.
+
+ This flag is useful for preventing jitter of entering letters into empty EditText.
+ The line height of the text is determined by the font files used for drawing text in a
+ line. However, in case of the empty text case, the line height cannot be determined and
+ the default line height: usually it is came from a font of Latin script. By making this
+ attribute to true, the TextView/EditText uses a line height that is likely used for the
+ locale associated with the widget. For example, if the system locale is Japanese, the
+ height of the EditText will be adjusted to meet the height of the Japanese font even if
+ the text is empty.
+
+ The default value for EditText is true if targetSdkVersion is
+ {@link android.os.Build.VERSION_CODE#VANILLA_ICE_CREAM} or later, otherwise false.
+ For other TextViews, the default value is false.
+ -->
+ <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+ <attr name="useLocalePreferredLineHeightForMinimum" format="boolean" />
</declare-styleable>
<declare-styleable name="TextViewAppearance">
<!-- Base text color, typeface, size, and style. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index d4e727e..c6bc589 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2141,6 +2141,12 @@
<item>com.android.location.fused</item>
</string-array>
+ <!-- Package name of the extension software fallback. -->
+ <string name="config_extensionFallbackPackageName" translatable="false"></string>
+
+ <!-- Service name of the extension software fallback. -->
+ <string name="config_extensionFallbackServiceName" translatable="false"></string>
+
<!-- Package name(s) of Advanced Driver Assistance applications. These packages have additional
management of access to location, specific to driving assistance use-cases. They must be system
packages. This configuration is only applicable to devices that declare
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index d0216b30..78ce2d9 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -82,6 +82,13 @@
<bool name="config_wlan_data_service_conn_persistence_on_restart">true</bool>
<java-symbol type="bool" name="config_wlan_data_service_conn_persistence_on_restart" />
+ <!-- Indicating whether the retry timer from setup data call response for data throttling should
+ be honored for emergency network request. By default this is off, meaning for emergency
+ network requests, the data frameworks will ignore the previous retry timer passed in from
+ setup data call response. -->
+ <bool name="config_honor_data_retry_timer_for_emergency_network">false</bool>
+ <java-symbol type="bool" name="config_honor_data_retry_timer_for_emergency_network" />
+
<!-- Cellular data service package name to bind to by default. If none is specified in an
overlay, an empty string is passed in -->
<string name="config_wwan_data_service_package" translatable="false">com.android.phone</string>
@@ -172,6 +179,35 @@
<integer name="config_satellite_nb_iot_inactivity_timeout_millis">180000</integer>
<java-symbol type="integer" name="config_satellite_nb_iot_inactivity_timeout_millis" />
+ <!-- The time duration in millis needed to switch the modem image from TN to NTN. -->
+ <integer name="config_satellite_modem_image_switching_duration_millis">20000</integer>
+ <java-symbol type="integer" name="config_satellite_modem_image_switching_duration_millis" />
+
+ <!-- The time duration in millis after which Telephony will abort the datagram sending requests.
+ Telephony starts a timer when receiving a datagram sending request in either OFF, IDLE, or
+ NOT_CONNECTED state. In NOT_CONNECTED, the duration of the timer is given by this config.
+ In OFF or IDLE state, the duration of the timer is the sum of this config and the
+ config_satellite_modem_image_switching_duration_millis.
+ -->
+ <integer name="config_datagram_wait_for_connected_state_timeout_millis">60000</integer>
+ <java-symbol type="integer" name="config_datagram_wait_for_connected_state_timeout_millis" />
+
+ <!-- The time duration in millis after which Telephony will stop waiting for the response of the
+ satellite enable request from modem, and send failure response to the client that has
+ requested Telephony to enable satellite.
+ -->
+ <integer name="config_wait_for_satellite_enabling_response_timeout_millis">180000</integer>
+ <java-symbol type="integer" name="config_wait_for_satellite_enabling_response_timeout_millis" />
+
+ <!-- The time duration in millis after which Telephony will abort the datagram sending requests
+ and send failure response to the client that has requested sending the datagrams. Telephony
+ starts a timer after pushing down the datagram sending request to modem. Before expiry, the
+ timer will be stopped when Telephony receives the response for the sending request from
+ modem.
+ -->
+ <integer name="config_wait_for_datagram_sending_response_timeout_millis">180000</integer>
+ <java-symbol type="integer" name="config_wait_for_datagram_sending_response_timeout_millis" />
+
<!-- The timeout duration in milliseconds to determine whether to recommend Dialer to show the
emergency messaging option to users.
@@ -183,6 +219,11 @@
<integer name="config_emergency_call_wait_for_connection_timeout_millis">20000</integer>
<java-symbol type="integer" name="config_emergency_call_wait_for_connection_timeout_millis" />
+ <!-- Indicates the data limit in bytes that can be used for bootstrap sim until factory reset.
+ -1 means unlimited. -->
+ <integer name="config_esim_bootstrap_data_limit_bytes">-1</integer>
+ <java-symbol type="integer" name="config_esim_bootstrap_data_limit_bytes" />
+
<!-- Telephony config for the PLMNs of all satellite providers. This is used by satellite modem
to identify providers that should be ignored if the carrier config
carrier_supported_satellite_services_per_provider_bundle does not support them.
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index f9cf28c..5ee5555 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -151,6 +151,10 @@
<public name="windowOptOutEdgeToEdgeEnforcement"/>
<!-- @FlaggedApi("android.security.content_uri_permission_apis") -->
<public name="requireContentUriPermissionFromCaller" />
+ <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") -->
+ <public name="languageSettingsActivity"/>
+ <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+ <public name="useLocalePreferredLineHeightForMinimum"/>
</staging-public-group>
<staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 3df7570..3d19c85 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2154,6 +2154,8 @@
<java-symbol type="string" name="config_systemImageEditor" />
<java-symbol type="string" name="config_datause_iface" />
<java-symbol type="string" name="config_activityRecognitionHardwarePackageName" />
+ <java-symbol type="string" name="config_extensionFallbackPackageName" />
+ <java-symbol type="string" name="config_extensionFallbackServiceName" />
<java-symbol type="string" name="config_fusedLocationProviderPackageName" />
<java-symbol type="string" name="config_gnssLocationProviderPackageName" />
<java-symbol type="string" name="config_geocoderProviderPackageName" />
diff --git a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
index a3f537e..36ab0d4 100644
--- a/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
+++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java
@@ -70,6 +70,9 @@
assertThat(imi.supportsInlineSuggestionsWithTouchExploration(), is(false));
assertThat(imi.supportsStylusHandwriting(), is(false));
assertThat(imi.createStylusHandwritingSettingsActivityIntent(), equalTo(null));
+ if (Flags.imeSwitcherRevamp()) {
+ assertThat(imi.createImeLanguageSettingsActivityIntent(), equalTo(null));
+ }
}
@Test
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index ee1a4ac..861f719 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -217,6 +217,7 @@
"src/android/os/**/*.java",
"src/android/telephony/PinResultTest.java",
"src/android/util/**/*.java",
+ "src/android/view/DisplayTest.java",
"src/android/view/DisplayInfoTest.java",
"src/com/android/internal/logging/**/*.java",
"src/com/android/internal/os/**/*.java",
diff --git a/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java b/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java
new file mode 100644
index 0000000..e12ca24a
--- /dev/null
+++ b/core/tests/coretests/src/android/os/BluetoothBatteryStatsTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 android.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class BluetoothBatteryStatsTest {
+
+ @Test
+ public void parcelability() {
+ BluetoothBatteryStats stats = new BluetoothBatteryStats(List.of(
+ new BluetoothBatteryStats.UidStats(42, 100, 200, 300, 400, 500),
+ new BluetoothBatteryStats.UidStats(99, 600, 700, 800, 900, 999)
+ ));
+
+ Parcel parcel = Parcel.obtain();
+ stats.writeToParcel(parcel, 0);
+ byte[] bytes = parcel.marshall();
+ parcel.recycle();
+
+ parcel = Parcel.obtain();
+ parcel.unmarshall(bytes, 0, bytes.length);
+ parcel.setDataPosition(0);
+
+ BluetoothBatteryStats actual = new BluetoothBatteryStats(parcel);
+
+ assertThat(actual.getUidStats()).hasSize(2);
+
+ BluetoothBatteryStats.UidStats uid1 = actual.getUidStats().stream()
+ .filter(s->s.uid == 42).findFirst().get();
+
+ assertThat(uid1.scanTimeMs).isEqualTo(100);
+ assertThat(uid1.unoptimizedScanTimeMs).isEqualTo(200);
+ assertThat(uid1.scanResultCount).isEqualTo(300);
+ assertThat(uid1.rxTimeMs).isEqualTo(400);
+ assertThat(uid1.txTimeMs).isEqualTo(500);
+
+ BluetoothBatteryStats.UidStats uid2 = actual.getUidStats().stream()
+ .filter(s->s.uid == 99).findFirst().get();
+ assertThat(uid2.scanTimeMs).isEqualTo(600);
+ }
+}
diff --git a/core/tests/coretests/src/android/os/WakeLockStatsTest.java b/core/tests/coretests/src/android/os/WakeLockStatsTest.java
new file mode 100644
index 0000000..2675ba0
--- /dev/null
+++ b/core/tests/coretests/src/android/os/WakeLockStatsTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 android.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class WakeLockStatsTest {
+
+ @Test
+ public void parcelablity() {
+ WakeLockStats wakeLockStats = new WakeLockStats(
+ List.of(new WakeLockStats.WakeLock(1, "foo", 200, 3000, 40000),
+ new WakeLockStats.WakeLock(2, "bar", 500, 6000, 70000)));
+
+ Parcel parcel = Parcel.obtain();
+ wakeLockStats.writeToParcel(parcel, 0);
+ byte[] bytes = parcel.marshall();
+ parcel.recycle();
+
+ parcel = Parcel.obtain();
+ parcel.unmarshall(bytes, 0, bytes.length);
+ parcel.setDataPosition(0);
+
+ WakeLockStats actual = WakeLockStats.CREATOR.createFromParcel(parcel);
+ assertThat(actual.getWakeLocks()).hasSize(2);
+ WakeLockStats.WakeLock wl1 = actual.getWakeLocks().get(0);
+ assertThat(wl1.uid).isEqualTo(1);
+ assertThat(wl1.name).isEqualTo("foo");
+ assertThat(wl1.timesAcquired).isEqualTo(200);
+ assertThat(wl1.totalTimeHeldMs).isEqualTo(3000);
+ assertThat(wl1.timeHeldMs).isEqualTo(40000);
+
+ WakeLockStats.WakeLock wl2 = actual.getWakeLocks().get(1);
+ assertThat(wl2.uid).isEqualTo(2);
+ }
+}
diff --git a/core/tests/coretests/src/android/view/DisplayTest.java b/core/tests/coretests/src/android/view/DisplayTest.java
new file mode 100644
index 0000000..4d2a1c4
--- /dev/null
+++ b/core/tests/coretests/src/android/view/DisplayTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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 android.view;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.util.DebugUtils;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.ArrayUtils;
+
+import org.junit.Test;
+
+import java.util.function.IntFunction;
+
+@SmallTest
+public class DisplayTest {
+ private static final int[] DISPLAY_STATES = {
+ Display.STATE_UNKNOWN,
+ Display.STATE_OFF,
+ Display.STATE_ON,
+ Display.STATE_DOZE,
+ Display.STATE_DOZE_SUSPEND,
+ Display.STATE_VR,
+ Display.STATE_ON_SUSPEND
+ };
+
+ @Test
+ public void isSuspendedState() {
+ assertOnlyTrueForStates(
+ Display::isSuspendedState,
+ Display.STATE_OFF,
+ Display.STATE_DOZE_SUSPEND,
+ Display.STATE_ON_SUSPEND
+ );
+ }
+
+ @Test
+ public void isDozeState() {
+ assertOnlyTrueForStates(
+ Display::isDozeState,
+ Display.STATE_DOZE,
+ Display.STATE_DOZE_SUSPEND
+ );
+ }
+
+ @Test
+ public void isActiveState() {
+ assertOnlyTrueForStates(
+ Display::isActiveState,
+ Display.STATE_ON,
+ Display.STATE_VR
+ );
+ }
+
+ @Test
+ public void isOffState() {
+ assertOnlyTrueForStates(
+ Display::isOffState,
+ Display.STATE_OFF
+ );
+ }
+
+ @Test
+ public void isOnState() {
+ assertOnlyTrueForStates(
+ Display::isOnState,
+ Display.STATE_ON,
+ Display.STATE_VR,
+ Display.STATE_ON_SUSPEND
+ );
+ }
+
+ private void assertOnlyTrueForStates(IntFunction<Boolean> function, int... trueStates) {
+ for (int state : DISPLAY_STATES) {
+ boolean actual = function.apply(state);
+ boolean expected = ArrayUtils.contains(trueStates, state);
+ assertWithMessage("Unexpected return for Display.STATE_"
+ + DebugUtils.constantToString(Display.class, "STATE_", state))
+ .that(actual).isEqualTo(expected);
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java
index e1bcd4a..936f4d7 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java
@@ -279,5 +279,204 @@
}
}
+ @Test
+ public void testCalculateBoundingRects_noBoundingRects_createsSingleRect() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(null);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(0, 0, 1000, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_noBoundingRectsAndLargerFrame_singleRectFitsRelFrame() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(null);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 1000), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(0, 0, 500, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_frameAtOrigin_resultRelativeToRelFrame() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100),
+ new Rect(800, 0, 1000, 100),
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_notAtOrigin_resultRelativeToRelFrame() {
+ mSource.setFrame(new Rect(100, 100, 1100, 200));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100), // 300x100, aligned left
+ new Rect(800, 0, 1000, 100), // 200x100, aligned right
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(100, 100, 1100, 1100), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_boundingRectFullyInsideFrameInWindow() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(100, 0, 400, 100), // Inside |frame| and |relativeFrame|.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(100, 0, 400, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_boundingRectOutsideFrameInWindow_dropped() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(700, 0, 1000, 100), // Inside |frame|, but outside |relativeFrame|.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false);
+
+ assertEquals(0, rects.length);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_boundingRectPartlyOutsideFrameInWindow_cropped() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(400, 0, 600, 100), // Inside |frame|, and only half inside |relativeFrame|.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 500, 100), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(400, 0, 500, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_framesNotAtOrigin_resultRelativeToWindowFrame() {
+ mSource.setFrame(new Rect(100, 100, 1100, 200));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100), // 300x100 aligned to left.
+ new Rect(800, 0, 1000, 100) // 200x100 align to right.
+ });
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(100, 100, 1100, 1100), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_captionBar() {
+ mCaptionSource.setFrame(new Rect(0, 0, 1000, 100));
+ mCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 200, 100), // 200x100, aligned left.
+ new Rect(800, 0, 1000, 100) // 200x100, aligned right.
+ });
+
+ final Rect[] rects = mCaptionSource.calculateBoundingRects(
+ new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 200, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_captionBarFrameMisaligned_rectsFixedToTop() {
+ mCaptionSource.setFrame(new Rect(500, 500, 1500, 600));
+ mCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 100, 100), // 100x100, aligned to left/top of frame
+ });
+
+ final Rect[] rects = mCaptionSource.calculateBoundingRects(
+ new Rect(495, 495, 1500, 1500), false);
+
+ assertEquals(1, rects.length);
+ // rect should be aligned to the top of relative frame, as if the caption frame had been
+ // corrected to be aligned at the top.
+ assertEquals(new Rect(0, 0, 100, 100), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_imeCaptionBarFrameMisaligned_rectsFixedToBottom() {
+ mImeCaptionSource.setFrame(new Rect(500, 1400, 1500, 1500));
+ mImeCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 100, 100), // 100x100, aligned to left/top of frame
+ });
+
+ final Rect[] rects = mImeCaptionSource.calculateBoundingRects(
+ new Rect(495, 495, 1500, 1500), false);
+
+ assertEquals(1, rects.length);
+ // rect should be aligned to the bottom of relative frame, as if the ime caption frame had
+ // been corrected to be aligned at the top.
+ assertEquals(new Rect(0, 905, 100, 1005), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_imeCaptionBar() {
+ mImeCaptionSource.setFrame(new Rect(0, 900, 1000, 1000)); // Frame at the bottom.
+ mImeCaptionSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 200, 100), // 200x100, aligned left.
+ });
+
+ final Rect[] rects = mImeCaptionSource.calculateBoundingRects(
+ new Rect(0, 0, 1000, 1000), false);
+
+ assertEquals(1, rects.length);
+ assertEquals(new Rect(0, 900, 200, 1000), rects[0]);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_invisible() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100),
+ new Rect(800, 0, 1000, 100),
+ });
+ mSource.setVisible(false);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000),
+ false /* ignoreVisibility */);
+
+ assertEquals(0, rects.length);
+ }
+
+ @Test
+ public void testCalculateBoundingRects_ignoreVisibility() {
+ mSource.setFrame(new Rect(0, 0, 1000, 100));
+ mSource.setBoundingRects(new Rect[]{
+ new Rect(0, 0, 300, 100),
+ new Rect(800, 0, 1000, 100),
+ });
+ mSource.setVisible(false);
+
+ final Rect[] rects = mSource.calculateBoundingRects(new Rect(0, 0, 1000, 1000),
+ true /* ignoreVisibility */);
+
+ assertEquals(2, rects.length);
+ assertEquals(new Rect(0, 0, 300, 100), rects[0]);
+ assertEquals(new Rect(800, 0, 1000, 100), rects[1]);
+ }
+
// Parcel and equals already tested via InsetsStateTest
}
diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 672875a..16bd20a 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -63,6 +63,8 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.List;
+
/**
* Tests for {@link InsetsState}.
*
@@ -88,6 +90,8 @@
null /* owner */, 1 /* index */, navigationBars());
private static final int ID_BOTTOM_GESTURES = InsetsSource.createId(
null /* owner */, 0 /* index */, systemGestures());
+ private static final int ID_EXTRA_CAPTION_BAR = InsetsSource.createId(
+ null /* owner */, 2 /* index */, captionBar());
private final InsetsState mState = new InsetsState();
private final InsetsState mState2 = new InsetsState();
@@ -420,9 +424,11 @@
public void testEquals_visibility() {
mState.getOrCreateSource(ID_IME, ime())
.setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) })
.setVisible(true);
mState2.getOrCreateSource(ID_IME, ime())
- .setFrame(new Rect(0, 0, 100, 100));
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) });
assertNotEqualsAndHashCode();
}
@@ -441,6 +447,30 @@
}
@Test
+ public void testEquals_sameBoundingRects() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) })
+ .setVisible(true);
+ mState2.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) });
+ assertEqualsAndHashCode();
+ }
+
+ @Test
+ public void testEquals_differentBoundingRects() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 10, 10) })
+ .setVisible(true);
+ mState2.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 100, 100))
+ .setBoundingRects(new Rect[]{ new Rect(0, 0, 20, 20) });
+ assertNotEqualsAndHashCode();
+ }
+
+ @Test
public void testEquals_samePrivacyIndicator() {
Rect one = new Rect(0, 1, 2, 3);
Rect two = new Rect(4, 5, 6, 7);
@@ -734,4 +764,94 @@
assertEquals(1, onIdNotFoundInState2Called[0]); // 1000.
assertEquals(1, onFinishCalled[0]);
}
+
+ @Test
+ public void testCalculateBoundingRects() {
+ mState.getOrCreateSource(ID_STATUS_BAR, statusBars())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(null)
+ .setVisible(true);
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{
+ new Rect(0, 0, 200, 100),
+ new Rect(800, 0, 1000, 100)
+ })
+ .setVisible(true);
+ SparseIntArray typeSideMap = new SparseIntArray();
+
+ WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false,
+ SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED,
+ typeSideMap);
+
+ assertEquals(
+ List.of(new Rect(0, 0, 1000, 100)),
+ insets.getBoundingRects(Type.statusBars())
+ );
+ assertEquals(
+ List.of(
+ new Rect(0, 0, 200, 100),
+ new Rect(800, 0, 1000, 100)
+ ),
+ insets.getBoundingRects(Type.captionBar())
+ );
+ }
+
+ @Test
+ public void testCalculateBoundingRects_multipleSourcesOfSameType_concatenated() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{new Rect(0, 0, 200, 100)})
+ .setVisible(true);
+ mState.getOrCreateSource(ID_EXTRA_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{new Rect(800, 0, 1000, 100)})
+ .setVisible(true);
+ SparseIntArray typeSideMap = new SparseIntArray();
+
+ WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false,
+ SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED,
+ typeSideMap);
+
+ final List<Rect> expected = List.of(
+ new Rect(0, 0, 200, 100),
+ new Rect(800, 0, 1000, 100)
+ );
+ final List<Rect> actual = insets.getBoundingRects(captionBar());
+ assertEquals(expected.size(), actual.size());
+
+ // Order does not matter.
+ assertTrue(actual.containsAll(expected));
+ }
+
+ @Test
+ public void testCalculateBoundingRects_captionBar_reportedAsSysGesturesAndTappableElement() {
+ mState.getOrCreateSource(ID_CAPTION_BAR, captionBar())
+ .setFrame(new Rect(0, 0, 1000, 100))
+ .setBoundingRects(new Rect[]{new Rect(0, 0, 200, 100)})
+ .setVisible(true);
+ SparseIntArray typeSideMap = new SparseIntArray();
+
+ WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 1000, 1000), null, false,
+ SOFT_INPUT_ADJUST_RESIZE, 0, 0, TYPE_APPLICATION, ACTIVITY_TYPE_UNDEFINED,
+ typeSideMap);
+
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.captionBar())
+ );
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.systemGestures())
+ );
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.mandatorySystemGestures())
+ );
+ assertEquals(
+ List.of(new Rect(0, 0, 200, 100)),
+ insets.getBoundingRects(Type.tappableElement())
+ );
+
+ }
}
diff --git a/core/tests/coretests/src/android/view/WindowInsetsTest.java b/core/tests/coretests/src/android/view/WindowInsetsTest.java
index 69abf5f..ab4543c 100644
--- a/core/tests/coretests/src/android/view/WindowInsetsTest.java
+++ b/core/tests/coretests/src/android/view/WindowInsetsTest.java
@@ -41,14 +41,14 @@
public void systemWindowInsets_afterConsuming_isConsumed() {
assertTrue(new WindowInsets(WindowInsets.createCompatTypeMap(new Rect(1, 2, 3, 4)), null,
null, false, 0, 0, null, null, null, null,
- WindowInsets.Type.systemBars(), false)
+ WindowInsets.Type.systemBars(), false, null, null, 0, 0)
.consumeSystemWindowInsets().isConsumed());
}
@Test
public void multiNullConstructor_isConsumed() {
assertTrue(new WindowInsets(null, null, null, false, 0, 0, null, null, null, null,
- WindowInsets.Type.systemBars(), false).isConsumed());
+ WindowInsets.Type.systemBars(), false, null, null, 0, 0).isConsumed());
}
@Test
@@ -65,7 +65,7 @@
WindowInsets.assignCompatInsets(insets, new Rect(0, 0, 0, 0));
WindowInsets windowInsets = new WindowInsets(insets, maxInsets, visible, false, 0,
0, null, null, null, DisplayShape.NONE, systemBars(),
- true /* compatIgnoreVisibility */);
+ true /* compatIgnoreVisibility */, null, null, 0, 0);
assertEquals(Insets.of(0, 10, 0, 0), windowInsets.getSystemWindowInsets());
}
}
diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
index 610b8ae..0b0fd66 100644
--- a/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
+++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityServiceConnectionImpl.java
@@ -19,10 +19,12 @@
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
import android.accessibilityservice.MagnificationConfig;
import android.annotation.NonNull;
import android.content.pm.ParceledListSlice;
import android.graphics.Region;
+import android.hardware.usb.UsbDevice;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteCallback;
@@ -237,4 +239,15 @@
int accessibilityWindowId,
SurfaceControl sc,
IAccessibilityInteractionConnectionCallback callback) {}
+
+ @Override
+ public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+ IBrailleDisplayController controller) {}
+
+ @Override
+ public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+ IBrailleDisplayController controller) {}
+
+ @Override
+ public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {}
}
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index f39bddd..51eb41c 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -20,10 +20,12 @@
import static android.view.MotionEvent.ACTION_HOVER_MOVE;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
+import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
import static android.view.stylus.HandwritingTestUtil.createView;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assume.assumeFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -91,6 +93,7 @@
private EditText mTestView1;
private EditText mTestView2;
private Context mContext;
+ private boolean mInitiateWithoutConnection;
@Before
public void setup() throws Exception {
@@ -119,6 +122,7 @@
mHandwritingInitiator.updateHandwritingAreasForView(mTestView1);
mHandwritingInitiator.updateHandwritingAreasForView(mTestView2);
doReturn(true).when(mHandwritingInitiator).tryAcceptStylusHandwritingDelegation(any());
+ mInitiateWithoutConnection = initiationWithoutInputConnection();
}
@Test
@@ -194,7 +198,9 @@
mTestView1.setText("hello");
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ if (!mInitiateWithoutConnection) {
+ mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ }
final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -214,7 +220,7 @@
}
@Test
- public void onTouchEvent_startHandwriting_inputConnectionBuiltAfterStylusMove() {
+ public void onTouchEvent_startHandwriting_servedViewUpdateAfterStylusMove() {
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -225,14 +231,19 @@
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
- // InputConnection is created after stylus movement.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ if (mInitiateWithoutConnection) {
+ // Focus is changed after stylus movement.
+ mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
+ } else {
+ // InputConnection is created after stylus movement.
+ mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ }
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
}
@Test
- public void onTouchEvent_startHandwriting_inputConnectionBuilt_stylusMoveInExtendedHWArea() {
+ public void onTouchEvent_startHandwriting_servedViewUpdate_stylusMoveInExtendedHWArea() {
mTestView1.setText("hello");
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
// The stylus down point is between mTestView1 and mTestView2, but it is within the
@@ -247,21 +258,35 @@
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
- // First create InputConnection for mTestView2 and verify that handwriting is not started.
- mHandwritingInitiator.onInputConnectionCreated(mTestView2);
- verify(mHandwritingInitiator, never()).startHandwriting(mTestView2);
+ if (!mInitiateWithoutConnection) {
+ // First create InputConnection for mTestView2 and verify that handwriting is not
+ // started.
+ mHandwritingInitiator.onInputConnectionCreated(mTestView2);
+ }
- // Next create InputConnection for mTextView1. Handwriting is started for this view since
- // the stylus down point is closest to this view.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ // Note: mTestView2 receives focus when initiationWithoutInputConnection() is enabled.
+ // verify that handwriting is not started.
+ verify(mHandwritingInitiator, never()).startHandwriting(mTestView2);
+ if (mInitiateWithoutConnection) {
+ // Focus is changed after stylus movement.
+ mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
+ } else {
+ // Next create InputConnection for mTextView1. Handwriting is started for this view
+ // since the stylus down point is closest to this view.
+ mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ }
+ // Handwriting is started for this view since the stylus down point is closest to this
+ // view.
verify(mHandwritingInitiator).startHandwriting(mTestView1);
// Since the stylus down point was outside the TextView's bounds, the handwriting initiator
// sets the cursor position.
verify(mTestView1).setSelection(4);
}
+
@Test
public void onTouchEvent_tryAcceptDelegation_delegatorCallbackCreatesInputConnection() {
+ assumeFalse(mInitiateWithoutConnection);
View delegateView = new EditText(mContext);
delegateView.setIsHandwritingDelegate(true);
@@ -281,6 +306,7 @@
verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView);
}
+
@Test
public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() {
View delegateView = new EditText(mContext);
@@ -288,8 +314,14 @@
mHandwritingInitiator.onInputConnectionCreated(delegateView);
reset(mHandwritingInitiator);
- mTestView1.setHandwritingDelegatorCallback(
- () -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
+ if (mInitiateWithoutConnection) {
+ mTestView1.setHandwritingDelegatorCallback(
+ () -> mHandwritingInitiator.updateFocusedView(
+ delegateView, /*fromTouchEvent*/ false));
+ } else {
+ mTestView1.setHandwritingDelegatorCallback(
+ () -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
+ }
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
@@ -339,6 +371,14 @@
assertThat(onTouchEventResult4).isTrue();
}
+ private void callOnInputConnectionOrUpdateViewFocus(View view) {
+ if (mInitiateWithoutConnection) {
+ mHandwritingInitiator.updateFocusedView(view, /*fromTouchEvent*/ true);
+ } else {
+ mHandwritingInitiator.onInputConnectionCreated(view);
+ }
+ }
+
@Test
public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() {
final Rect rect = new Rect(600, 600, 900, 900);
@@ -346,7 +386,7 @@
false /* isStylusHandwritingAvailable */);
mHandwritingInitiator.updateHandwritingAreasForView(testView);
- mHandwritingInitiator.onInputConnectionCreated(testView);
+ callOnInputConnectionOrUpdateViewFocus(testView);
final int x1 = (rect.left + rect.right) / 2;
final int y1 = (rect.top + rect.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -365,7 +405,7 @@
@Test
public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() {
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ callOnInputConnectionOrUpdateViewFocus(mTestView1);
final int x1 = 200;
final int y1 = 200;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -381,7 +421,7 @@
@Test
public void onTouchEvent_notStartHandwriting_when_stylusMove_outOfHWArea() {
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ callOnInputConnectionOrUpdateViewFocus(mTestView1);
final int x1 = 10;
final int y1 = 10;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -397,7 +437,7 @@
@Test
public void onTouchEvent_notStartHandwriting_when_stylusMove_afterTimeOut() {
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ callOnInputConnectionOrUpdateViewFocus(mTestView1);
final int x1 = 10;
final int y1 = 10;
final long time1 = 10L;
@@ -433,8 +473,9 @@
@Test
public void onTouchEvent_focusView_inputConnectionAlreadyBuilt_stylusMoveOnce_withinHWArea() {
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
-
+ if (!mInitiateWithoutConnection) {
+ mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ }
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -487,14 +528,14 @@
verify(mTestView2, times(1)).requestFocus();
- mHandwritingInitiator.onInputConnectionCreated(mTestView2);
+ callOnInputConnectionOrUpdateViewFocus(mTestView2);
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView2);
}
@Test
public void onTouchEvent_handwritingAreaOverlapped_focusedViewHasPriority() {
// Simulate the case where mTestView1 is focused.
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ callOnInputConnectionOrUpdateViewFocus(mTestView1);
// The ACTION_DOWN location is within the handwriting bounds of both mTestView1 and
// mTestView2. Although it's closer to mTestView2's handwriting bounds, handwriting is
// initiated for mTestView1 because it's focused.
@@ -559,9 +600,14 @@
// Set mTextView2 to be the delegate of mTestView1.
mTestView2.setIsHandwritingDelegate(true);
- mTestView1.setHandwritingDelegatorCallback(
- () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2));
-
+ if (mInitiateWithoutConnection) {
+ mTestView1.setHandwritingDelegatorCallback(
+ () -> mHandwritingInitiator.updateFocusedView(
+ mTestView2, /*fromTouchEvent*/ false));
+ } else {
+ mTestView1.setHandwritingDelegatorCallback(
+ () -> mHandwritingInitiator.onInputConnectionCreated(mTestView2));
+ }
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Prerequisite check, verify that handwriting started for delegateView.
@@ -610,8 +656,13 @@
assertThat(icon1).isNull();
// Simulate that focus is switched to mTestView2 first and then switched back.
- mHandwritingInitiator.onInputConnectionCreated(mTestView2);
- mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ if (mInitiateWithoutConnection) {
+ mHandwritingInitiator.updateFocusedView(mTestView2, /*fromTouchEvent*/ true);
+ mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
+ } else {
+ mHandwritingInitiator.onInputConnectionCreated(mTestView2);
+ mHandwritingInitiator.onInputConnectionCreated(mTestView1);
+ }
PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
// After the change of focus, hover icon shows again.
@@ -620,9 +671,15 @@
@Test
public void autoHandwriting_whenDisabled_wontStartHW() {
- View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */,
- true /* isStylusHandwritingAvailable */);
- mHandwritingInitiator.onInputConnectionCreated(mockView);
+ if (mInitiateWithoutConnection) {
+ mTestView1.setAutoHandwritingEnabled(false);
+ mTestView1.setHandwritingDelegatorCallback(null);
+ mHandwritingInitiator.updateFocusedView(mTestView1, /*fromTouchEvent*/ true);
+ } else {
+ View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */,
+ true /* isStylusHandwritingAvailable */);
+ mHandwritingInitiator.onInputConnectionCreated(mockView);
+ }
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
@@ -639,6 +696,7 @@
@Test
public void onInputConnectionCreated() {
+ assumeFalse(mInitiateWithoutConnection);
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
assertThat(mHandwritingInitiator.mConnectedView).isNotNull();
assertThat(mHandwritingInitiator.mConnectedView.get()).isEqualTo(mTestView1);
@@ -646,6 +704,7 @@
@Test
public void onInputConnectionCreated_whenAutoHandwritingIsDisabled() {
+ assumeFalse(mInitiateWithoutConnection);
View view = new View(mContext);
view.setAutoHandwritingEnabled(false);
assertThat(view.isAutoHandwritingEnabled()).isFalse();
@@ -656,6 +715,7 @@
@Test
public void onInputConnectionClosed() {
+ assumeFalse(mInitiateWithoutConnection);
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
mHandwritingInitiator.onInputConnectionClosed(mTestView1);
@@ -664,6 +724,7 @@
@Test
public void onInputConnectionClosed_whenAutoHandwritingIsDisabled() {
+ assumeFalse(mInitiateWithoutConnection);
View view = new View(mContext);
view.setAutoHandwritingEnabled(false);
mHandwritingInitiator.onInputConnectionCreated(view);
@@ -674,6 +735,7 @@
@Test
public void onInputConnectionCreated_inputConnectionRestarted() {
+ assumeFalse(mInitiateWithoutConnection);
// When IMM restarts input connection, View#onInputConnectionCreatedInternal might be
// called before View#onInputConnectionClosedInternal. As a result, we need to handle the
// case where "one view "2 InputConnections".
diff --git a/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java b/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java
new file mode 100644
index 0000000..8bdf4c6
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/os/BackgroundThreadTest.java
@@ -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.internal.os;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.ConditionVariable;
+import android.os.Handler;
+import android.os.Looper;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+
+public class BackgroundThreadTest {
+
+ @Rule
+ public final RavenwoodRule mRavenwood =
+ new RavenwoodRule.Builder().setProvideMainThread(true).build();
+
+ @Test
+ public void test_get() {
+ BackgroundThread thread = BackgroundThread.get();
+ assertThat(thread.getLooper()).isNotEqualTo(Looper.getMainLooper());
+ }
+
+ @Test
+ public void test_getHandler() {
+ Handler handler = BackgroundThread.getHandler();
+ ConditionVariable done = new ConditionVariable();
+ handler.post(done::open);
+ boolean success = done.block(5000);
+ assertThat(success).isTrue();
+ }
+
+ @Test
+ public void test_getExecutor() {
+ Executor executor = BackgroundThread.getExecutor();
+ ConditionVariable done = new ConditionVariable();
+ executor.execute(done::open);
+ boolean success = done.block(5000);
+ assertThat(success).isTrue();
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
index e064e74..78ef92b 100644
--- a/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/LongMultiStateCounterTest.java
@@ -22,7 +22,6 @@
import android.os.BadParcelableException;
import android.os.Parcel;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
@@ -34,7 +33,6 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
-@IgnoreUnderRavenwood(blockedBy = LongMultiStateCounterTest.class)
public class LongMultiStateCounterTest {
@Rule
public final RavenwoodRule mRavenwood = new RavenwoodRule();
diff --git a/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java b/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java
index ae2ef0cb..9c337d7 100644
--- a/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java
+++ b/core/tests/coretests/src/com/android/internal/power/EnergyConsumerStatsTest.java
@@ -34,7 +34,6 @@
import static org.junit.Assert.assertTrue;
import android.os.Parcel;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.ravenwood.RavenwoodRule;
import android.view.Display;
@@ -49,7 +48,6 @@
* Test class for {@link EnergyConsumerStats}.
*/
@SmallTest
-@IgnoreUnderRavenwood(reason = "Needs kernel support")
public class EnergyConsumerStatsTest {
@Rule
public final RavenwoodRule mRavenwood = new RavenwoodRule();
diff --git a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
index 84dd274..8d66cfc 100644
--- a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java
@@ -169,7 +169,8 @@
private WindowInsets insetsWith(Insets content, DisplayCutout cutout) {
return new WindowInsets(WindowInsets.createCompatTypeMap(content.toRect()), null, null,
- false, 0, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false);
+ false, 0, 0, cutout, null, null, null, WindowInsets.Type.systemBars(), false,
+ null, null, 0, 0);
}
private ViewGroup createViewGroupWithId(int id) {
diff --git a/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
index 08333ec..bf9221a 100644
--- a/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/NotificationOptimizedLinearLayoutComparisonTest.java
@@ -31,6 +31,7 @@
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.widget.LinearLayout;
+import android.widget.TextView;
import android.widget.flags.Flags;
import androidx.test.InstrumentationRegistry;
@@ -73,7 +74,7 @@
private static final int[] LAYOUT_PARAMS = {MATCH_PARENT, WRAP_CONTENT, 0, 50};
private static final int[] CHILD_WEIGHTS = {0, 1};
-
+ private static final int[] CHILD_MARGINS = {0, 10, -10};
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -84,35 +85,96 @@
mContext = InstrumentationRegistry.getTargetContext();
}
+
@Test
public void test() throws Throwable {
+ final List<View> controlChildren =
+ new ArrayList<>();
+ final List<View> testChildren =
+ new ArrayList<>();
+
+ final View controlChild1 = buildChildView();
+ final View controlChild2 = buildChildView();
+ controlChildren.add(controlChild1);
+ controlChildren.add(controlChild2);
+
+ final View testChild1 = buildChildView();
+ final View testChild2 = buildChildView();
+ testChildren.add(testChild1);
+ testChildren.add(testChild2);
+
+ final LinearLayout controlContainer = buildLayout(false, controlChildren);
+
+ final LinearLayout testContainer = buildLayout(true, testChildren);
+
+ final LinearLayout.LayoutParams firstChildLayoutParams = new LinearLayout.LayoutParams(0,
+ 0);
+ final LinearLayout.LayoutParams secondChildLayoutParams = new LinearLayout.LayoutParams(0,
+ 0);
+ controlChild1.setLayoutParams(firstChildLayoutParams);
+ controlChild2.setLayoutParams(secondChildLayoutParams);
+ testChild1.setLayoutParams(firstChildLayoutParams);
+ testChild2.setLayoutParams(secondChildLayoutParams);
+
for (int orientation : ORIENTATIONS) {
- for (int widthSpec : MEASURE_SPECS) {
- for (int heightSpec : MEASURE_SPECS) {
- for (int firstChildGravity : GRAVITIES) {
- for (int secondChildGravity : GRAVITIES) {
- for (int firstChildLayoutWidth : LAYOUT_PARAMS) {
- for (int firstChildLayoutHeight : LAYOUT_PARAMS) {
- for (int secondChildLayoutWidth : LAYOUT_PARAMS) {
- for (int secondChildLayoutHeight : LAYOUT_PARAMS) {
+ controlContainer.setOrientation(orientation);
+ testContainer.setOrientation(orientation);
+
+ for (int firstChildLayoutWidth : LAYOUT_PARAMS) {
+ firstChildLayoutParams.width = firstChildLayoutWidth;
+ for (int firstChildLayoutHeight : LAYOUT_PARAMS) {
+ firstChildLayoutParams.height = firstChildLayoutHeight;
+
+ for (int secondChildLayoutWidth : LAYOUT_PARAMS) {
+ secondChildLayoutParams.width = secondChildLayoutWidth;
+ for (int secondChildLayoutHeight : LAYOUT_PARAMS) {
+ secondChildLayoutParams.height = secondChildLayoutHeight;
+
+ for (int firstChildMargin : CHILD_MARGINS) {
+ firstChildLayoutParams.setMargins(firstChildMargin,
+ firstChildMargin, firstChildMargin, firstChildMargin);
+ for (int secondChildMargin : CHILD_MARGINS) {
+ secondChildLayoutParams.setMargins(secondChildMargin,
+ secondChildMargin, secondChildMargin,
+ secondChildMargin);
+
+ for (int firstChildGravity : GRAVITIES) {
+ firstChildLayoutParams.gravity = firstChildGravity;
+ for (int secondChildGravity : GRAVITIES) {
+ secondChildLayoutParams.gravity = secondChildGravity;
+
for (int firstChildWeight : CHILD_WEIGHTS) {
+ firstChildLayoutParams.weight = firstChildWeight;
for (int secondChildWeight : CHILD_WEIGHTS) {
- executeTest(/*testSpec =*/createTestSpec(
- orientation,
- widthSpec, heightSpec,
- firstChildLayoutWidth,
- firstChildLayoutHeight,
- secondChildLayoutWidth,
- secondChildLayoutHeight,
- firstChildGravity,
- secondChildGravity,
- firstChildWeight,
- secondChildWeight));
+ secondChildLayoutParams.weight =
+ secondChildWeight;
+
+ for (int widthSpec : MEASURE_SPECS) {
+ for (int heightSpec : MEASURE_SPECS) {
+ executeTest(controlContainer,
+ testContainer,
+ createTestSpec(
+ orientation,
+ widthSpec, heightSpec,
+ firstChildLayoutWidth,
+ firstChildLayoutHeight,
+ secondChildLayoutWidth,
+ secondChildLayoutHeight,
+ firstChildGravity,
+ secondChildGravity,
+ firstChildWeight,
+ secondChildWeight,
+ firstChildMargin,
+ secondChildMargin)
+ );
+ }
+ }
}
}
}
}
}
+
}
}
}
@@ -121,47 +183,8 @@
}
}
- private void executeTest(TestSpec testSpec) {
- // GIVEN
- final List<View> controlChildren =
- new ArrayList<>();
- final List<View> testChildren =
- new ArrayList<>();
-
- controlChildren.add(
- buildChildView(
- testSpec.mFirstChildLayoutWidth,
- testSpec.mFirstChildLayoutHeight,
- testSpec.mFirstChildGravity,
- testSpec.mFirstChildWeight));
- controlChildren.add(
- buildChildView(
- testSpec.mSecondChildLayoutWidth,
- testSpec.mSecondChildLayoutHeight,
- testSpec.mSecondChildGravity,
- testSpec.mSecondChildWeight));
-
- testChildren.add(
- buildChildView(
- testSpec.mFirstChildLayoutWidth,
- testSpec.mFirstChildLayoutHeight,
- testSpec.mFirstChildGravity,
- testSpec.mFirstChildWeight));
- testChildren.add(
- buildChildView(
- testSpec.mSecondChildLayoutWidth,
- testSpec.mSecondChildLayoutHeight,
- testSpec.mSecondChildGravity,
- testSpec.mSecondChildWeight));
-
- final LinearLayout controlContainer = buildLayout(false,
- testSpec.mOrientation,
- controlChildren);
-
- final LinearLayout testContainer = buildLayout(true,
- testSpec.mOrientation,
- testChildren);
-
+ private void executeTest(LinearLayout controlContainer, LinearLayout testContainer,
+ TestSpec testSpec) {
// WHEN
controlContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
testContainer.measure(testSpec.mWidthSpec, testSpec.mHeightSpec);
@@ -171,6 +194,7 @@
assertLayoutsEqual("Test Case:" + testSpec, controlContainer, testContainer);
}
+
private static class TestSpec {
private final int mOrientation;
private final int mWidthSpec;
@@ -183,6 +207,8 @@
private final int mSecondChildGravity;
private final int mFirstChildWeight;
private final int mSecondChildWeight;
+ private final int mFirstChildMargin;
+ private final int mSecondChildMargin;
TestSpec(
int orientation,
@@ -195,7 +221,9 @@
int firstChildGravity,
int secondChildGravity,
int firstChildWeight,
- int secondChildWeight) {
+ int secondChildWeight,
+ int firstChildMargin,
+ int secondChildMargin) {
mOrientation = orientation;
mWidthSpec = widthSpec;
mHeightSpec = heightSpec;
@@ -207,6 +235,8 @@
mSecondChildGravity = secondChildGravity;
mFirstChildWeight = firstChildWeight;
mSecondChildWeight = secondChildWeight;
+ mFirstChildMargin = firstChildMargin;
+ mSecondChildMargin = secondChildMargin;
}
@Override
@@ -223,6 +253,8 @@
+ ", mSecondChildGravity=" + mSecondChildGravity
+ ", mFirstChildWeight=" + mFirstChildWeight
+ ", mSecondChildWeight=" + mSecondChildWeight
+ + ", mFirstChildMargin=" + mFirstChildMargin
+ + ", mSecondChildMargin=" + mSecondChildMargin
+ '}';
}
@@ -246,15 +278,13 @@
}
}
- private LinearLayout buildLayout(boolean isNotificationOptimized,
- @LinearLayout.OrientationMode int orientation, List<View> children) {
+ private LinearLayout buildLayout(boolean isNotificationOptimized, List<View> children) {
final LinearLayout linearLayout;
if (isNotificationOptimized) {
linearLayout = new NotificationOptimizedLinearLayout(mContext);
} else {
linearLayout = new LinearLayout(mContext);
}
- linearLayout.setOrientation(orientation);
for (int i = 0; i < children.size(); i++) {
linearLayout.addView(children.get(i));
}
@@ -262,7 +292,8 @@
}
private void assertLayoutsEqual(String testCase, View controlView, View testView) {
- mExpect.withMessage("MeasuredWidths are not equal. Test Case:" + testCase)
+ mExpect.withMessage(
+ "MeasuredWidths are not equal. Test Case:" + testCase)
.that(testView.getMeasuredWidth()).isEqualTo(controlView.getMeasuredWidth());
mExpect.withMessage("MeasuredHeights are not equal. Test Case:" + testCase)
.that(testView.getMeasuredHeight()).isEqualTo(controlView.getMeasuredHeight());
@@ -286,23 +317,12 @@
}
}
- private static class TestView extends View {
- TestView(Context context) {
- super(context);
- }
-
- @Override
- public int getBaseline() {
- return 5;
- }
- }
-
-
private TestSpec createTestSpec(int orientation,
int widthSpec, int heightSpec,
int firstChildLayoutWidth, int firstChildLayoutHeight, int secondChildLayoutWidth,
int secondChildLayoutHeight, int firstChildGravity, int secondChildGravity,
- int firstChildWeight, int secondChildWeight) {
+ int firstChildWeight, int secondChildWeight, int firstChildMargin,
+ int secondChildMargin) {
return new TestSpec(
orientation,
@@ -314,16 +334,16 @@
firstChildGravity,
secondChildGravity,
firstChildWeight,
- secondChildWeight);
+ secondChildWeight,
+ firstChildMargin,
+ secondChildMargin);
}
- private View buildChildView(int childLayoutWidth, int childLayoutHeight,
- int childGravity, int childWeight) {
- final View childView = new TestView(mContext);
- // Set desired size using LayoutParams
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(childLayoutWidth,
- childLayoutHeight, childWeight);
- params.gravity = childGravity;
+ private View buildChildView() {
+ final View childView = new TextView(mContext);
+ // this is initial value. We are going to mutate this layout params during the test.
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(MATCH_PARENT,
+ WRAP_CONTENT);
childView.setLayoutParams(params);
return childView;
}
diff --git a/core/tests/devicestatetests/Android.bp b/core/tests/devicestatetests/Android.bp
index 7748de5..60848b3 100644
--- a/core/tests/devicestatetests/Android.bp
+++ b/core/tests/devicestatetests/Android.bp
@@ -29,6 +29,8 @@
"androidx.test.rules",
"frameworks-base-testutils",
"mockito-target-minus-junit4",
+ "platform-test-annotations",
+ "testng",
],
libs: ["android.test.runner"],
platform_apis: true,
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
similarity index 93%
rename from services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java
rename to core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
index d54524e..396d403 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.server.devicestate;
+package android.hardware.devicestate;
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
@@ -25,18 +25,17 @@
import android.platform.test.annotations.Presubmit;
-import androidx.test.runner.AndroidJUnit4;
-
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/**
- * Unit tests for {@link DeviceState}.
+ * Unit tests for {@link android.hardware.devicestate.DeviceState}.
* <p/>
* Run with <code>atest DeviceStateTest</code>.
*/
@Presubmit
-@RunWith(AndroidJUnit4.class)
+@RunWith(JUnit4.class)
public final class DeviceStateTest {
@Test
public void testConstruct() {
diff --git a/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java b/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java
index 7723d58..f91172d 100644
--- a/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java
+++ b/core/tests/utiltests/src/com/android/internal/util/FastXmlSerializerTest.java
@@ -45,7 +45,6 @@
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
-@IgnoreUnderRavenwood(blockedBy = Xml.class)
public class FastXmlSerializerTest {
private static final String TAG = "FastXmlSerializerTest";
@@ -146,6 +145,7 @@
@Test
@LargeTest
+ @IgnoreUnderRavenwood(reason = "Long test runtime")
public void testAllCharacters() throws Exception {
boolean ok = true;
for (int i = 0; i < 0xffff; i++) {
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 0baaff0..fdb5208 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -575,6 +575,11 @@
<permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
<!-- Permissions required for CTS test - CtsContactKeysProviderPrivilegedApp -->
<permission name="android.permission.WRITE_VERIFICATION_STATE_E2EE_CONTACT_KEYS"/>
+ <!-- Permission required for CTS test BlockedNumberContractTest -->
+ <permission name="android.permission.WRITE_BLOCKED_NUMBERS" />
+ <permission name="android.permission.READ_BLOCKED_NUMBERS" />
+ <!-- Permission required for CTS test - PackageManagerTest -->
+ <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/>
</privapp-permissions>
<privapp-permissions package="com.android.statementservice">
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index ea7c6ed..5825bbf 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -165,6 +165,25 @@
}
@Test
+ fun testGetRestingPosition_afterBoundsChange() {
+ positioner.update(defaultDeviceConfig.copy(isLargeScreen = true,
+ windowBounds = Rect(0, 0, 2000, 1600)))
+
+ // Set the resting position to the right side
+ var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+ val restingPosition = PointF(allowableStackRegion.right, allowableStackRegion.centerY())
+ positioner.restingPosition = restingPosition
+
+ // Now make the device smaller
+ positioner.update(defaultDeviceConfig.copy(isLargeScreen = false,
+ windowBounds = Rect(0, 0, 1000, 1600)))
+
+ // Check the resting position is on the correct side
+ allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
+ assertThat(positioner.restingPosition.x).isEqualTo(allowableStackRegion.right)
+ }
+
+ @Test
fun testHasUserModifiedDefaultPosition_false() {
positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true))
assertThat(positioner.hasUserModifiedDefaultPosition()).isFalse()
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index c03b6f8..cda29c9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -120,6 +120,13 @@
@VisibleForTesting
public void updateInternal(int rotation, Insets insets, Rect bounds) {
+ BubbleStackView.RelativeStackPosition prevStackPosition = null;
+ if (mRestingStackPosition != null && mScreenRect != null && !mScreenRect.equals(bounds)) {
+ // Save the resting position as a relative position with the previous bounds, at the
+ // end of the update we'll restore it based on the new bounds.
+ prevStackPosition = new BubbleStackView.RelativeStackPosition(getRestingPosition(),
+ getAllowableStackPositionRegion(1));
+ }
mRotation = rotation;
mInsets = insets;
@@ -182,6 +189,12 @@
R.dimen.bubbles_flyout_min_width_large_screen);
mMaxBubbles = calculateMaxBubbles();
+
+ if (prevStackPosition != null) {
+ // Get the new resting position based on the updated values
+ mRestingStackPosition = prevStackPosition.getAbsolutePositionInRegion(
+ getAllowableStackPositionRegion(1));
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index ead5ad2..bd9d89c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -64,6 +64,7 @@
import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
import com.android.wm.shell.draganddrop.DragAndDropController;
+import com.android.wm.shell.draganddrop.UnhandledDragController;
import com.android.wm.shell.freeform.FreeformComponents;
import com.android.wm.shell.freeform.FreeformTaskListener;
import com.android.wm.shell.freeform.FreeformTaskTransitionHandler;
@@ -558,6 +559,14 @@
@WMSingleton
@Provides
+ static UnhandledDragController provideUnhandledDragController(
+ IWindowManager wmService,
+ @ShellMainThread ShellExecutor mainExecutor) {
+ return new UnhandledDragController(wmService, mainExecutor);
+ }
+
+ @WMSingleton
+ @Provides
static DragAndDropController provideDragAndDropController(Context context,
ShellInit shellInit,
ShellController shellController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
index 605600f..ba08b09 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java
@@ -143,7 +143,7 @@
final SurfaceControl leash = change.getLeash();
final Rect startBounds = change.getStartAbsBounds();
- startT.setPosition(leash, startBounds.left, startBounds.right)
+ startT.setPosition(leash, startBounds.left, startBounds.top)
.setWindowCrop(leash, startBounds.width(), startBounds.height())
.show(leash);
mDesktopModeWindowDecoration.showResizeVeil(startT, startBounds);
@@ -154,7 +154,7 @@
SurfaceControl.Transaction t = mTransactionSupplier.get();
animator.addUpdateListener(animation -> {
final Rect animationValue = (Rect) animator.getAnimatedValue();
- t.setPosition(leash, animationValue.left, animationValue.right)
+ t.setPosition(leash, animationValue.left, animationValue.top)
.setWindowCrop(leash, animationValue.width(), animationValue.height())
.show(leash);
mDesktopModeWindowDecoration.updateResizeVeil(t, animationValue);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt
new file mode 100644
index 0000000..ccf48d0
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/UnhandledDragController.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.wm.shell.draganddrop
+
+import android.os.RemoteException
+import android.util.Log
+import android.view.DragEvent
+import android.view.IWindowManager
+import android.window.IUnhandledDragCallback
+import android.window.IUnhandledDragListener
+import androidx.annotation.VisibleForTesting
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import java.util.function.Consumer
+
+/**
+ * Manages the listener and callbacks for unhandled global drags.
+ */
+class UnhandledDragController(
+ val wmService: IWindowManager,
+ mainExecutor: ShellExecutor
+) {
+ private var callback: UnhandledDragAndDropCallback? = null
+
+ private val unhandledDragListener: IUnhandledDragListener =
+ object : IUnhandledDragListener.Stub() {
+ override fun onUnhandledDrop(event: DragEvent, callback: IUnhandledDragCallback) {
+ mainExecutor.execute() {
+ this@UnhandledDragController.onUnhandledDrop(event, callback)
+ }
+ }
+ }
+
+ /**
+ * Listener called when an unhandled drag is started.
+ */
+ interface UnhandledDragAndDropCallback {
+ /**
+ * Called when a global drag is unhandled (ie. dropped outside of all visible windows, or
+ * dropped on a window that does not want to handle it).
+ *
+ * The implementer _must_ call onFinishedCallback, and if it consumes the drop, then it is
+ * also responsible for releasing up the drag surface provided via the drag event.
+ */
+ fun onUnhandledDrop(dragEvent: DragEvent, onFinishedCallback: Consumer<Boolean>) {}
+ }
+
+ /**
+ * Sets a listener for callbacks when an unhandled drag happens.
+ */
+ fun setListener(listener: UnhandledDragAndDropCallback?) {
+ val updateWm = (callback == null && listener != null)
+ || (callback != null && listener == null)
+ callback = listener
+ if (updateWm) {
+ try {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+ "%s unhandled drag listener",
+ if (callback != null) "Registering" else "Unregistering")
+ wmService.setUnhandledDragListener(
+ if (callback != null) unhandledDragListener else null)
+ } catch (e: RemoteException) {
+ Log.e(TAG, "Failed to set unhandled drag listener")
+ }
+ }
+ }
+
+ @VisibleForTesting
+ fun onUnhandledDrop(dragEvent: DragEvent, wmCallback: IUnhandledDragCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+ "onUnhandledDrop: %s", dragEvent)
+ if (callback == null) {
+ wmCallback.notifyUnhandledDropComplete(false)
+ }
+
+ callback?.onUnhandledDrop(dragEvent) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP,
+ "Notifying onUnhandledDrop complete: %b", it)
+ wmCallback.notifyUnhandledDropComplete(it)
+ }
+ }
+
+ companion object {
+ private val TAG = UnhandledDragController::class.java.simpleName
+ }
+}
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 e5045ae..70b2f21 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
@@ -2959,13 +2959,17 @@
}
public void goToFullscreenFromSplit() {
- boolean leftOrTop;
- if (mSideStage.isFocused()) {
- leftOrTop = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+ // If main stage is focused, toEnd = true if
+ // mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT. Otherwise toEnd = false
+ // If side stage is focused, toEnd = true if
+ // mSideStagePosition = SPLIT_POSITION_TOP_OR_LEFT. Otherwise toEnd = false
+ final boolean toEnd;
+ if (mMainStage.isFocused()) {
+ toEnd = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
} else {
- leftOrTop = (mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT);
+ toEnd = (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
}
- mSplitLayout.flingDividerToDismiss(!leftOrTop, EXIT_REASON_FULLSCREEN_SHORTCUT);
+ mSplitLayout.flingDividerToDismiss(toEnd, EXIT_REASON_FULLSCREEN_SHORTCUT);
}
/** Move the specified task to fullscreen, regardless of focus state. */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
index 93d7636..196e04e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java
@@ -480,7 +480,7 @@
WindowContainerTransaction wct = new WindowContainerTransaction();
if (mCaptionInsets != null) {
wct.addInsetsSource(mTaskToken, mCaptionInsetsOwner, 0,
- WindowInsets.Type.captionBar(), mCaptionInsets);
+ WindowInsets.Type.captionBar(), mCaptionInsets, null /* boundingRects */);
} else {
wct.removeInsetsSource(mTaskToken, mCaptionInsetsOwner, 0,
WindowInsets.Type.captionBar());
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index afe837e..35d5940 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -298,10 +298,11 @@
mCaptionInsetsRect.bottom =
mCaptionInsetsRect.top + outResult.mCaptionHeight;
wct.addInsetsSource(mTaskInfo.token,
- mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect);
+ mOwner, 0 /* index */, WindowInsets.Type.captionBar(), mCaptionInsetsRect,
+ null /* boundingRects */);
wct.addInsetsSource(mTaskInfo.token,
mOwner, 0 /* index */, WindowInsets.Type.mandatorySystemGestures(),
- mCaptionInsetsRect);
+ mCaptionInsetsRect, null /* boundingRects */);
} else {
wct.removeInsetsSource(mTaskInfo.token, mOwner, 0 /* index */,
WindowInsets.Type.captionBar());
@@ -546,7 +547,7 @@
final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId);
final Rect captionInsets = new Rect(0, 0, 0, captionHeight);
wct.addInsetsSource(mTaskInfo.token, mOwner, 0 /* index */, WindowInsets.Type.captionBar(),
- captionInsets);
+ captionInsets, null /* boundingRects */);
}
static class RelayoutParams {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt
new file mode 100644
index 0000000..522f052
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/UnhandledDragControllerTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.wm.shell.draganddrop
+
+import android.os.RemoteException
+import android.view.DragEvent
+import android.view.DragEvent.ACTION_DROP
+import android.view.IWindowManager
+import android.view.SurfaceControl
+import android.window.IUnhandledDragCallback
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.draganddrop.UnhandledDragController.UnhandledDragAndDropCallback
+import java.util.function.Consumer
+import junit.framework.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Tests for the unhandled drag controller.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UnhandledDragControllerTest : ShellTestCase() {
+ @Mock
+ private lateinit var mIWindowManager: IWindowManager
+
+ @Mock
+ private lateinit var mMainExecutor: ShellExecutor
+
+ private lateinit var mController: UnhandledDragController
+
+ @Before
+ @Throws(RemoteException::class)
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ mController = UnhandledDragController(mIWindowManager, mMainExecutor)
+ }
+
+ @Test
+ fun setListener_registersUnregistersWithWM() {
+ mController.setListener(object : UnhandledDragAndDropCallback {})
+ mController.setListener(object : UnhandledDragAndDropCallback {})
+ mController.setListener(object : UnhandledDragAndDropCallback {})
+ verify(mIWindowManager, Mockito.times(1))
+ .setUnhandledDragListener(ArgumentMatchers.any())
+
+ reset(mIWindowManager)
+ mController.setListener(null)
+ mController.setListener(null)
+ mController.setListener(null)
+ verify(mIWindowManager, Mockito.times(1))
+ .setUnhandledDragListener(ArgumentMatchers.isNull())
+ }
+
+ @Test
+ fun onUnhandledDrop_noListener_expectNotifyUnhandled() {
+ // Simulate an unhandled drop
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
+ null, null, false)
+ val wmCallback = mock(IUnhandledDragCallback::class.java)
+ mController.onUnhandledDrop(dropEvent, wmCallback)
+
+ verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(false))
+ }
+
+ @Test
+ fun onUnhandledDrop_withListener_expectNotifyHandled() {
+ val lastDragEvent = arrayOfNulls<DragEvent>(1)
+
+ // Set a listener to listen for unhandled drops
+ mController.setListener(object : UnhandledDragAndDropCallback {
+ override fun onUnhandledDrop(dragEvent: DragEvent,
+ onFinishedCallback: Consumer<Boolean>) {
+ lastDragEvent[0] = dragEvent
+ onFinishedCallback.accept(true)
+ dragEvent.dragSurface.release()
+ }
+ })
+
+ // Simulate an unhandled drop
+ val dragSurface = mock(SurfaceControl::class.java)
+ val dropEvent = DragEvent.obtain(ACTION_DROP, 0f, 0f, 0f, 0f, null, null, null,
+ dragSurface, null, false)
+ val wmCallback = mock(IUnhandledDragCallback::class.java)
+ mController.onUnhandledDrop(dropEvent, wmCallback)
+
+ verify(wmCallback).notifyUnhandledDropComplete(ArgumentMatchers.eq(true))
+ verify(dragSurface).release()
+ assertEquals(lastDragEvent.get(0), dropEvent)
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 7b53f70..228b25c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -259,7 +259,8 @@
any(),
eq(0 /* index */),
eq(WindowInsets.Type.captionBar()),
- eq(new Rect(100, 300, 400, 364)));
+ eq(new Rect(100, 300, 400, 364)),
+ any());
}
verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
@@ -569,9 +570,9 @@
windowDecor.relayout(taskInfo);
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(captionBar()), any());
+ eq(0) /* index */, eq(captionBar()), any(), any());
verify(mMockWindowContainerTransaction).addInsetsSource(eq(taskInfo.token), any(),
- eq(0) /* index */, eq(mandatorySystemGestures()), any());
+ eq(0) /* index */, eq(mandatorySystemGestures()), any(), any());
}
@Test
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index 8748dab..46f636e 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -117,6 +117,10 @@
return true;
}
+void AssetManager2::PresetApkAssets(ApkAssetsList apk_assets) {
+ BuildDynamicRefTable(apk_assets);
+}
+
bool AssetManager2::SetApkAssets(std::initializer_list<ApkAssetsPtr> apk_assets,
bool invalidate_caches) {
return SetApkAssets(ApkAssetsList(apk_assets.begin(), apk_assets.size()), invalidate_caches);
@@ -432,13 +436,18 @@
return false;
}
-void AssetManager2::SetConfigurations(std::vector<ResTable_config> configurations) {
+void AssetManager2::SetConfigurations(std::vector<ResTable_config> configurations,
+ bool force_refresh) {
int diff = 0;
- if (configurations_.size() != configurations.size()) {
+ if (force_refresh) {
diff = -1;
} else {
- for (int i = 0; i < configurations_.size(); i++) {
- diff |= configurations_[i].diff(configurations[i]);
+ if (configurations_.size() != configurations.size()) {
+ diff = -1;
+ } else {
+ for (int i = 0; i < configurations_.size(); i++) {
+ diff |= configurations_[i].diff(configurations[i]);
+ }
}
}
configurations_ = std::move(configurations);
@@ -775,8 +784,7 @@
bool has_locale = false;
if (result->config.locale == 0) {
if (default_locale_ != 0) {
- ResTable_config conf;
- conf.locale = default_locale_;
+ ResTable_config conf = {.locale = default_locale_};
// Since we know conf has a locale and only a locale, match will tell us if that locale
// matches
has_locale = conf.match(config);
diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h
index d9ff35b..17a8ba6 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -124,6 +124,9 @@
// new resource IDs.
bool SetApkAssets(ApkAssetsList apk_assets, bool invalidate_caches = true);
bool SetApkAssets(std::initializer_list<ApkAssetsPtr> apk_assets, bool invalidate_caches = true);
+ // This one is an optimization - it skips all calculations for applying the currently set
+ // configuration, expecting a configuration update later with a forced refresh.
+ void PresetApkAssets(ApkAssetsList apk_assets);
const ApkAssetsPtr& GetApkAssets(ApkAssetsCookie cookie) const;
int GetApkAssetsCount() const {
@@ -156,7 +159,7 @@
// Sets/resets the configuration for this AssetManager. This will cause all
// caches that are related to the configuration change to be invalidated.
- void SetConfigurations(std::vector<ResTable_config> configurations);
+ void SetConfigurations(std::vector<ResTable_config> configurations, bool force_refresh = false);
inline const std::vector<ResTable_config>& GetConfigurations() const {
return configurations_;
diff --git a/libs/input/SpriteIcon.h b/libs/input/SpriteIcon.h
index 9e6cc81..0939af4 100644
--- a/libs/input/SpriteIcon.h
+++ b/libs/input/SpriteIcon.h
@@ -27,33 +27,20 @@
* Icon that a sprite displays, including its hotspot.
*/
struct SpriteIcon {
- inline SpriteIcon() : style(PointerIconStyle::TYPE_NULL), hotSpotX(0), hotSpotY(0) {}
- inline SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX,
- float hotSpotY, bool drawNativeDropShadow)
+ explicit SpriteIcon() = default;
+ explicit SpriteIcon(const graphics::Bitmap& bitmap, PointerIconStyle style, float hotSpotX,
+ float hotSpotY, bool drawNativeDropShadow)
: bitmap(bitmap),
style(style),
hotSpotX(hotSpotX),
hotSpotY(hotSpotY),
drawNativeDropShadow(drawNativeDropShadow) {}
- graphics::Bitmap bitmap;
- PointerIconStyle style;
- float hotSpotX;
- float hotSpotY;
- bool drawNativeDropShadow;
-
- inline SpriteIcon copy() const {
- return SpriteIcon(bitmap.copy(ANDROID_BITMAP_FORMAT_RGBA_8888), style, hotSpotX, hotSpotY,
- drawNativeDropShadow);
- }
-
- inline void reset() {
- bitmap.reset();
- style = PointerIconStyle::TYPE_NULL;
- hotSpotX = 0;
- hotSpotY = 0;
- drawNativeDropShadow = false;
- }
+ graphics::Bitmap bitmap{};
+ PointerIconStyle style{PointerIconStyle::TYPE_NULL};
+ float hotSpotX{};
+ float hotSpotY{};
+ bool drawNativeDropShadow{false};
inline bool isValid() const { return bitmap.isValid() && !bitmap.isEmpty(); }
diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl
index 04e99ea..7727078 100644
--- a/media/java/android/media/IMediaRouterService.aidl
+++ b/media/java/android/media/IMediaRouterService.aidl
@@ -27,7 +27,6 @@
import android.media.RoutingSessionInfo;
import android.os.Bundle;
import android.os.UserHandle;
-
/**
* {@hide}
*/
@@ -56,6 +55,7 @@
void registerRouter2(IMediaRouter2 router, String packageName);
void unregisterRouter2(IMediaRouter2 router);
+ void updateScanningStateWithRouter2(IMediaRouter2 router, @JavaPassthrough(annotation="@android.media.MediaRouter2.ScanningState") int scanningState);
void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
in RouteDiscoveryPreference preference);
void setRouteListingPreference(IMediaRouter2 router,
@@ -81,8 +81,7 @@
void unregisterManager(IMediaRouter2Manager manager);
void setRouteVolumeWithManager(IMediaRouter2Manager manager, int requestId,
in MediaRoute2Info route, int volume);
- void startScan(IMediaRouter2Manager manager);
- void stopScan(IMediaRouter2Manager manager);
+ void updateScanningState(IMediaRouter2Manager manager, @JavaPassthrough(annotation="@android.media.MediaRouter2.ScanningState") int scanningState);
void requestCreateSessionWithManager(IMediaRouter2Manager manager, int requestId,
in RoutingSessionInfo oldSession, in @nullable MediaRoute2Info route,
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 425db06..7fa3ed6 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -19,11 +19,14 @@
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
import static com.android.media.flags.Flags.FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2;
+import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
+import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -42,9 +45,12 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -73,6 +79,48 @@
// Not only MediaRouter2, but also to service / manager / provider.
// TODO: ensure thread-safe and document it
public final class MediaRouter2 {
+
+ /**
+ * The state of a router not requesting route scanning.
+ *
+ * @hide
+ */
+ public static final int SCANNING_STATE_NOT_SCANNING = 0;
+
+ /**
+ * The state of a router requesting scanning only while the user interacts with its owner app.
+ *
+ * <p>The device's screen must be on and the app must be in the foreground to trigger scanning
+ * under this state.
+ *
+ * @hide
+ */
+ public static final int SCANNING_STATE_WHILE_INTERACTIVE = 1;
+
+ /**
+ * The state of a router requesting unrestricted scanning.
+ *
+ * <p>This state triggers scanning regardless of the restrictions required for {@link
+ * #SCANNING_STATE_WHILE_INTERACTIVE}.
+ *
+ * <p>Routers requesting unrestricted scanning must hold {@link
+ * Manifest.permission#MEDIA_ROUTING_CONTROL}.
+ *
+ * @hide
+ */
+ public static final int SCANNING_STATE_SCANNING_FULL = 2;
+
+ /** @hide */
+ @IntDef(
+ prefix = "SCANNING_STATE",
+ value = {
+ SCANNING_STATE_NOT_SCANNING,
+ SCANNING_STATE_WHILE_INTERACTIVE,
+ SCANNING_STATE_SCANNING_FULL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ScanningState {}
+
private static final String TAG = "MR2";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final Object sSystemRouterLock = new Object();
@@ -123,6 +171,13 @@
@GuardedBy("mLock")
private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private int mScreenOffScanRequestCount = 0;
+
+ @GuardedBy("mLock")
+ private int mScreenOnScanRequestCount = 0;
+
+ private final SparseArray<ScanRequest> mScanRequestsMap = new SparseArray<>();
private final AtomicInteger mNextRequestId = new AtomicInteger(1);
private final Handler mHandler;
@@ -335,6 +390,100 @@
mImpl.stopScan();
}
+ /**
+ * Requests the system to actively scan for routes based on the router's {@link
+ * RouteDiscoveryPreference route discovery preference}.
+ *
+ * <p>You must call {@link #cancelScanRequest(ScanToken)} promptly to preserve system resources
+ * like battery. Avoid scanning unless there is clear intention from the user to start routing
+ * their media.
+ *
+ * <p>{@code scanRequest} specifies relevant scanning options, like whether the system should
+ * scan with the screen off. Screen off scanning requires {@link
+ * Manifest.permission#MEDIA_ROUTING_CONTROL}
+ *
+ * <p>Proxy routers use the registered {@link RouteDiscoveryPreference} of their target routers.
+ *
+ * @return A unique {@link ScanToken} that identifies the scan request.
+ */
+ @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
+ @NonNull
+ public ScanToken requestScan(@NonNull ScanRequest scanRequest) {
+ Objects.requireNonNull(scanRequest, "scanRequest must not be null.");
+ ScanToken token = new ScanToken(mNextRequestId.getAndIncrement());
+
+ synchronized (mLock) {
+ boolean shouldUpdate =
+ mScreenOffScanRequestCount == 0
+ && (scanRequest.isScreenOffScan() || mScreenOnScanRequestCount == 0);
+
+ if (shouldUpdate) {
+ try {
+ mImpl.updateScanningState(
+ scanRequest.isScreenOffScan()
+ ? SCANNING_STATE_SCANNING_FULL
+ : SCANNING_STATE_WHILE_INTERACTIVE);
+
+ if (scanRequest.isScreenOffScan()) {
+ mScreenOffScanRequestCount++;
+ } else {
+ mScreenOnScanRequestCount++;
+ }
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ mScanRequestsMap.put(token.mId, scanRequest);
+ return token;
+ }
+ }
+
+ /**
+ * Releases the active scan request linked to the provided {@link ScanToken}.
+ *
+ * @see #requestScan(ScanRequest)
+ * @param token {@link ScanToken} of the {@link ScanRequest} to release.
+ * @throws IllegalArgumentException if the token does not match any active scan request.
+ */
+ @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
+ public void cancelScanRequest(@NonNull ScanToken token) {
+ Objects.requireNonNull(token, "token must not be null");
+
+ synchronized (mLock) {
+ ScanRequest request = mScanRequestsMap.get(token.mId);
+
+ if (request == null) {
+ throw new IllegalArgumentException(
+ "The token does not match any active scan request");
+ }
+
+ boolean shouldUpdate =
+ mScreenOffScanRequestCount == 1
+ && (request.isScreenOffScan() || mScreenOnScanRequestCount == 1);
+
+ if (shouldUpdate) {
+ try {
+ if (request.isScreenOffScan() && mScreenOnScanRequestCount == 0) {
+ mImpl.updateScanningState(SCANNING_STATE_NOT_SCANNING);
+ } else {
+ mImpl.updateScanningState(SCANNING_STATE_WHILE_INTERACTIVE);
+ }
+
+ if (request.isScreenOffScan()) {
+ mScreenOffScanRequestCount--;
+ } else {
+ mScreenOnScanRequestCount--;
+ }
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+
+ mScanRequestsMap.remove(token.mId);
+ }
+ }
+
private MediaRouter2(Context appContext) {
mContext = appContext;
mMediaRouterService =
@@ -1429,6 +1578,78 @@
}
/**
+ * Represents an active scan request registered in the system.
+ *
+ * <p>See {@link #requestScan(ScanRequest)} for more information.
+ */
+ @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
+ public static final class ScanToken {
+ private final int mId;
+
+ private ScanToken(int id) {
+ mId = id;
+ }
+ }
+
+ /**
+ * Represents a set of parameters for scanning requests.
+ *
+ * <p>See {@link #requestScan(ScanRequest)} for more details.
+ */
+ @FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
+ public static final class ScanRequest {
+ private final boolean mIsScreenOffScan;
+
+ private ScanRequest(boolean isScreenOffScan) {
+ mIsScreenOffScan = isScreenOffScan;
+ }
+
+ /**
+ * Returns whether the scan request corresponds to a screen-off scan.
+ *
+ * @see #requestScan(ScanRequest)
+ */
+ public boolean isScreenOffScan() {
+ return mIsScreenOffScan;
+ }
+
+ /**
+ * Builder class for {@link ScanRequest}.
+ *
+ * @see #requestScan(ScanRequest)
+ */
+ public static final class Builder {
+ boolean mIsScreenOffScan;
+
+ /**
+ * Creates a builder for a {@link ScanRequest} instance.
+ *
+ * @see #requestScan(ScanRequest)
+ */
+ public Builder() {}
+
+ /**
+ * Sets whether the app is requesting to scan even while the screen is off, bypassing
+ * default scanning restrictions. Only companion apps holding {@link
+ * Manifest.permission#MEDIA_ROUTING_CONTROL} should set this to {@code true}.
+ *
+ * @see #requestScan(ScanRequest)
+ */
+ @NonNull
+ public Builder setScreenOffScan(boolean isScreenOffScan) {
+ mIsScreenOffScan = isScreenOffScan;
+ return this;
+ }
+
+ /** Returns a new {@link ScanRequest} instance. */
+ @NonNull
+ public ScanRequest build() {
+ return new ScanRequest(mIsScreenOffScan);
+ }
+ }
+ }
+
+ /**
* A class to control media routing session in media route provider. For example,
* selecting/deselecting/transferring to routes of a session can be done through this. Instances
* are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is
@@ -1532,8 +1753,9 @@
/**
* Returns the unmodifiable list of transferable routes for the session.
*
- * @hide
+ * @see RoutingSessionInfo#getTransferableRoutes()
*/
+ @FlaggedApi(FLAG_ENABLE_GET_TRANSFERABLE_ROUTES)
@NonNull
public List<MediaRoute2Info> getTransferableRoutes() {
List<String> transferableRoutes;
@@ -2092,6 +2314,9 @@
* ProxyMediaRouter2Impl proxy} {@link MediaRouter2} instances.
*/
private interface MediaRouter2Impl {
+
+ void updateScanningState(@ScanningState int scanningState) throws RemoteException;
+
void startScan();
void stopScan();
@@ -2195,11 +2420,17 @@
}
@Override
+ public void updateScanningState(int scanningState) throws RemoteException {
+ mMediaRouterService.updateScanningState(mClient, scanningState);
+ }
+
+ @Override
public void startScan() {
if (!mIsScanning.getAndSet(true)) {
if (mScanRequestCount.getAndIncrement() == 0) {
try {
- mMediaRouterService.startScan(mClient);
+ mMediaRouterService.updateScanningState(
+ mClient, SCANNING_STATE_WHILE_INTERACTIVE);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -2221,7 +2452,8 @@
})
== 0) {
try {
- mMediaRouterService.stopScan(mClient);
+ mMediaRouterService.updateScanningState(
+ mClient, SCANNING_STATE_NOT_SCANNING);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -3041,6 +3273,18 @@
// Do nothing.
}
+ @Override
+ @GuardedBy("mLock")
+ public void updateScanningState(int scanningState) throws RemoteException {
+ if (scanningState != SCANNING_STATE_NOT_SCANNING) {
+ registerRouterStubIfNeededLocked();
+ }
+ mMediaRouterService.updateScanningStateWithRouter2(mStub, scanningState);
+ if (scanningState == SCANNING_STATE_NOT_SCANNING) {
+ unregisterRouterStubIfNeededLocked(/* isScanningStopping */ true);
+ }
+ }
+
/**
* Returns {@code null}. The client package name is only associated to proxy {@link
* MediaRouter2} instances.
@@ -3103,7 +3347,7 @@
mStub, mDiscoveryPreference);
}
- unregisterRouterStubIfNeededLocked();
+ unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);
} catch (RemoteException ex) {
Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
@@ -3313,7 +3557,7 @@
}
try {
- unregisterRouterStubIfNeededLocked();
+ unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
@@ -3331,10 +3575,12 @@
}
@GuardedBy("mLock")
- private void unregisterRouterStubIfNeededLocked() throws RemoteException {
+ private void unregisterRouterStubIfNeededLocked(boolean isScanningStopping)
+ throws RemoteException {
if (mStub != null
&& mRouteCallbackRecords.isEmpty()
- && mNonSystemRoutingControllers.isEmpty()) {
+ && mNonSystemRoutingControllers.isEmpty()
+ && (mScanRequestsMap.size() == 0 || isScanningStopping)) {
mMediaRouterService.unregisterRouter2(mStub);
mStub = null;
}
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 06c0996..488d544 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -16,6 +16,9 @@
package android.media;
+import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING;
+import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
+
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import android.Manifest;
@@ -174,7 +177,7 @@
public void registerScanRequest() {
if (mScanRequestCount.getAndIncrement() == 0) {
try {
- mMediaRouterService.startScan(mClient);
+ mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_WHILE_INTERACTIVE);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -201,7 +204,7 @@
})
== 0) {
try {
- mMediaRouterService.stopScan(mClient);
+ mMediaRouterService.updateScanningState(mClient, SCANNING_STATE_NOT_SCANNING);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
diff --git a/media/java/android/media/flags/media_better_together.aconfig b/media/java/android/media/flags/media_better_together.aconfig
index df9ecdc..8dba040 100644
--- a/media/java/android/media/flags/media_better_together.aconfig
+++ b/media/java/android/media/flags/media_better_together.aconfig
@@ -85,8 +85,22 @@
}
flag {
+ name: "enable_get_transferable_routes"
+ namespace: "media_solutions"
+ description: "Exposes RoutingController#getTransferableRoutes() (previously hidden) to the public API."
+ bug: "323154573"
+}
+
+flag {
name: "enable_prevention_of_keep_alive_route_providers"
namespace: "media_solutions"
description: "Enables mechanisms to prevent route providers from keeping malicious apps alive."
bug: "263520343"
}
+
+flag {
+ name: "enable_screen_off_scanning"
+ namespace: "media_solutions"
+ description: "Enable new MediaRouter2 API to enable watch companion apps to scan while the phone screen is off."
+ bug: "281072508"
+}
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig
new file mode 100644
index 0000000..c4b38c7
--- /dev/null
+++ b/media/java/android/media/flags/projection.aconfig
@@ -0,0 +1,11 @@
+package: "com.android.media.flags"
+
+# Project link: https://gantry.corp.google.com/projects/android_platform_window_surfaces/changes
+
+flag {
+ name: "limit_manage_media_projection"
+ namespace: "lse_desktop_experience"
+ description: "Limit signature permission manage_media_projection to the SystemUI role"
+ bug: "323008518"
+ is_fixed_read_only: true
+}
diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java
index 5ed8d40..f1c5c9d 100644
--- a/media/java/android/media/metrics/EditingEndedEvent.java
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -20,6 +20,7 @@
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
+import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
@@ -27,6 +28,8 @@
import android.os.Parcelable;
import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
/** Event for an editing operation having ended. */
@@ -156,14 +159,66 @@
@SuppressWarnings("HidingField") // Hiding field from superclass as for playback events.
private final long mTimeSinceCreatedMillis;
+ private final ArrayList<MediaItemInfo> mInputMediaItemInfos;
+ @Nullable private final MediaItemInfo mOutputMediaItemInfo;
+
+ /** @hide */
+ @LongDef(
+ prefix = {"OPERATION_TYPE_"},
+ flag = true,
+ value = {
+ OPERATION_TYPE_VIDEO_TRANSCODE,
+ OPERATION_TYPE_AUDIO_TRANSCODE,
+ OPERATION_TYPE_VIDEO_EDIT,
+ OPERATION_TYPE_AUDIO_EDIT,
+ OPERATION_TYPE_VIDEO_TRANSMUX,
+ OPERATION_TYPE_AUDIO_TRANSMUX,
+ OPERATION_TYPE_PAUSED,
+ OPERATION_TYPE_RESUMED,
+ })
+ @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ public @interface OperationType {}
+
+ /** Input video was decoded and re-encoded. */
+ public static final long OPERATION_TYPE_VIDEO_TRANSCODE = 1;
+
+ /** Input audio was decoded and re-encoded. */
+ public static final long OPERATION_TYPE_AUDIO_TRANSCODE = 1L << 1;
+
+ /** Input video was edited. */
+ public static final long OPERATION_TYPE_VIDEO_EDIT = 1L << 2;
+
+ /** Input audio was edited. */
+ public static final long OPERATION_TYPE_AUDIO_EDIT = 1L << 3;
+
+ /** Input video samples were writted (muxed) directly to the output file without transcoding. */
+ public static final long OPERATION_TYPE_VIDEO_TRANSMUX = 1L << 4;
+
+ /** Input audio samples were written (muxed) directly to the output file without transcoding. */
+ public static final long OPERATION_TYPE_AUDIO_TRANSMUX = 1L << 5;
+
+ /** The editing operation was paused before it completed. */
+ public static final long OPERATION_TYPE_PAUSED = 1L << 6;
+
+ /** The editing operation resumed a previous (paused) operation. */
+ public static final long OPERATION_TYPE_RESUMED = 1L << 7;
+
+ private final @OperationType long mOperationTypes;
+
private EditingEndedEvent(
@FinalState int finalState,
@ErrorCode int errorCode,
long timeSinceCreatedMillis,
+ ArrayList<MediaItemInfo> inputMediaItemInfos,
+ @Nullable MediaItemInfo outputMediaItemInfo,
+ @OperationType long operationTypes,
@NonNull Bundle extras) {
mFinalState = finalState;
mErrorCode = errorCode;
mTimeSinceCreatedMillis = timeSinceCreatedMillis;
+ mInputMediaItemInfos = inputMediaItemInfos;
+ mOutputMediaItemInfo = outputMediaItemInfo;
+ mOperationTypes = operationTypes;
mMetricsBundle = extras.deepCopy();
}
@@ -194,6 +249,23 @@
return mTimeSinceCreatedMillis;
}
+ /** Gets information about the input media items, or an empty list if unspecified. */
+ @NonNull
+ public List<MediaItemInfo> getInputMediaItemInfos() {
+ return new ArrayList<>(mInputMediaItemInfos);
+ }
+
+ /** Gets information about the output media item, or {@code null} if unspecified. */
+ @Nullable
+ public MediaItemInfo getOutputMediaItemInfo() {
+ return mOutputMediaItemInfo;
+ }
+
+ /** Gets a set of flags describing the types of operations performed. */
+ public @OperationType long getOperationTypes() {
+ return mOperationTypes;
+ }
+
/**
* Gets metrics-related information that is not supported by dedicated methods.
*
@@ -208,7 +280,7 @@
@Override
@NonNull
public String toString() {
- return "PlaybackErrorEvent { "
+ return "EditingEndedEvent { "
+ "finalState = "
+ mFinalState
+ ", "
@@ -217,6 +289,15 @@
+ ", "
+ "timeSinceCreatedMillis = "
+ mTimeSinceCreatedMillis
+ + ", "
+ + "inputMediaItemInfos = "
+ + mInputMediaItemInfos
+ + ", "
+ + "outputMediaItemInfo = "
+ + mOutputMediaItemInfo
+ + ", "
+ + "operationTypes = "
+ + mOperationTypes
+ " }";
}
@@ -227,12 +308,21 @@
EditingEndedEvent that = (EditingEndedEvent) o;
return mFinalState == that.mFinalState
&& mErrorCode == that.mErrorCode
+ && Objects.equals(mInputMediaItemInfos, that.mInputMediaItemInfos)
+ && Objects.equals(mOutputMediaItemInfo, that.mOutputMediaItemInfo)
+ && mOperationTypes == that.mOperationTypes
&& mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis;
}
@Override
public int hashCode() {
- return Objects.hash(mFinalState, mErrorCode, mTimeSinceCreatedMillis);
+ return Objects.hash(
+ mFinalState,
+ mErrorCode,
+ mInputMediaItemInfos,
+ mOutputMediaItemInfo,
+ mOperationTypes,
+ mTimeSinceCreatedMillis);
}
@Override
@@ -240,6 +330,9 @@
dest.writeInt(mFinalState);
dest.writeInt(mErrorCode);
dest.writeLong(mTimeSinceCreatedMillis);
+ dest.writeTypedList(mInputMediaItemInfos);
+ dest.writeTypedObject(mOutputMediaItemInfo, /* parcelableFlags= */ 0);
+ dest.writeLong(mOperationTypes);
dest.writeBundle(mMetricsBundle);
}
@@ -249,15 +342,14 @@
}
private EditingEndedEvent(@NonNull Parcel in) {
- int finalState = in.readInt();
- int errorCode = in.readInt();
- long timeSinceCreatedMillis = in.readLong();
- Bundle metricsBundle = in.readBundle();
-
- mFinalState = finalState;
- mErrorCode = errorCode;
- mTimeSinceCreatedMillis = timeSinceCreatedMillis;
- mMetricsBundle = metricsBundle;
+ mFinalState = in.readInt();
+ mErrorCode = in.readInt();
+ mTimeSinceCreatedMillis = in.readLong();
+ mInputMediaItemInfos = new ArrayList<>();
+ in.readTypedList(mInputMediaItemInfos, MediaItemInfo.CREATOR);
+ mOutputMediaItemInfo = in.readTypedObject(MediaItemInfo.CREATOR);
+ mOperationTypes = in.readLong();
+ mMetricsBundle = in.readBundle();
}
public static final @NonNull Creator<EditingEndedEvent> CREATOR =
@@ -277,8 +369,11 @@
@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
public static final class Builder {
private final @FinalState int mFinalState;
+ private final ArrayList<MediaItemInfo> mInputMediaItemInfos;
private @ErrorCode int mErrorCode;
private long mTimeSinceCreatedMillis;
+ @Nullable private MediaItemInfo mOutputMediaItemInfo;
+ private @OperationType long mOperationTypes;
private Bundle mMetricsBundle;
/**
@@ -290,6 +385,7 @@
mFinalState = finalState;
mErrorCode = ERROR_CODE_NONE;
mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN;
+ mInputMediaItemInfos = new ArrayList<>();
mMetricsBundle = new Bundle();
}
@@ -312,20 +408,49 @@
return this;
}
+ /** Adds information about a media item that was input to the editing operation. */
+ public @NonNull Builder addInputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) {
+ mInputMediaItemInfos.add(Objects.requireNonNull(mediaItemInfo));
+ return this;
+ }
+
+ /** Sets information about the output media item. */
+ public @NonNull Builder setOutputMediaItemInfo(@NonNull MediaItemInfo mediaItemInfo) {
+ mOutputMediaItemInfo = Objects.requireNonNull(mediaItemInfo);
+ return this;
+ }
+
+ /**
+ * Adds an operation type to the set of operations performed.
+ *
+ * @param operationType A type of operation performed as part of this editing operation.
+ */
+ public @NonNull Builder addOperationType(@OperationType long operationType) {
+ mOperationTypes |= operationType;
+ return this;
+ }
+
/**
* Sets metrics-related information that is not supported by dedicated methods.
*
* <p>Used for backwards compatibility by the metrics infrastructure.
*/
public @NonNull Builder setMetricsBundle(@NonNull Bundle metricsBundle) {
- mMetricsBundle = metricsBundle;
+ mMetricsBundle = Objects.requireNonNull(metricsBundle);
return this;
}
/** Builds an instance. */
public @NonNull EditingEndedEvent build() {
return new EditingEndedEvent(
- mFinalState, mErrorCode, mTimeSinceCreatedMillis, mMetricsBundle);
+ mFinalState,
+ mErrorCode,
+ mTimeSinceCreatedMillis,
+ mInputMediaItemInfos,
+ mOutputMediaItemInfo,
+ mOperationTypes,
+ mMetricsBundle);
}
}
+
}
diff --git a/media/java/android/media/metrics/MediaItemInfo.java b/media/java/android/media/metrics/MediaItemInfo.java
new file mode 100644
index 0000000..63dd3cc
--- /dev/null
+++ b/media/java/android/media/metrics/MediaItemInfo.java
@@ -0,0 +1,565 @@
+/*
+ * 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 android.media.metrics;
+
+import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING;
+
+import android.annotation.FlaggedApi;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.hardware.DataSpace;
+import android.media.MediaCodec;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Size;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** Represents information about a piece of media (for example, an audio or video file). */
+@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+public final class MediaItemInfo implements Parcelable {
+
+ /** @hide */
+ @IntDef(
+ prefix = {"SOURCE_TYPE_"},
+ value = {
+ SOURCE_TYPE_UNSPECIFIED,
+ SOURCE_TYPE_GALLERY,
+ SOURCE_TYPE_CAMERA,
+ SOURCE_TYPE_EDITING_SESSION,
+ SOURCE_TYPE_LOCAL_FILE,
+ SOURCE_TYPE_REMOTE_FILE,
+ SOURCE_TYPE_REMOTE_LIVE_STREAM,
+ SOURCE_TYPE_GENERATED,
+ })
+ @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ public @interface SourceType {}
+
+ /** The media item's source is not known. */
+ public static final int SOURCE_TYPE_UNSPECIFIED = 0;
+
+ /** The media item came from the device gallery. */
+ public static final int SOURCE_TYPE_GALLERY = 1;
+
+ /** The media item came directly from camera capture. */
+ public static final int SOURCE_TYPE_CAMERA = 2;
+
+ /** The media item was output by a previous editing session. */
+ public static final int SOURCE_TYPE_EDITING_SESSION = 3;
+
+ /** The media item is stored on the local device's file system. */
+ public static final int SOURCE_TYPE_LOCAL_FILE = 4;
+
+ /** The media item is a remote file (for example, it's loaded from an HTTP server). */
+ public static final int SOURCE_TYPE_REMOTE_FILE = 5;
+
+ /** The media item is a remotely-served live stream. */
+ public static final int SOURCE_TYPE_REMOTE_LIVE_STREAM = 6;
+
+ /** The media item was generated by another system. */
+ public static final int SOURCE_TYPE_GENERATED = 7;
+
+ /** @hide */
+ @LongDef(
+ prefix = {"DATA_TYPE_"},
+ flag = true,
+ value = {
+ DATA_TYPE_IMAGE,
+ DATA_TYPE_VIDEO,
+ DATA_TYPE_AUDIO,
+ DATA_TYPE_METADATA,
+ DATA_TYPE_DEPTH,
+ DATA_TYPE_GAIN_MAP,
+ DATA_TYPE_HIGH_FRAME_RATE,
+ DATA_TYPE_CUE_POINTS,
+ DATA_TYPE_GAPLESS,
+ DATA_TYPE_SPATIAL_AUDIO,
+ DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO,
+ })
+ @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ public @interface DataType {}
+
+ /** The media item includes image data. */
+ public static final long DATA_TYPE_IMAGE = 1L;
+
+ /** The media item includes video data. */
+ public static final long DATA_TYPE_VIDEO = 1L << 1;
+
+ /** The media item includes audio data. */
+ public static final long DATA_TYPE_AUDIO = 1L << 2;
+
+ /** The media item includes metadata. */
+ public static final long DATA_TYPE_METADATA = 1L << 3;
+
+ /** The media item includes depth (z-distance) information. */
+ public static final long DATA_TYPE_DEPTH = 1L << 4;
+
+ /** The media item includes gain map information (for example, an Ultra HDR gain map). */
+ public static final long DATA_TYPE_GAIN_MAP = 1L << 5;
+
+ /** The media item includes high frame rate video data. */
+ public static final long DATA_TYPE_HIGH_FRAME_RATE = 1L << 6;
+
+ /** The media item includes time-dependent speed setting metadata. */
+ public static final long DATA_TYPE_CUE_POINTS = 1L << 7;
+
+ /** The media item includes gapless audio metadata. */
+ public static final long DATA_TYPE_GAPLESS = 1L << 8;
+
+ /** The media item includes spatial audio data. */
+ public static final long DATA_TYPE_SPATIAL_AUDIO = 1L << 9;
+
+ /** The media item includes high dynamic range (HDR) video. */
+ public static final long DATA_TYPE_HIGH_DYNAMIC_RANGE_VIDEO = 1L << 10;
+
+ /** Special value for numerical fields where the value was not specified. */
+ public static final int VALUE_UNSPECIFIED = -1;
+
+ private final @SourceType int mSourceType;
+ private final @DataType long mDataTypes;
+ private final long mDurationMillis;
+ private final long mClipDurationMillis;
+ @Nullable private final String mContainerMimeType;
+ private final List<String> mSampleMimeTypes;
+ private final List<String> mCodecNames;
+ private final int mAudioSampleRateHz;
+ private final int mAudioChannelCount;
+ private final long mAudioSampleCount;
+ private final Size mVideoSize;
+ private final int mVideoDataSpace;
+ private final float mVideoFrameRate;
+ private final long mVideoSampleCount;
+
+ private MediaItemInfo(
+ @SourceType int sourceType,
+ @DataType long dataTypes,
+ long durationMillis,
+ long clipDurationMillis,
+ @Nullable String containerMimeType,
+ List<String> sampleMimeTypes,
+ List<String> codecNames,
+ int audioSampleRateHz,
+ int audioChannelCount,
+ long audioSampleCount,
+ Size videoSize,
+ int videoDataSpace,
+ float videoFrameRate,
+ long videoSampleCount) {
+ mSourceType = sourceType;
+ mDataTypes = dataTypes;
+ mDurationMillis = durationMillis;
+ mClipDurationMillis = clipDurationMillis;
+ mContainerMimeType = containerMimeType;
+ mSampleMimeTypes = sampleMimeTypes;
+ mCodecNames = codecNames;
+ mAudioSampleRateHz = audioSampleRateHz;
+ mAudioChannelCount = audioChannelCount;
+ mAudioSampleCount = audioSampleCount;
+ mVideoSize = videoSize;
+ mVideoDataSpace = videoDataSpace;
+ mVideoFrameRate = videoFrameRate;
+ mVideoSampleCount = videoSampleCount;
+ }
+
+ /**
+ * Returns where the media item came from, or {@link #SOURCE_TYPE_UNSPECIFIED} if not specified.
+ */
+ public @SourceType int getSourceType() {
+ return mSourceType;
+ }
+
+ /** Returns the data types that are present in the media item. */
+ public @DataType long getDataTypes() {
+ return mDataTypes;
+ }
+
+ /**
+ * Returns the duration of the media item, in milliseconds, or {@link #VALUE_UNSPECIFIED} if not
+ * specified.
+ */
+ public long getDurationMillis() {
+ return mDurationMillis;
+ }
+
+ /**
+ * Returns the duration of the clip taken from the media item, in milliseconds, or {@link
+ * #VALUE_UNSPECIFIED} if not specified.
+ */
+ public long getClipDurationMillis() {
+ return mClipDurationMillis;
+ }
+
+ /** Returns the MIME type of the media container, or {@code null} if unspecified. */
+ @Nullable
+ public String getContainerMimeType() {
+ return mContainerMimeType;
+ }
+
+ /**
+ * Returns the MIME types of samples stored in the media container, or an empty list if not
+ * known.
+ */
+ @NonNull
+ public List<String> getSampleMimeTypes() {
+ return new ArrayList<>(mSampleMimeTypes);
+ }
+
+ /**
+ * Returns the {@linkplain MediaCodec#getName() media codec names} for codecs that were used as
+ * part of encoding/decoding this media item, or an empty list if not known or not applicable.
+ */
+ @NonNull
+ public List<String> getCodecNames() {
+ return new ArrayList<>(mCodecNames);
+ }
+
+ /**
+ * Returns the sample rate of audio, in Hertz, or {@link #VALUE_UNSPECIFIED} if not specified.
+ */
+ public int getAudioSampleRateHz() {
+ return mAudioSampleRateHz;
+ }
+
+ /** Returns the number of audio channels, or {@link #VALUE_UNSPECIFIED} if not specified. */
+ public int getAudioChannelCount() {
+ return mAudioChannelCount;
+ }
+
+ /**
+ * Returns the number of audio frames in the item, after clipping (if applicable), or {@link
+ * #VALUE_UNSPECIFIED} if not specified.
+ */
+ public long getAudioSampleCount() {
+ return mAudioSampleCount;
+ }
+
+ /**
+ * Returns the video size, in pixels, or a {@link Size} with width and height set to {@link
+ * #VALUE_UNSPECIFIED} if not specified.
+ */
+ @NonNull
+ public Size getVideoSize() {
+ return mVideoSize;
+ }
+
+ /** Returns the {@linkplain DataSpace data space} for video, as a packed integer. */
+ @SuppressLint("MethodNameUnits") // Packed integer for an android.hardware.DataSpace.
+ public int getVideoDataSpace() {
+ return mVideoDataSpace;
+ }
+
+ /**
+ * Returns the average video frame rate, in frames per second, or {@link #VALUE_UNSPECIFIED} if
+ * not specified.
+ */
+ public float getVideoFrameRate() {
+ return mVideoFrameRate;
+ }
+
+ /**
+ * Returns the number of video frames, aftrer clipping (if applicable), or {@link
+ * #VALUE_UNSPECIFIED} if not specified.
+ */
+ public long getVideoSampleCount() {
+ return mVideoSampleCount;
+ }
+
+ /** Builder for {@link MediaItemInfo}. */
+ @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+ public static final class Builder {
+
+ private @SourceType int mSourceType;
+ private @DataType long mDataTypes;
+ private long mDurationMillis;
+ private long mClipDurationMillis;
+ @Nullable private String mContainerMimeType;
+ private final ArrayList<String> mSampleMimeTypes;
+ private final ArrayList<String> mCodecNames;
+ private int mAudioSampleRateHz;
+ private int mAudioChannelCount;
+ private long mAudioSampleCount;
+ @Nullable private Size mVideoSize;
+ private int mVideoDataSpace;
+ private float mVideoFrameRate;
+ private long mVideoSampleCount;
+
+ /** Creates a new builder. */
+ public Builder() {
+ mSourceType = SOURCE_TYPE_UNSPECIFIED;
+ mDurationMillis = VALUE_UNSPECIFIED;
+ mClipDurationMillis = VALUE_UNSPECIFIED;
+ mSampleMimeTypes = new ArrayList<>();
+ mCodecNames = new ArrayList<>();
+ mAudioSampleRateHz = VALUE_UNSPECIFIED;
+ mAudioChannelCount = VALUE_UNSPECIFIED;
+ mAudioSampleCount = VALUE_UNSPECIFIED;
+ mVideoSize = new Size(VALUE_UNSPECIFIED, VALUE_UNSPECIFIED);
+ mVideoFrameRate = VALUE_UNSPECIFIED;
+ mVideoSampleCount = VALUE_UNSPECIFIED;
+ }
+
+ /** Sets where the media item came from. */
+ public @NonNull Builder setSourceType(@SourceType int sourceType) {
+ mSourceType = sourceType;
+ return this;
+ }
+
+ /** Adds an additional data type represented as part of the media item. */
+ public @NonNull Builder addDataType(@DataType long dataType) {
+ mDataTypes |= dataType;
+ return this;
+ }
+
+ /** Sets the duration of the media item, in milliseconds. */
+ public @NonNull Builder setDurationMillis(long durationMillis) {
+ mDurationMillis = durationMillis;
+ return this;
+ }
+
+ /** Sets the duration of the clip taken from the media item, in milliseconds. */
+ public @NonNull Builder setClipDurationMillis(long clipDurationMillis) {
+ mClipDurationMillis = clipDurationMillis;
+ return this;
+ }
+
+ /** Sets the MIME type of the media container. */
+ public @NonNull Builder setContainerMimeType(@NonNull String containerMimeType) {
+ mContainerMimeType = Objects.requireNonNull(containerMimeType);
+ return this;
+ }
+
+ /** Adds a sample MIME type stored in the media container. */
+ public @NonNull Builder addSampleMimeType(@NonNull String mimeType) {
+ mSampleMimeTypes.add(Objects.requireNonNull(mimeType));
+ return this;
+ }
+
+ /**
+ * Adds an {@linkplain MediaCodec#getName() media codec name} that was used as part of
+ * decoding/encoding this media item.
+ */
+ public @NonNull Builder addCodecName(@NonNull String codecName) {
+ mCodecNames.add(Objects.requireNonNull(codecName));
+ return this;
+ }
+
+ /** Sets the sample rate of audio, in Hertz. */
+ public @NonNull Builder setAudioSampleRateHz(@IntRange(from = 0) int audioSampleRateHz) {
+ mAudioSampleRateHz = audioSampleRateHz;
+ return this;
+ }
+
+ /** Sets the number of audio channels. */
+ public @NonNull Builder setAudioChannelCount(@IntRange(from = 0) int audioChannelCount) {
+ mAudioChannelCount = audioChannelCount;
+ return this;
+ }
+
+ /** Sets the number of audio frames in the item, after clipping (if applicable). */
+ public @NonNull Builder setAudioSampleCount(@IntRange(from = 0) long audioSampleCount) {
+ mAudioSampleCount = audioSampleCount;
+ return this;
+ }
+
+ /** Sets the video size, in pixels. */
+ public @NonNull Builder setVideoSize(@NonNull Size videoSize) {
+ mVideoSize = Objects.requireNonNull(videoSize);
+ return this;
+ }
+
+ /**
+ * Sets the {@link DataSpace} of video frames.
+ *
+ * @param videoDataSpace The data space, returned by {@link DataSpace#pack(int, int, int)}.
+ */
+ public @NonNull Builder setVideoDataSpace(int videoDataSpace) {
+ mVideoDataSpace = videoDataSpace;
+ return this;
+ }
+
+ /** Sets the average video frame rate, in frames per second. */
+ public @NonNull Builder setVideoFrameRate(@FloatRange(from = 0) float videoFrameRate) {
+ mVideoFrameRate = videoFrameRate;
+ return this;
+ }
+
+ /** Sets the number of video frames, after clipping (if applicable). */
+ public @NonNull Builder setVideoSampleCount(@IntRange(from = 0) long videoSampleCount) {
+ mVideoSampleCount = videoSampleCount;
+ return this;
+ }
+
+ /** Builds an instance. */
+ @NonNull
+ public MediaItemInfo build() {
+ return new MediaItemInfo(
+ mSourceType,
+ mDataTypes,
+ mDurationMillis,
+ mClipDurationMillis,
+ mContainerMimeType,
+ mSampleMimeTypes,
+ mCodecNames,
+ mAudioSampleRateHz,
+ mAudioChannelCount,
+ mAudioSampleCount,
+ mVideoSize,
+ mVideoDataSpace,
+ mVideoFrameRate,
+ mVideoSampleCount);
+ }
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "MediaItemInfo { "
+ + "sourceType = "
+ + mSourceType
+ + ", "
+ + "dataTypes = "
+ + mDataTypes
+ + ", "
+ + "durationMillis = "
+ + mDurationMillis
+ + ", "
+ + "clipDurationMillis = "
+ + mClipDurationMillis
+ + ", "
+ + "containerMimeType = "
+ + mContainerMimeType
+ + ", "
+ + "sampleMimeTypes = "
+ + mSampleMimeTypes
+ + ", "
+ + "codecNames = "
+ + mCodecNames
+ + ", "
+ + "audioSampleRateHz = "
+ + mAudioSampleRateHz
+ + ", "
+ + "audioChannelCount = "
+ + mAudioChannelCount
+ + ", "
+ + "audioSampleCount = "
+ + mAudioSampleCount
+ + ", "
+ + "videoSize = "
+ + mVideoSize
+ + ", "
+ + "videoDataSpace = "
+ + mVideoDataSpace
+ + ", "
+ + "videoFrameRate = "
+ + mVideoFrameRate
+ + ", "
+ + "videoSampleCount = "
+ + mVideoSampleCount
+ + " }";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ MediaItemInfo that = (MediaItemInfo) o;
+ return mSourceType == that.mSourceType
+ && mDataTypes == that.mDataTypes
+ && mDurationMillis == that.mDurationMillis
+ && mClipDurationMillis == that.mClipDurationMillis
+ && Objects.equals(mContainerMimeType, that.mContainerMimeType)
+ && mSampleMimeTypes.equals(that.mSampleMimeTypes)
+ && mCodecNames.equals(that.mCodecNames)
+ && mAudioSampleRateHz == that.mAudioSampleRateHz
+ && mAudioChannelCount == that.mAudioChannelCount
+ && mAudioSampleCount == that.mAudioSampleCount
+ && Objects.equals(mVideoSize, that.mVideoSize)
+ && Objects.equals(mVideoDataSpace, that.mVideoDataSpace)
+ && mVideoFrameRate == that.mVideoFrameRate
+ && mVideoSampleCount == that.mVideoSampleCount;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mSourceType, mDataTypes);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mSourceType);
+ dest.writeLong(mDataTypes);
+ dest.writeLong(mDurationMillis);
+ dest.writeLong(mClipDurationMillis);
+ dest.writeString(mContainerMimeType);
+ dest.writeStringList(mSampleMimeTypes);
+ dest.writeStringList(mCodecNames);
+ dest.writeInt(mAudioSampleRateHz);
+ dest.writeInt(mAudioChannelCount);
+ dest.writeLong(mAudioSampleCount);
+ dest.writeInt(mVideoSize.getWidth());
+ dest.writeInt(mVideoSize.getHeight());
+ dest.writeInt(mVideoDataSpace);
+ dest.writeFloat(mVideoFrameRate);
+ dest.writeLong(mVideoSampleCount);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private MediaItemInfo(@NonNull Parcel in) {
+ mSourceType = in.readInt();
+ mDataTypes = in.readLong();
+ mDurationMillis = in.readLong();
+ mClipDurationMillis = in.readLong();
+ mContainerMimeType = in.readString();
+ mSampleMimeTypes = new ArrayList<>();
+ in.readStringList(mSampleMimeTypes);
+ mCodecNames = new ArrayList<>();
+ in.readStringList(mCodecNames);
+ mAudioSampleRateHz = in.readInt();
+ mAudioChannelCount = in.readInt();
+ mAudioSampleCount = in.readLong();
+ int videoSizeWidth = in.readInt();
+ int videoSizeHeight = in.readInt();
+ mVideoSize = new Size(videoSizeWidth, videoSizeHeight);
+ mVideoDataSpace = in.readInt();
+ mVideoFrameRate = in.readFloat();
+ mVideoSampleCount = in.readLong();
+ }
+
+ public static final @NonNull Creator<MediaItemInfo> CREATOR =
+ new Creator<>() {
+ @Override
+ public MediaItemInfo[] newArray(int size) {
+ return new MediaItemInfo[size];
+ }
+
+ @Override
+ public MediaItemInfo createFromParcel(@NonNull Parcel in) {
+ return new MediaItemInfo(in);
+ }
+ };
+}
diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java
index e3290d6..2a0648d 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -180,7 +180,7 @@
@SuppressLint("UnflaggedApi")
@TestApi
@NonNull
- public Intent createScreenCaptureIntent(@Nullable LaunchCookie launchCookie) {
+ public Intent createScreenCaptureIntent(@NonNull LaunchCookie launchCookie) {
Intent i = createScreenCaptureIntent();
i.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie);
return i;
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 59b10c6..76664a6 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -18,6 +18,7 @@
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StringDef;
@@ -59,7 +60,7 @@
*/
@FlaggedApi(Flags.FLAG_ENABLE_AD_SERVICE_FW)
@SystemService(Context.TV_AD_SERVICE)
-public class TvAdManager {
+public final class TvAdManager {
// TODO: implement more methods and unhide APIs.
private static final String TAG = "TvAdManager";
@@ -237,6 +238,76 @@
*/
public static final String SESSION_DATA_KEY_REQUEST_ID = "request_id";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "SESSION_STATE_", value = {
+ SESSION_STATE_STOPPED,
+ SESSION_STATE_RUNNING,
+ SESSION_STATE_ERROR})
+ public @interface SessionState {}
+
+ /**
+ * Stopped (or not started) state of AD service session.
+ */
+ public static final int SESSION_STATE_STOPPED = 1;
+ /**
+ * Running state of AD service session.
+ */
+ public static final int SESSION_STATE_RUNNING = 2;
+ /**
+ * Error state of AD service session.
+ */
+ public static final int SESSION_STATE_ERROR = 3;
+
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = false, prefix = "ERROR_", value = {
+ ERROR_NONE,
+ ERROR_UNKNOWN,
+ ERROR_NOT_SUPPORTED,
+ ERROR_WEAK_SIGNAL,
+ ERROR_RESOURCE_UNAVAILABLE,
+ ERROR_BLOCKED,
+ ERROR_ENCRYPTED,
+ ERROR_UNKNOWN_CHANNEL,
+ })
+ public @interface ErrorCode {}
+
+ /**
+ * No error.
+ */
+ public static final int ERROR_NONE = 0;
+ /**
+ * Unknown error code.
+ */
+ public static final int ERROR_UNKNOWN = 1;
+ /**
+ * Error code for an unsupported channel.
+ */
+ public static final int ERROR_NOT_SUPPORTED = 2;
+ /**
+ * Error code for weak signal.
+ */
+ public static final int ERROR_WEAK_SIGNAL = 3;
+ /**
+ * Error code when resource (e.g. tuner) is unavailable.
+ */
+ public static final int ERROR_RESOURCE_UNAVAILABLE = 4;
+ /**
+ * Error code for blocked contents.
+ */
+ public static final int ERROR_BLOCKED = 5;
+ /**
+ * Error code when the key or module is missing for the encrypted channel.
+ */
+ public static final int ERROR_ENCRYPTED = 6;
+ /**
+ * Error code when the current channel is an unknown channel.
+ */
+ public static final int ERROR_UNKNOWN_CHANNEL = 7;
+
private final ITvAdManager mService;
private final int mUserId;
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 6c8a8fd..2bba0f3 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -280,7 +280,6 @@
/**
* Requests the bounds of the current video.
- * @hide
*/
@CallSuper
public void requestCurrentVideoBounds() {
@@ -304,7 +303,6 @@
/**
* Requests the URI of the current channel.
- * @hide
*/
@CallSuper
public void requestCurrentChannelUri() {
@@ -328,7 +326,6 @@
/**
* Requests the list of {@link TvTrackInfo}.
- * @hide
*/
@CallSuper
public void requestTrackInfoList() {
@@ -354,7 +351,6 @@
* Requests current TV input ID.
*
* @see android.media.tv.TvInputInfo
- * @hide
*/
@CallSuper
public void requestCurrentTvInputId() {
@@ -393,7 +389,6 @@
* @param data the original bytes to be signed.
*
* @see #onSigningResult(String, byte[])
- * @hide
*/
@CallSuper
public void requestSigning(@NonNull String signingId, @NonNull String algorithm,
@@ -535,28 +530,24 @@
* Receives current video bounds.
*
* @param bounds the rectangle area for rendering the current video.
- * @hide
*/
public void onCurrentVideoBounds(@NonNull Rect bounds) {
}
/**
* Receives current channel URI.
- * @hide
*/
public void onCurrentChannelUri(@Nullable Uri channelUri) {
}
/**
* Receives track list.
- * @hide
*/
public void onTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
}
/**
* Receives current TV input ID.
- * @hide
*/
public void onCurrentTvInputId(@Nullable String inputId) {
}
@@ -569,7 +560,6 @@
* @param result the signed result.
*
* @see #requestSigning(String, String, String, byte[])
- * @hide
*/
public void onSigningResult(@NonNull String signingId, @NonNull byte[] result) {
}
@@ -584,7 +574,6 @@
* "onRequestSigning" can also be added to the params.
*
* @see TvAdView#ERROR_KEY_METHOD_NAME
- * @hide
*/
public void onError(@NonNull String errMsg, @NonNull Bundle params) {
}
@@ -601,7 +590,6 @@
* {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
* See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
* how to parse this data.
- * @hide
*/
public void onTvMessage(@TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
@@ -671,6 +659,30 @@
}
/**
+ * Notifies when the session state is changed.
+ *
+ * @param state the current session state.
+ * @param err the error code for error state. {@link TvAdManager#ERROR_NONE} is
+ * used when the state is not {@link TvAdManager#SESSION_STATE_ERROR}.
+ */
+ @CallSuper
+ public void notifySessionStateChanged(
+ @TvAdManager.SessionState int state,
+ @TvAdManager.ErrorCode int err) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ if (DEBUG) {
+ Log.d(TAG, "notifySessionStateChanged (state="
+ + state + "; err=" + err + ")");
+ }
+ // TODO: handle session callback
+ }
+ });
+ }
+
+ /**
* Takes care of dispatching incoming input events and tells whether the event was handled.
*/
int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index ee01468..2fac8ce 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -61,10 +61,19 @@
* The name of the method where the error happened, if applicable. For example, if there is an
* error during signing, the request name is "onRequestSigning".
* @see #notifyError(String, Bundle)
- * @hide
*/
public static final String ERROR_KEY_METHOD_NAME = "method_name";
+ /**
+ * The error code of an error.
+ *
+ * <p>It can be {@link TvAdManager#ERROR_WEAK_SIGNAL},
+ * {@link TvAdManager#ERROR_RESOURCE_UNAVAILABLE}, etc.
+ *
+ * @see #notifyError(String, Bundle)
+ */
+ public static final String ERROR_KEY_ERROR_CODE = "error_code";
+
private final TvAdManager mTvAdManager;
private final Handler mHandler = new Handler();
@@ -486,7 +495,6 @@
* Sends current video bounds to related TV AD service.
*
* @param bounds the rectangle area for rendering the current video.
- * @hide
*/
public void sendCurrentVideoBounds(@NonNull Rect bounds) {
if (DEBUG) {
@@ -502,7 +510,6 @@
*
* @param channelUri The current channel URI; {@code null} if there is no currently tuned
* channel.
- * @hide
*/
public void sendCurrentChannelUri(@Nullable Uri channelUri) {
if (DEBUG) {
@@ -515,7 +522,6 @@
/**
* Sends track info list to related TV AD service.
- * @hide
*/
public void sendTrackInfoList(@Nullable List<TvTrackInfo> tracks) {
if (DEBUG) {
@@ -532,7 +538,6 @@
* @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is
* tuned.
* @see android.media.tv.TvInputInfo
- * @hide
*/
public void sendCurrentTvInputId(@Nullable String inputId) {
if (DEBUG) {
@@ -553,7 +558,6 @@
* @param signingId the ID to identify the request. It's the same as the corresponding ID in
* {@link TvAdService.Session#requestSigning(String, String, String, byte[])}
* @param result the signed result.
- * @hide
*/
public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
if (DEBUG) {
@@ -574,7 +578,7 @@
* can also be added to the params.
*
* @see #ERROR_KEY_METHOD_NAME
- * @hide
+ * @see #ERROR_KEY_ERROR_CODE
*/
public void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
if (DEBUG) {
@@ -597,7 +601,6 @@
* {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
* See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
* how to parse this data.
- * @hide
*/
public void notifyTvMessage(@NonNull @TvInputManager.TvMessageType int type,
@NonNull Bundle data) {
@@ -633,7 +636,6 @@
* @param callback the callback to receive events. MUST NOT be {@code null}.
*
* @see #clearCallback()
- * @hide
*/
public void setCallback(
@NonNull @CallbackExecutor Executor executor,
@@ -649,7 +651,6 @@
* Clears the callback.
*
* @see #setCallback(Executor, TvAdCallback)
- * @hide
*/
public void clearCallback() {
synchronized (mCallbackLock) {
@@ -845,7 +846,6 @@
/**
* Callback used to receive various status updates on the {@link TvAdView}.
- * @hide
*/
public abstract static class TvAdCallback {
@@ -898,5 +898,20 @@
public void onRequestSigning(@NonNull String serviceId, @NonNull String signingId,
@NonNull String algorithm, @NonNull String alias, @NonNull byte[] data) {
}
+
+ /**
+ * This is called when the state of corresponding AD service is changed.
+ *
+ * @param serviceId The ID of the AD service bound to this view.
+ * @param state the current state.
+ * @param err the error code for error state. {@link TvAdManager#ERROR_NONE}
+ * is used when the state is not
+ * {@link TvAdManager#SESSION_STATE_ERROR}.
+ */
+ public void onStateChanged(
+ @NonNull String serviceId,
+ @TvAdManager.SessionState int state,
+ @TvAdManager.ErrorCode int err) {
+ }
}
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index eba26d4..f332f81 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -873,6 +873,9 @@
/**
* Called when the corresponding TV input selected to a track.
+ *
+ * If the track is deselected and no track is currently selected,
+ * trackId is an empty string.
*/
public void onTrackSelected(@TvTrackInfo.Type int type, @NonNull String trackId) {
}
@@ -1845,6 +1848,10 @@
if (DEBUG) {
Log.d(TAG, "notifyTrackSelected (type=" + type + "trackId=" + trackId + ")");
}
+ // TvInputService accepts a Null String, but onTrackSelected expects NonNull.
+ if (trackId == null) {
+ trackId = "";
+ }
onTrackSelected(type, trackId);
}
diff --git a/opengl/java/android/opengl/OWNERS b/opengl/java/android/opengl/OWNERS
index 9c6c610..e340bc6 100644
--- a/opengl/java/android/opengl/OWNERS
+++ b/opengl/java/android/opengl/OWNERS
@@ -2,3 +2,5 @@
sumir@google.com
prahladk@google.com
+ianelliott@google.com
+lpy@google.com
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
index 7cc95c5..0fc1845 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.kt
@@ -36,6 +36,7 @@
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
+import android.os.Flags
import android.os.Process
import android.os.UserHandle
import android.os.UserManager
@@ -97,16 +98,17 @@
}
}
- if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P
- && !isPermissionGranted(
+ if (getMaxTargetSdkVersionForUid(context, callingUid) >= Build.VERSION_CODES.P &&
+ !isPermissionGranted(
context, Manifest.permission.REQUEST_DELETE_PACKAGES, callingUid
- )
- && !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid)
+ ) &&
+ !isPermissionGranted(context, Manifest.permission.DELETE_PACKAGES, callingUid)
) {
Log.e(
- LOG_TAG, "Uid " + callingUid + " does not have "
- + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
- + Manifest.permission.DELETE_PACKAGES
+ LOG_TAG,
+ "Uid " + callingUid + " does not have " +
+ Manifest.permission.REQUEST_DELETE_PACKAGES + " or " +
+ Manifest.permission.DELETE_PACKAGES
)
return UninstallAborted(UninstallAborted.ABORT_REASON_GENERIC_ERROR)
}
@@ -138,8 +140,9 @@
val profiles = userManager!!.userProfiles
if (!profiles.contains(uninstalledUser)) {
Log.e(
- LOG_TAG, "User " + Process.myUserHandle() + " can't request uninstall "
- + "for user " + uninstalledUser
+ LOG_TAG,
+ "User " + Process.myUserHandle() + " can't request uninstall " +
+ "for user " + uninstalledUser
)
return UninstallAborted(UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED)
}
@@ -202,9 +205,13 @@
val isSingleUser = isSingleUser()
if (isUpdate) {
- messageBuilder.append(context.getString(
- if (isSingleUser) R.string.uninstall_update_text
- else R.string.uninstall_update_text_multiuser
+ messageBuilder.append(
+ context.getString(
+ if (isSingleUser) {
+ R.string.uninstall_update_text
+ } else {
+ R.string.uninstall_update_text_multiuser
+ }
)
)
} else if (uninstallFromAllUsers && !isSingleUser) {
@@ -214,42 +221,42 @@
val customUserManager = context.createContextAsUser(uninstalledUser!!, 0)
.getSystemService(UserManager::class.java)
val userName = customUserManager!!.userName
-
- val uninstalledUserType = getUninstalledUserType(myUserHandle, uninstalledUser!!)
- val messageString: String
- when (uninstalledUserType) {
- UserManager.USER_TYPE_PROFILE_MANAGED -> {
+ var messageString = context.getString(
+ R.string.uninstall_application_text_user,
+ userName
+ )
+ if (userManager!!.isSameProfileGroup(myUserHandle, uninstalledUser!!)) {
+ if (customUserManager!!.isManagedProfile()) {
messageString = context.getString(
- R.string.uninstall_application_text_current_user_work_profile, userName
+ R.string.uninstall_application_text_current_user_work_profile, userName
)
- }
-
- UserManager.USER_TYPE_PROFILE_CLONE -> {
+ } else if (customUserManager!!.isCloneProfile()){
isClonedApp = true
messageString = context.getString(
- R.string.uninstall_application_text_current_user_clone_profile
+ R.string.uninstall_application_text_current_user_clone_profile
)
- }
-
- else -> {
+ } else if (Flags.allowPrivateProfile() && customUserManager!!.isPrivateProfile()) {
+ // TODO(b/324244123): Get these Strings from a User Property API.
messageString = context.getString(
- R.string.uninstall_application_text_user, userName
+ R.string.uninstall_application_text_current_user_private_profile
)
}
-
}
messageBuilder.append(messageString)
} else if (isCloneProfile(uninstalledUser!!)) {
isClonedApp = true
- messageBuilder.append(context.getString(
+ messageBuilder.append(
+ context.getString(
R.string.uninstall_application_text_current_user_clone_profile
)
)
- } else if (myUserHandle == UserHandle.SYSTEM
- && hasClonedInstance(targetAppInfo!!.packageName)
+ } else if (myUserHandle == UserHandle.SYSTEM &&
+ hasClonedInstance(targetAppInfo!!.packageName)
) {
- messageBuilder.append(context.getString(
- R.string.uninstall_application_text_with_clone_instance, targetAppLabel
+ messageBuilder.append(
+ context.getString(
+ R.string.uninstall_application_text_with_clone_instance,
+ targetAppLabel
)
)
} else {
@@ -296,31 +303,6 @@
return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2)
}
- /**
- * Returns the type of the user from where an app is being uninstalled. We are concerned with
- * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile
- * belong to the same profile group.
- */
- private fun getUninstalledUserType(
- myUserHandle: UserHandle,
- uninstalledUserHandle: UserHandle
- ): String? {
- if (!userManager!!.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) {
- return null
- }
- val customUserManager = context.createContextAsUser(uninstalledUserHandle, 0)
- .getSystemService(UserManager::class.java)
- val userTypes =
- arrayOf(UserManager.USER_TYPE_PROFILE_MANAGED, UserManager.USER_TYPE_PROFILE_CLONE)
-
- for (userType in userTypes) {
- if (customUserManager!!.isUserOfType(userType)) {
- return userType
- }
- }
- return null
- }
-
private fun hasClonedInstance(packageName: String): Boolean {
// Check if clone user is present on the device.
var cloneUser: UserHandle? = null
@@ -334,8 +316,8 @@
}
// Check if another instance of given package exists in clone user profile.
return try {
- cloneUser != null
- && packageManager.getPackageUidAsUser(
+ cloneUser != null &&
+ packageManager.getPackageUidAsUser(
packageName, PackageManager.PackageInfoFlags.of(0), cloneUser.identifier
) > 0
} catch (e: PackageManager.NameNotFoundException) {
@@ -382,7 +364,9 @@
val storageStatsManager = context.getSystemService(StorageStatsManager::class.java)
try {
val stats = storageStatsManager!!.queryStatsForPackage(
- packageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user
+ packageManager.getApplicationInfo(pkg, 0).storageUuid,
+ pkg,
+ user
)
return stats.getDataBytes()
} catch (e: Exception) {
@@ -423,17 +407,24 @@
broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId)
broadcastIntent.setPackage(context.packageName)
val pendingIntent = PendingIntent.getBroadcast(
- context, uninstallId, broadcastIntent,
+ context,
+ uninstallId,
+ broadcastIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
if (!startUninstall(
- targetPackageName!!, uninstalledUser!!, pendingIntent, uninstallFromAllUsers,
+ targetPackageName!!,
+ uninstalledUser!!,
+ pendingIntent,
+ uninstallFromAllUsers,
keepData
)
) {
handleUninstallResult(
PackageInstaller.STATUS_FAILURE,
- PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0
+ PackageManager.DELETE_FAILED_INTERNAL_ERROR,
+ null,
+ 0
)
}
}
@@ -474,9 +465,14 @@
// Caller did not want the result back. So, we either show a Toast, or a Notification.
if (status == PackageInstaller.STATUS_SUCCESS) {
- val statusMessage = if (isClonedApp) context.getString(
- R.string.uninstall_done_clone_app, targetAppLabel
- ) else context.getString(R.string.uninstall_done_app, targetAppLabel)
+ val statusMessage = if (isClonedApp) {
+ context.getString(
+ R.string.uninstall_done_clone_app,
+ targetAppLabel
+ )
+ } else {
+ context.getString(R.string.uninstall_done_app, targetAppLabel)
+ }
uninstallResult.setValue(
UninstallSuccess(activityResultCode = legacyStatus, message = statusMessage)
)
@@ -499,27 +495,32 @@
findUserOfDeviceAdmin(myUserHandle, targetPackageName!!)
if (otherBlockingUserHandle == null) {
Log.d(
- LOG_TAG, "Uninstall failed because $targetPackageName"
- + " is a device admin"
+ LOG_TAG,
+ "Uninstall failed because $targetPackageName" +
+ " is a device admin"
)
addDeviceManagerButton(context, uninstallFailedNotification)
setBigText(
- uninstallFailedNotification, context.getString(
+ uninstallFailedNotification,
+ context.getString(
R.string.uninstall_failed_device_policy_manager
)
)
} else {
Log.d(
- LOG_TAG, "Uninstall failed because $targetPackageName"
- + " is a device admin of user $otherBlockingUserHandle"
+ LOG_TAG,
+ "Uninstall failed because $targetPackageName" +
+ " is a device admin of user $otherBlockingUserHandle"
)
val userName = context.createContextAsUser(otherBlockingUserHandle, 0)
.getSystemService(UserManager::class.java)!!.userName
setBigText(
- uninstallFailedNotification, String.format(
+ uninstallFailedNotification,
+ String.format(
context.getString(
R.string.uninstall_failed_device_policy_manager_of_user
- ), userName
+ ),
+ userName
)
)
}
@@ -528,7 +529,9 @@
PackageManager.DELETE_FAILED_OWNER_BLOCKED -> {
val otherBlockingUserHandle = findBlockingUser(targetPackageName!!)
val isProfileOfOrSame = isProfileOfOrSame(
- userManager!!, myUserHandle, otherBlockingUserHandle
+ userManager!!,
+ myUserHandle,
+ otherBlockingUserHandle
)
if (isProfileOfOrSame) {
addDeviceManagerButton(context, uninstallFailedNotification)
@@ -538,15 +541,19 @@
var bigText: String? = null
if (otherBlockingUserHandle == null) {
Log.d(
- LOG_TAG, "Uninstall failed for $targetPackageName " +
+ LOG_TAG,
+ "Uninstall failed for $targetPackageName " +
"with code $status no blocking user"
)
} else if (otherBlockingUserHandle === UserHandle.SYSTEM) {
bigText = context.getString(R.string.uninstall_blocked_device_owner)
} else {
bigText = context.getString(
- if (uninstallFromAllUsers) R.string.uninstall_all_blocked_profile_owner
- else R.string.uninstall_blocked_profile_owner
+ if (uninstallFromAllUsers) {
+ R.string.uninstall_all_blocked_profile_owner
+ } else {
+ R.string.uninstall_blocked_profile_owner
+ }
)
}
bigText?.let { setBigText(uninstallFailedNotification, it) }
@@ -554,8 +561,9 @@
else -> {
Log.d(
- LOG_TAG, "Uninstall blocked for $targetPackageName"
- + " with legacy code $legacyStatus"
+ LOG_TAG,
+ "Uninstall blocked for $targetPackageName" +
+ " with legacy code $legacyStatus"
)
}
}
@@ -639,7 +647,9 @@
Icon.createWithResource(context, R.drawable.ic_settings_multiuser),
context.getString(R.string.manage_users),
PendingIntent.getActivity(
- context, 0, getUserSettingsIntent(),
+ context,
+ 0,
+ getUserSettingsIntent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
@@ -668,7 +678,9 @@
Icon.createWithResource(context, R.drawable.ic_lock),
context.getString(R.string.manage_device_administrators),
PendingIntent.getActivity(
- context, 0, getDeviceManagerIntent(),
+ context,
+ 0,
+ getDeviceManagerIntent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
@@ -706,7 +718,8 @@
context.createContextAsUser(targetUser, 0)
.packageManager.packageInstaller.uninstall(
VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
- flags, pendingIntent.intentSender
+ flags,
+ pendingIntent.intentSender
)
true
} catch (e: IllegalArgumentException) {
@@ -719,7 +732,8 @@
if (callback != null) {
callback!!.onUninstallComplete(
targetPackageName!!,
- PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user"
+ PackageManager.DELETE_FAILED_ABORTED,
+ "Cancelled by user"
)
}
}
diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig
index bab6781..d622eb8 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -17,3 +17,9 @@
}
}
+flag {
+ name: "bluetooth_qs_tile_dialog_auto_on_toggle"
+ namespace: "bluetooth"
+ description: "Displays the auto on toggle in the bluetooth QS tile dialog"
+ bug: "316985153"
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index e3012cd..249fa7f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -1621,6 +1621,7 @@
}
public static class AppEntry extends SizeInfo {
+ @VisibleForTesting String mProfileType;
@Nullable public final File apkFile;
public final long id;
public String label;
@@ -1647,11 +1648,6 @@
*/
public boolean isHomeApp;
- /**
- * Whether or not it's a cloned app .
- */
- public boolean isCloned;
-
public String getNormalizedLabel() {
if (normalizedLabel != null) {
return normalizedLabel;
@@ -1692,11 +1688,21 @@
() -> this.ensureLabelDescriptionLocked(context));
}
UserManager um = UserManager.get(context);
- this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
UserInfo userInfo = um.getUserInfo(UserHandle.getUserId(info.uid));
- if (userInfo != null) {
- this.isCloned = userInfo.isCloneProfile();
- }
+ mProfileType = userInfo.userType;
+ this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
+ }
+
+ public boolean isClonedProfile() {
+ return UserManager.USER_TYPE_PROFILE_CLONE.equals(mProfileType);
+ }
+
+ public boolean isManagedProfile() {
+ return UserManager.USER_TYPE_PROFILE_MANAGED.equals(mProfileType);
+ }
+
+ public boolean isPrivateProfile() {
+ return UserManager.USER_TYPE_PROFILE_PRIVATE.equals(mProfileType);
}
/**
@@ -1890,16 +1896,24 @@
};
public static final AppFilter FILTER_WORK = new AppFilter() {
- private int mCurrentUser;
@Override
- public void init() {
- mCurrentUser = ActivityManager.getCurrentUser();
- }
+ public void init() {}
@Override
public boolean filterApp(AppEntry entry) {
- return !entry.showInPersonalTab;
+ return !entry.showInPersonalTab && entry.isManagedProfile();
+ }
+ };
+
+ public static final AppFilter FILTER_PRIVATE_PROFILE = new AppFilter() {
+
+ @Override
+ public void init() {}
+
+ @Override
+ public boolean filterApp(AppEntry entry) {
+ return !entry.showInPersonalTab && entry.isPrivateProfile();
}
};
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
index c5598bf..213a66e 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
@@ -22,6 +22,7 @@
import static org.mockito.Mockito.when;
import android.content.pm.ApplicationInfo;
+import android.os.UserManager;
import org.junit.Before;
import org.junit.Test;
@@ -297,11 +298,26 @@
@Test
public void testPersonalAndWorkFiltersDisplaysCorrectApps() {
mEntry.showInPersonalTab = true;
+ mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM;
assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue();
assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isFalse();
mEntry.showInPersonalTab = false;
+ mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_MANAGED;
assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse();
assertThat(ApplicationsState.FILTER_WORK.filterApp(mEntry)).isTrue();
}
+
+ @Test
+ public void testPrivateProfileFilterDisplaysCorrectApps() {
+ mEntry.showInPersonalTab = true;
+ mEntry.mProfileType = UserManager.USER_TYPE_FULL_SYSTEM;
+ assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isTrue();
+ assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isFalse();
+
+ mEntry.showInPersonalTab = false;
+ mEntry.mProfileType = UserManager.USER_TYPE_PROFILE_PRIVATE;
+ assertThat(ApplicationsState.FILTER_PERSONAL.filterApp(mEntry)).isFalse();
+ assertThat(ApplicationsState.FILTER_PRIVATE_PROFILE.filterApp(mEntry)).isTrue();
+ }
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index cdcf8e4..8ae117e 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -264,5 +264,6 @@
Settings.Secure.EVEN_DIMMER_ACTIVATED,
Settings.Secure.EVEN_DIMMER_MIN_NITS,
Settings.Secure.STYLUS_POINTER_ICON_ENABLED,
+ Settings.Secure.CAMERA_EXTENSIONS_FALLBACK
};
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 35d45a9..5adae375 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -418,5 +418,6 @@
VALIDATORS.put(Secure.CREDENTIAL_SERVICE_PRIMARY, NULLABLE_COMPONENT_NAME_VALIDATOR);
VALIDATORS.put(Secure.AUTOFILL_SERVICE, AUTOFILL_SERVICE_VALIDATOR);
VALIDATORS.put(Secure.STYLUS_POINTER_ICON_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Secure.CAMERA_EXTENSIONS_FALLBACK, BOOLEAN_VALIDATOR);
}
}
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
index e55bbec..9ecbd50 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java
@@ -31,6 +31,7 @@
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
public class SettingsStateTest extends AndroidTestCase {
@@ -626,4 +627,121 @@
assertEquals(VALUE2, settingsState.getSettingLocked(INVALID_STAGED_FLAG_1).getValue());
}
}
+
+ public void testsetSettingsLockedKeepTrunkDefault() throws Exception {
+ final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile));
+ os.print(
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>"
+ + "<settings version=\"120\">"
+ + " <setting id=\"0\" name=\"test_namespace/flag0\" "
+ + "value=\"false\" package=\"com.android.flags\" />"
+ + " <setting id=\"1\" name=\"test_namespace/flag1\" "
+ + "value=\"false\" package=\"com.android.flags\" />"
+ + " <setting id=\"2\" name=\"test_namespace/com.android.flags.flag3\" "
+ + "value=\"false\" package=\"com.android.flags\" />"
+ + " <setting id=\"3\" "
+ + "name=\"test_another_namespace/com.android.another.flags.flag0\" "
+ + "value=\"false\" package=\"com.android.another.flags\" />"
+ + "</settings>");
+ os.close();
+
+ int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+
+ SettingsState settingsState = new SettingsState(
+ getContext(), mLock, mSettingsFile, configKey,
+ SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+ String prefix = "test_namespace";
+ Map<String, String> keyValues =
+ Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false");
+ String packageName = "com.android.flags";
+
+ parsed_flags flags = parsed_flags
+ .newBuilder()
+ .addParsedFlag(parsed_flag
+ .newBuilder()
+ .setPackage(packageName)
+ .setName("flag3")
+ .setNamespace(prefix)
+ .setDescription("test flag")
+ .addBug("12345678")
+ .setState(Aconfig.flag_state.DISABLED)
+ .setPermission(Aconfig.flag_permission.READ_WRITE))
+ .addParsedFlag(parsed_flag
+ .newBuilder()
+ .setPackage("com.android.another.flags")
+ .setName("flag0")
+ .setNamespace("test_another_namespace")
+ .setDescription("test flag")
+ .addBug("12345678")
+ .setState(Aconfig.flag_state.DISABLED)
+ .setPermission(Aconfig.flag_permission.READ_WRITE))
+ .build();
+
+ synchronized (mLock) {
+ settingsState.loadAconfigDefaultValues(
+ flags.toByteArray(), settingsState.getAconfigDefaultValues());
+ List<String> updates =
+ settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
+ assertEquals(3, updates.size());
+
+ SettingsState.Setting s;
+
+ s = settingsState.getSettingLocked("test_namespace/flag0");
+ assertEquals("true", s.getValue());
+
+ s = settingsState.getSettingLocked("test_namespace/flag1");
+ assertNull(s.getValue());
+
+ s = settingsState.getSettingLocked("test_namespace/flag2");
+ assertEquals("false", s.getValue());
+
+ s = settingsState.getSettingLocked("test_namespace/com.android.flags.flag3");
+ assertEquals("false", s.getValue());
+
+ s = settingsState.getSettingLocked(
+ "test_another_namespace/com.android.another.flags.flag0");
+ assertEquals("false", s.getValue());
+ }
+ }
+
+ public void testsetSettingsLockedNoTrunkDefault() throws Exception {
+ final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile));
+ os.print(
+ "<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>"
+ + "<settings version=\"120\">"
+ + " <setting id=\"0\" name=\"test_namespace/flag0\" "
+ + "value=\"false\" package=\"com.android.flags\" />"
+ + " <setting id=\"1\" name=\"test_namespace/flag1\" "
+ + "value=\"false\" package=\"com.android.flags\" />"
+ + "</settings>");
+ os.close();
+
+ int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0);
+
+ SettingsState settingsState = new SettingsState(
+ getContext(), mLock, mSettingsFile, configKey,
+ SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper());
+
+ Map<String, String> keyValues =
+ Map.of("test_namespace/flag0", "true", "test_namespace/flag2", "false");
+ String packageName = "com.android.flags";
+
+ synchronized (mLock) {
+ List<String> updates =
+ settingsState.setSettingsLocked("test_namespace/", keyValues, packageName);
+ assertEquals(3, updates.size());
+
+ SettingsState.Setting s;
+
+ s = settingsState.getSettingLocked("test_namespace/flag0");
+ assertEquals("true", s.getValue());
+
+ s = settingsState.getSettingLocked("test_namespace/flag1");
+ assertNull(s.getValue());
+
+ s = settingsState.getSettingLocked("test_namespace/flag2");
+ assertEquals("false", s.getValue());
+ }
+ }
}
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 84ef6e5..c086baa 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -862,6 +862,8 @@
<!-- Permission required for CTS test - CtsTelephonyProviderTestCases -->
<uses-permission android:name="android.permission.WRITE_APN_SETTINGS" />
+ <uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
+ <uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.LOG_FOREGROUND_RESOURCE_USE" />
@@ -917,6 +919,9 @@
<!-- Permissions required for CTS test - GrammaticalInflectionManagerTest -->
<uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
+ <!-- Permission required for CTS test - CtsPackageManagerTestCases-->
+ <uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT" />
+
<application
android:label="@string/app_label"
android:theme="@android:style/Theme.DeviceDefault.DayNight"
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING b/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING
index 2bd52b5..29a25ad 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING
+++ b/packages/SystemUI/accessibility/accessibilitymenu/TEST_MAPPING
@@ -4,8 +4,15 @@
"name": "AccessibilityMenuServiceTests",
"options": [
{
- "include-annotation": "android.platform.test.annotations.Presubmit"
- },
+ "exclude-annotation": "android.support.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "AccessibilityMenuServiceTests",
+ "options": [
{
"exclude-annotation": "android.support.test.filters.FlakyTest"
}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index 9d1af0e..72c1092 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -36,6 +36,7 @@
import android.app.KeyguardManager;
import android.app.UiAutomation;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -57,6 +58,7 @@
import org.junit.After;
import org.junit.AfterClass;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -330,8 +332,10 @@
AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal()));
Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND);
- String expectedPackage = expectedIntent.resolveActivity(
- sInstrumentation.getContext().getPackageManager()).getPackageName();
+ ComponentName componentName = expectedIntent.resolveActivity(
+ sInstrumentation.getContext().getPackageManager());
+ Assume.assumeNotNull(componentName);
+ String expectedPackage = componentName.getPackageName();
sUiAutomation.executeAndWaitForEvent(
() -> assistantButton.performAction(CLICK_ID),
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 7a4e60a..56576f1 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -354,13 +354,6 @@
}
flag {
- name: "bluetooth_qs_tile_dialog_auto_on_toggle"
- namespace: "systemui"
- description: "Displays the auto on toggle in the bluetooth QS tile dialog"
- bug: "316985153"
-}
-
-flag {
name: "smartspace_relocate_to_bottom"
namespace: "systemui"
description: "Relocate Smartspace to bottom of the Lock Screen"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt
new file mode 100644
index 0000000..abe1e3d
--- /dev/null
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffect.kt
@@ -0,0 +1,377 @@
+/*
+ * 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.surfaceeffects.loadingeffect
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.graphics.Paint
+import android.graphics.RenderEffect
+import android.view.View
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
+
+/**
+ * Plays loading effect with the given configuration.
+ *
+ * @param baseType immutable base shader type. This is used for constructing the shader. Reconstruct
+ * the [LoadingEffect] if the base type needs to be changed.
+ * @param config immutable parameters that are used for drawing the effect.
+ * @param paintCallback triggered every frame when animation is playing. Use this to draw the effect
+ * with [Canvas.drawPaint].
+ * @param renderEffectCallback triggered every frame when animation is playing. Use this to draw the
+ * effect with [RenderEffect].
+ * @param animationStateChangedCallback triggered when the [AnimationState] changes. Optional.
+ *
+ * The client is responsible to actually draw the [Paint] or [RenderEffect] returned in the
+ * callback. Note that [View.invalidate] must be called on each callback. There are a few ways to
+ * render the effect:
+ * 1) Use [Canvas.drawPaint]. (Preferred. Significantly cheaper!)
+ * 2) Set [RenderEffect] to the [View]. (Good for chaining effects.)
+ * 3) Use [RenderNode.setRenderEffect]. (This may be least preferred, as 2 should do what you want.)
+ *
+ * <p>First approach is more performant than other ones because [RenderEffect] forces an
+ * intermediate render pass of the View to a texture to feed into it.
+ *
+ * <p>If going with the first approach, your custom [View] would look like as follow:
+ * <pre>{@code
+ * private var paint: Paint? = null
+ * // Override [View.onDraw].
+ * override fun onDraw(canvas: Canvas) {
+ * // RuntimeShader requires hardwareAcceleration.
+ * if (!canvas.isHardwareAccelerated) return
+ *
+ * paint?.let { canvas.drawPaint(it) }
+ * }
+ *
+ * // This is called [Callback.onDraw]
+ * fun draw(paint: Paint) {
+ * this.paint = paint
+ *
+ * // Must call invalidate to trigger View#onDraw
+ * invalidate()
+ * }
+ * }</pre>
+ *
+ * <p>If going with the second approach, it doesn't require an extra custom [View], and it is as
+ * simple as calling [View.setRenderEffect] followed by [View.invalidate]. You can also chain the
+ * effect with other [RenderEffect].
+ *
+ * <p>Third approach is an option, but it's more of a boilerplate so you would like to stick with
+ * the second option. If you want to go with this option for some reason, below is the example:
+ * <pre>{@code
+ * // Initialize the shader and paint to use to pass into the [Canvas].
+ * private val renderNode = RenderNode("LoadingEffect")
+ *
+ * // Override [View.onDraw].
+ * override fun onDraw(canvas: Canvas) {
+ * // RuntimeShader requires hardwareAcceleration.
+ * if (!canvas.isHardwareAccelerated) return
+ *
+ * if (renderNode.hasDisplayList()) {
+ * canvas.drawRenderNode(renderNode)
+ * }
+ * }
+ *
+ * // This is called [Callback.onDraw]
+ * fun draw(renderEffect: RenderEffect) {
+ * renderNode.setPosition(0, 0, width, height)
+ * renderNode.setRenderEffect(renderEffect)
+ *
+ * val recordingCanvas = renderNode.beginRecording()
+ * // We need at least 1 drawing instruction.
+ * recordingCanvas.drawColor(Color.TRANSPARENT)
+ * renderNode.endRecording()
+ *
+ * // Must call invalidate to trigger View#onDraw
+ * invalidate()
+ * }
+ * }</pre>
+ */
+class LoadingEffect
+private constructor(
+ baseType: TurbulenceNoiseShader.Companion.Type,
+ private val config: TurbulenceNoiseAnimationConfig,
+ private val paintCallback: PaintDrawCallback?,
+ private val renderEffectCallback: RenderEffectDrawCallback?,
+ private val animationStateChangedCallback: AnimationStateChangedCallback? = null
+) {
+ constructor(
+ baseType: TurbulenceNoiseShader.Companion.Type,
+ config: TurbulenceNoiseAnimationConfig,
+ paintCallback: PaintDrawCallback,
+ animationStateChangedCallback: AnimationStateChangedCallback? = null
+ ) : this(
+ baseType,
+ config,
+ paintCallback,
+ renderEffectCallback = null,
+ animationStateChangedCallback
+ )
+ constructor(
+ baseType: TurbulenceNoiseShader.Companion.Type,
+ config: TurbulenceNoiseAnimationConfig,
+ renderEffectCallback: RenderEffectDrawCallback,
+ animationStateChangedCallback: AnimationStateChangedCallback? = null
+ ) : this(
+ baseType,
+ config,
+ paintCallback = null,
+ renderEffectCallback,
+ animationStateChangedCallback
+ )
+
+ private val turbulenceNoiseShader: TurbulenceNoiseShader =
+ TurbulenceNoiseShader(baseType).apply { applyConfig(config) }
+ private var currentAnimator: ValueAnimator? = null
+ private var state: AnimationState = AnimationState.NOT_PLAYING
+ set(value) {
+ if (field != value) {
+ animationStateChangedCallback?.onStateChanged(field, value)
+ field = value
+ }
+ }
+
+ // We create a paint instance only if the client renders it with Paint.
+ private val paint =
+ if (paintCallback != null) {
+ Paint().apply { this.shader = turbulenceNoiseShader }
+ } else {
+ null
+ }
+
+ /** Plays LoadingEffect. */
+ fun play() {
+ if (state != AnimationState.NOT_PLAYING) {
+ return // Ignore if any of the animation is playing.
+ }
+
+ playEaseIn()
+ }
+
+ // TODO(b/237282226): Support force finish.
+ /** Finishes the main animation, which triggers the ease-out animation. */
+ fun finish() {
+ if (state == AnimationState.MAIN) {
+ // Calling Animator#end sets the animation state back to the initial state. Using pause
+ // to avoid visual artifacts.
+ currentAnimator?.pause()
+ currentAnimator = null
+
+ playEaseOut()
+ }
+ }
+
+ /** Updates the noise color dynamically. */
+ fun updateColor(newColor: Int) {
+ turbulenceNoiseShader.setColor(newColor)
+ }
+
+ /**
+ * Retrieves the noise offset x, y, z values. This is useful for replaying the animation
+ * smoothly from the last animation, by passing in the last values to the next animation.
+ */
+ fun getNoiseOffset(): Array<Float> {
+ return arrayOf(
+ turbulenceNoiseShader.noiseOffsetX,
+ turbulenceNoiseShader.noiseOffsetY,
+ turbulenceNoiseShader.noiseOffsetZ
+ )
+ }
+
+ private fun playEaseIn() {
+ if (state != AnimationState.NOT_PLAYING) {
+ return
+ }
+ state = AnimationState.EASE_IN
+
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = config.easeInDuration.toLong()
+
+ // Animation should start from the initial position to avoid abrupt transition.
+ val initialX = turbulenceNoiseShader.noiseOffsetX
+ val initialY = turbulenceNoiseShader.noiseOffsetY
+ val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+ animator.addUpdateListener { updateListener ->
+ val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+ val progress = updateListener.animatedValue as Float
+
+ turbulenceNoiseShader.setNoiseMove(
+ initialX + timeInSec * config.noiseMoveSpeedX,
+ initialY + timeInSec * config.noiseMoveSpeedY,
+ initialZ + timeInSec * config.noiseMoveSpeedZ
+ )
+
+ // TODO: Replace it with a better curve.
+ turbulenceNoiseShader.setOpacity(progress * config.luminosityMultiplier)
+
+ draw()
+ }
+
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ currentAnimator = null
+ playMain()
+ }
+ }
+ )
+
+ animator.start()
+ this.currentAnimator = animator
+ }
+
+ private fun playMain() {
+ if (state != AnimationState.EASE_IN) {
+ return
+ }
+ state = AnimationState.MAIN
+
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = config.maxDuration.toLong()
+
+ // Animation should start from the initial position to avoid abrupt transition.
+ val initialX = turbulenceNoiseShader.noiseOffsetX
+ val initialY = turbulenceNoiseShader.noiseOffsetY
+ val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+ turbulenceNoiseShader.setOpacity(config.luminosityMultiplier)
+
+ animator.addUpdateListener { updateListener ->
+ val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+ turbulenceNoiseShader.setNoiseMove(
+ initialX + timeInSec * config.noiseMoveSpeedX,
+ initialY + timeInSec * config.noiseMoveSpeedY,
+ initialZ + timeInSec * config.noiseMoveSpeedZ
+ )
+
+ draw()
+ }
+
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ currentAnimator = null
+ playEaseOut()
+ }
+ }
+ )
+
+ animator.start()
+ this.currentAnimator = animator
+ }
+
+ private fun playEaseOut() {
+ if (state != AnimationState.MAIN) {
+ return
+ }
+ state = AnimationState.EASE_OUT
+
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.duration = config.easeOutDuration.toLong()
+
+ // Animation should start from the initial position to avoid abrupt transition.
+ val initialX = turbulenceNoiseShader.noiseOffsetX
+ val initialY = turbulenceNoiseShader.noiseOffsetY
+ val initialZ = turbulenceNoiseShader.noiseOffsetZ
+
+ animator.addUpdateListener { updateListener ->
+ val timeInSec = updateListener.currentPlayTime * MS_TO_SEC
+ val progress = updateListener.animatedValue as Float
+
+ turbulenceNoiseShader.setNoiseMove(
+ initialX + timeInSec * config.noiseMoveSpeedX,
+ initialY + timeInSec * config.noiseMoveSpeedY,
+ initialZ + timeInSec * config.noiseMoveSpeedZ
+ )
+
+ // TODO: Replace it with a better curve.
+ turbulenceNoiseShader.setOpacity((1f - progress) * config.luminosityMultiplier)
+
+ draw()
+ }
+
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ currentAnimator = null
+ state = AnimationState.NOT_PLAYING
+ }
+ }
+ )
+
+ animator.start()
+ this.currentAnimator = animator
+ }
+
+ private fun draw() {
+ paintCallback?.onDraw(paint!!)
+ renderEffectCallback?.onDraw(
+ RenderEffect.createRuntimeShaderEffect(turbulenceNoiseShader, "in_src")
+ )
+ }
+
+ companion object {
+ /**
+ * States of the loading effect animation.
+ *
+ * <p>The state is designed to be follow the order below: [AnimationState.EASE_IN],
+ * [AnimationState.MAIN], [AnimationState.EASE_OUT]. Note that ease in and out don't
+ * necessarily mean the acceleration and deceleration in the animation curve. They simply
+ * mean each stage of the animation. (i.e. Intro, core, and rest)
+ */
+ enum class AnimationState {
+ EASE_IN,
+ MAIN,
+ EASE_OUT,
+ NOT_PLAYING
+ }
+
+ /** Client must implement one of the draw callbacks. */
+ interface PaintDrawCallback {
+ /**
+ * A callback with a [Paint] object that contains shader info, which is triggered every
+ * frame while animation is playing. Note that the [Paint] object here is always the
+ * same instance.
+ */
+ fun onDraw(loadingPaint: Paint)
+ }
+
+ interface RenderEffectDrawCallback {
+ /**
+ * A callback with a [RenderEffect] object that contains shader info, which is triggered
+ * every frame while animation is playing. Note that the [RenderEffect] instance is
+ * different each time to update shader uniforms.
+ */
+ fun onDraw(loadingRenderEffect: RenderEffect)
+ }
+
+ /** Optional callback that is triggered when the animation state changes. */
+ interface AnimationStateChangedCallback {
+ /**
+ * A callback that's triggered when the [AnimationState] changes. Example usage is
+ * performing a cleanup when [AnimationState] becomes [NOT_PLAYING].
+ */
+ fun onStateChanged(oldState: AnimationState, newState: AnimationState) {}
+ }
+
+ private const val MS_TO_SEC = 0.001f
+
+ private val TAG = LoadingEffect::class.java.simpleName
+ }
+}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
index 30108ac..8dd90a8 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/turbulencenoise/TurbulenceNoiseShader.kt
@@ -30,6 +30,7 @@
companion object {
private const val UNIFORMS =
"""
+ uniform shader in_src; // Needed to support RenderEffect.
uniform float in_gridNum;
uniform vec3 in_noiseMove;
uniform vec2 in_size;
@@ -114,6 +115,7 @@
setSize(config.width, config.height)
setLumaMatteFactors(config.lumaMatteBlendFactor, config.lumaMatteOverallBrightness)
setInverseNoiseLuminosity(config.shouldInverseNoiseLuminosity)
+ setNoiseMove(config.noiseOffsetX, config.noiseOffsetY, config.noiseOffsetZ)
}
/** Sets the number of grid for generating noise. */
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 9a34d6f..36ab46b4 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -25,6 +25,7 @@
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
@@ -104,7 +105,7 @@
throwComposeUnavailableError()
}
- override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View {
+ override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
throwComposeUnavailableError()
}
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 51d2a03..5b6aa09 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -40,6 +40,7 @@
import com.android.systemui.communal.ui.compose.CommunalContainer
import com.android.systemui.communal.ui.compose.CommunalHub
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.keyboard.stickykeys.ui.view.createStickyKeyIndicatorView
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
@@ -161,7 +162,7 @@
}
}
- override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View {
+ override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
return ComposeView(context).apply {
setContent { PlatformTheme { CommunalContainer(viewModel = viewModel) } }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index 92bc1f1..bc85513 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -23,7 +23,9 @@
import com.android.compose.animation.scene.updateSceneTransitionLayoutState
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
+import com.android.systemui.communal.ui.compose.extensions.allowGestures
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
@@ -51,7 +53,7 @@
@Composable
fun CommunalContainer(
modifier: Modifier = Modifier,
- viewModel: BaseCommunalViewModel,
+ viewModel: CommunalViewModel,
) {
val currentScene: SceneKey by
viewModel.currentScene
@@ -63,6 +65,7 @@
onChangeScene = { viewModel.onSceneChanged(it.toCommunalSceneKey()) },
transitions = sceneTransitions,
)
+ val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false)
// This effect exposes the SceneTransitionLayout's observable transition state to the rest of
// the system, and unsets it when the view is disposed to avoid a memory leak.
@@ -75,7 +78,7 @@
SceneTransitionLayout(
state = sceneTransitionLayoutState,
- modifier = modifier.fillMaxSize(),
+ modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed),
swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize),
) {
scene(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt
new file mode 100644
index 0000000..a7de1ee
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.composable
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.Edge as ComposeAwareEdge
+import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.TransitionKey as ComposeAwareTransitionKey
+import com.android.compose.animation.scene.UserAction as ComposeAwareUserAction
+import com.android.compose.animation.scene.UserActionDistance as ComposeAwareUserActionDistance
+import com.android.compose.animation.scene.UserActionResult as ComposeAwareUserActionResult
+import com.android.systemui.scene.shared.model.Direction
+import com.android.systemui.scene.shared.model.Edge
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.TransitionKey
+import com.android.systemui.scene.shared.model.UserAction
+import com.android.systemui.scene.shared.model.UserActionDistance
+import com.android.systemui.scene.shared.model.UserActionResult
+
+// TODO(b/293899074): remove this file once we can use the types from SceneTransitionLayout.
+
+fun SceneKey.asComposeAware(): ComposeAwareSceneKey {
+ return ComposeAwareSceneKey(
+ debugName = toString(),
+ identity = this,
+ )
+}
+
+fun TransitionKey.asComposeAware(): ComposeAwareTransitionKey {
+ return ComposeAwareTransitionKey(
+ debugName = debugName,
+ identity = this,
+ )
+}
+
+fun UserAction.asComposeAware(): ComposeAwareUserAction {
+ return when (this) {
+ is UserAction.Swipe ->
+ Swipe(
+ pointerCount = pointerCount,
+ fromSource =
+ when (this.fromEdge) {
+ null -> null
+ Edge.LEFT -> ComposeAwareEdge.Left
+ Edge.TOP -> ComposeAwareEdge.Top
+ Edge.RIGHT -> ComposeAwareEdge.Right
+ Edge.BOTTOM -> ComposeAwareEdge.Bottom
+ },
+ direction =
+ when (this.direction) {
+ Direction.LEFT -> SwipeDirection.Left
+ Direction.UP -> SwipeDirection.Up
+ Direction.RIGHT -> SwipeDirection.Right
+ Direction.DOWN -> SwipeDirection.Down
+ }
+ )
+ is UserAction.Back -> Back
+ }
+}
+
+fun UserActionResult.asComposeAware(): ComposeAwareUserActionResult {
+ val composeUnaware = this
+ return ComposeAwareUserActionResult(
+ toScene = composeUnaware.toScene.asComposeAware(),
+ transitionKey = composeUnaware.transitionKey?.asComposeAware(),
+ distance = composeUnaware.distance?.asComposeAware(),
+ )
+}
+
+fun UserActionDistance.asComposeAware(): ComposeAwareUserActionDistance {
+ val composeUnware = this
+ return object : ComposeAwareUserActionDistance {
+ override fun Density.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return composeUnware.absoluteDistance(
+ fromSceneWidth = fromSceneSize.width,
+ fromSceneHeight = fromSceneSize.height,
+ isHorizontal = orientation == Orientation.Horizontal,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt
new file mode 100644
index 0000000..4c03664
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.composable
+
+import com.android.compose.animation.scene.ObservableTransitionState as ComposeAwareObservableTransitionState
+import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+
+fun ComposeAwareSceneKey.asComposeUnaware(): SceneKey {
+ return this.identity as SceneKey
+}
+
+fun ComposeAwareObservableTransitionState.asComposeUnaware(): ObservableTransitionState {
+ return when (this) {
+ is ComposeAwareObservableTransitionState.Idle ->
+ ObservableTransitionState.Idle(scene.asComposeUnaware())
+ is ComposeAwareObservableTransitionState.Transition ->
+ ObservableTransitionState.Transition(
+ fromScene = fromScene.asComposeUnaware(),
+ toScene = toScene.asComposeUnaware(),
+ progress = progress,
+ isInitiatedByUserInput = isInitiatedByUserInput,
+ isUserInputOngoing = isUserInputOngoing,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
new file mode 100644
index 0000000..60c0b77
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.ui.composable
+
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.observableTransitionState
+import com.android.systemui.scene.shared.model.SceneDataSource
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.TransitionKey
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * An implementation of [SceneDataSource] that's backed by a [MutableSceneTransitionLayoutState].
+ */
+class SceneTransitionLayoutDataSource(
+ private val state: MutableSceneTransitionLayoutState,
+
+ /**
+ * The [CoroutineScope] of the @Composable that's using this, it's critical that this is *not*
+ * the application scope.
+ */
+ private val coroutineScope: CoroutineScope,
+) : SceneDataSource {
+ override val currentScene: StateFlow<SceneKey> =
+ state
+ .observableTransitionState()
+ .flatMapLatest { observableTransitionState ->
+ when (observableTransitionState) {
+ is ObservableTransitionState.Idle -> flowOf(observableTransitionState.scene)
+ is ObservableTransitionState.Transition ->
+ observableTransitionState.isUserInputOngoing.map { isUserInputOngoing ->
+ if (isUserInputOngoing) {
+ observableTransitionState.fromScene
+ } else {
+ observableTransitionState.toScene
+ }
+ }
+ }
+ }
+ .map { it.asComposeUnaware() }
+ .stateIn(
+ scope = coroutineScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = state.transitionState.currentScene.asComposeUnaware(),
+ )
+
+ override fun changeScene(
+ toScene: SceneKey,
+ transitionKey: TransitionKey?,
+ ) {
+ state.setTargetScene(
+ targetScene = toScene.asComposeAware(),
+ transitionKey = transitionKey?.asComposeAware(),
+ coroutineScope = coroutineScope,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
index 72e884e..9c2791f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt
@@ -120,6 +120,7 @@
internal fun promptInfo(
logoRes: Int = -1,
logoBitmap: Bitmap? = null,
+ logoDescription: String? = null,
title: String = "title",
subtitle: String = "sub",
description: String = "desc",
@@ -132,6 +133,7 @@
val info = PromptInfo()
info.logoRes = logoRes
info.logoBitmap = logoBitmap
+ info.logoDescription = logoDescription
info.title = title
info.subtitle = subtitle
info.description = description
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index f70b6a5..b299ca7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -45,6 +45,7 @@
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.MediaHierarchyManager
import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
@@ -102,6 +103,7 @@
testScope,
kosmos.communalInteractor,
kosmos.communalTutorialInteractor,
+ kosmos.shadeInteractor,
mediaHost,
logcatLogBuffer("CommunalViewModelTest"),
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
index 7242cb2..f6c0566 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt
@@ -54,7 +54,7 @@
private lateinit var powerInteractor: PowerInteractor
private lateinit var underTest: LightRevealScrimRepositoryImpl
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Before
fun setUp() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index c23ec22..bf1d76f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -74,6 +74,8 @@
private val dozeParameters = kosmos.dozeParameters
private val underTest by lazy { kosmos.keyguardRootViewModel }
+ private val viewState = ViewStateAccessor()
+
@Before
fun setUp() {
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
@@ -251,7 +253,7 @@
@Test
fun alpha_idleOnHub_isZero() =
testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
+ val alpha by collectLastValue(underTest.alpha(viewState))
// Hub transition state is idle with hub open.
communalRepository.setTransitionState(
@@ -269,7 +271,7 @@
@Test
fun alpha_transitionToHub_isZero() =
testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
+ val alpha by collectLastValue(underTest.alpha(viewState))
keyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
@@ -283,7 +285,7 @@
@Test
fun alpha_transitionFromHubToLockscreen_isOne() =
testScope.runTest {
- val alpha by collectLastValue(underTest.alpha)
+ val alpha by collectLastValue(underTest.alpha(viewState))
// Transition to the glanceable hub and back.
keyguardTransitionRepository.sendTransitionSteps(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt
new file mode 100644
index 0000000..ed4b1e6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.shared.model
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.initialSceneKey
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneDataSourceDelegatorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val initialSceneKey = kosmos.initialSceneKey
+ private val fakeSceneDataSource = kosmos.fakeSceneDataSource
+
+ private val underTest = kosmos.sceneDataSourceDelegator
+
+ @Test
+ fun currentScene_withoutDelegate_startsWithInitialScene() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ underTest.setDelegate(null)
+
+ assertThat(currentScene).isEqualTo(initialSceneKey)
+ }
+
+ @Test
+ fun currentScene_withoutDelegate_doesNothing() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ underTest.setDelegate(null)
+ assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer)
+
+ underTest.changeScene(toScene = SceneKey.Bouncer)
+
+ assertThat(currentScene).isEqualTo(initialSceneKey)
+ }
+
+ @Test
+ fun currentScene_withDelegate_startsWithInitialScene() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isEqualTo(initialSceneKey)
+ }
+
+ @Test
+ fun currentScene_withDelegate_changesScenes() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+ assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer)
+
+ underTest.changeScene(toScene = SceneKey.Bouncer)
+
+ assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
+ }
+
+ @Test
+ fun currentScene_reflectsDelegate() =
+ testScope.runTest {
+ val currentScene by collectLastValue(underTest.currentScene)
+
+ fakeSceneDataSource.changeScene(toScene = SceneKey.Bouncer)
+
+ assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index 0c7ce97..288c083 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -16,10 +16,12 @@
package com.android.systemui.statusbar.notification.collection.render
+import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.assertLogsWtf
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -30,7 +32,7 @@
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
-import org.junit.Assert.assertThrows
+import org.junit.Assume
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -116,9 +118,9 @@
underTest.setGroupExpanded(summary1, false)
// Expanding again should throw.
- assertThrows(IllegalArgumentException::class.java) {
- underTest.setGroupExpanded(summary1, true)
- }
+ // TODO(b/320238410): Remove this check when robolectric supports wtf assertions.
+ Assume.assumeFalse(Build.FINGERPRINT.contains("robolectric"))
+ assertLogsWtf { underTest.setGroupExpanded(summary1, true) }
}
@Test
diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml
index e853f02..4e540de 100644
--- a/packages/SystemUI/res-keyguard/values/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values/dimens.xml
@@ -169,9 +169,6 @@
<dimen name="weather_clock_smartspace_translateX">0dp</dimen>
<dimen name="weather_clock_smartspace_translateY">0dp</dimen>
- <!-- Additional length to add to the SFPS sensor length we get from framework so that the length
- of the progress bar matches the length of the power button -->
- <dimen name="sfps_progress_bar_length_extra_padding">12dp</dimen>
<!-- Thickness of the progress bar we show for the SFPS based authentication. -->
<dimen name="sfps_progress_bar_thickness">6dp</dimen>
<!-- Padding from the edge of the screen for the progress bar -->
diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml
index a877853..0ecdcfc 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml
@@ -13,6 +13,16 @@
android:scaleType="fitXY"
android:visibility="gone" />
+ <TextView
+ android:id="@+id/logo_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ android:visibility="gone"/>
+
<ImageView
android:id="@+id/background"
android:layout_width="0dp"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
index 10f7113..e759074 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -28,6 +28,15 @@
android:scaleType="fitXY"/>
<TextView
+ android:id="@+id/logo_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"/>
+
+ <TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
index a0f916c..ac781ec 100644
--- a/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/bluetooth_tile_dialog.xml
@@ -81,7 +81,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="21dp"
- android:minHeight="145dp"
+ android:minHeight="@dimen/bluetooth_dialog_scroll_view_min_height"
android:fillViewport="true"
app:layout_constrainedHeight="true"
app:layout_constraintStart_toStartOf="parent"
@@ -97,11 +97,11 @@
<TextView
android:id="@+id/bluetooth_toggle_title"
android:layout_width="0dp"
- android:layout_height="64dp"
- android:maxLines="1"
+ android:layout_height="68dp"
+ android:maxLines="2"
android:ellipsize="end"
android:gravity="start|center_vertical"
- android:paddingEnd="0dp"
+ android:paddingEnd="15dp"
android:paddingStart="36dp"
android:text="@string/turn_on_bluetooth"
android:clickable="false"
@@ -114,7 +114,7 @@
<Switch
android:id="@+id/bluetooth_toggle"
android:layout_width="wrap_content"
- android:layout_height="64dp"
+ android:layout_height="68dp"
android:gravity="start|center_vertical"
android:paddingEnd="40dp"
android:contentDescription="@string/turn_on_bluetooth"
@@ -126,14 +126,79 @@
app:layout_constraintStart_toEndOf="@+id/bluetooth_toggle_title"
app:layout_constraintTop_toTopOf="parent" />
+ <androidx.constraintlayout.widget.Group
+ android:id="@+id/bluetooth_auto_on_toggle_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:constraint_referenced_ids="bluetooth_auto_on_toggle_title,bluetooth_auto_on_toggle,bluetooth_auto_on_toggle_info_icon,bluetooth_auto_on_toggle_info_text" />
+
+ <TextView
+ android:id="@+id/bluetooth_auto_on_toggle_title"
+ android:layout_width="0dp"
+ android:layout_height="68dp"
+ android:layout_marginBottom="20dp"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:text="@string/turn_on_bluetooth_auto_tomorrow"
+ android:gravity="start|center_vertical"
+ android:paddingEnd="15dp"
+ android:paddingStart="36dp"
+ android:clickable="false"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"
+ android:textSize="16sp"
+ app:layout_constraintEnd_toStartOf="@+id/bluetooth_auto_on_toggle"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle_title" />
+
+ <Switch
+ android:id="@+id/bluetooth_auto_on_toggle"
+ android:layout_width="wrap_content"
+ android:layout_height="68dp"
+ android:layout_marginBottom="20dp"
+ android:gravity="start|center_vertical"
+ android:paddingEnd="40dp"
+ android:contentDescription="@string/turn_on_bluetooth_auto_tomorrow"
+ android:switchMinWidth="@dimen/settingslib_switch_track_width"
+ android:theme="@style/MainSwitch.Settingslib"
+ android:thumb="@drawable/settingslib_thumb_selector"
+ android:track="@drawable/settingslib_track_selector"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@+id/bluetooth_auto_on_toggle_title"
+ app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" />
+
+ <ImageView
+ android:id="@+id/bluetooth_auto_on_toggle_info_icon"
+ android:src="@drawable/ic_info_outline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:tint="?android:attr/textColorTertiary"
+ android:paddingStart="36dp"
+ android:layout_marginTop="20dp"
+ android:layout_marginBottom="@dimen/bluetooth_dialog_layout_margin"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/bluetooth_auto_on_toggle" />
+
+ <TextView
+ android:id="@+id/bluetooth_auto_on_toggle_info_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:paddingStart="36dp"
+ android:paddingEnd="40dp"
+ android:text="@string/turn_on_bluetooth_auto_info"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/bluetooth_auto_on_toggle_info_icon" />
+
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/device_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle"
- app:layout_constraintBottom_toTopOf="@+id/see_all_button" />
+ app:layout_constraintTop_toBottomOf="@+id/bluetooth_toggle" />
<Button
android:id="@+id/see_all_button"
@@ -168,12 +233,10 @@
android:background="@drawable/bluetooth_tile_dialog_bg_off"
android:layout_width="0dp"
android:layout_height="64dp"
- android:layout_marginBottom="9dp"
android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/see_all_button"
- app:layout_constraintBottom_toTopOf="@+id/done_button"
android:drawableStart="@drawable/ic_add"
android:drawablePadding="20dp"
android:drawableTint="?android:attr/textColorPrimary"
@@ -186,11 +249,19 @@
android:ellipsize="end"
android:visibility="gone" />
+ <androidx.constraintlayout.widget.Barrier
+ android:id="@+id/barrier"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="pair_new_device_button,bluetooth_auto_on_toggle_info_text" />
+
<Button
android:id="@+id/done_button"
style="@style/Widget.Dialog.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:layout_marginTop="9dp"
android:layout_marginBottom="@dimen/dialog_bottom_padding"
android:layout_marginEnd="@dimen/dialog_side_padding"
android:layout_marginStart="@dimen/dialog_side_padding"
@@ -200,7 +271,9 @@
android:maxLines="1"
android:text="@string/inline_done_button"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintBottom_toBottomOf="parent" />
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/barrier"
+ app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index cc31754..7537a00 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1717,6 +1717,10 @@
<dimen name="bluetooth_dialog_layout_margin">16dp</dimen>
<!-- The height of the bluetooth device in bluetooth dialog. -->
<dimen name="bluetooth_dialog_device_height">72dp</dimen>
+ <!-- The height of the main scroll view in bluetooth dialog. -->
+ <dimen name="bluetooth_dialog_scroll_view_min_height">145dp</dimen>
+ <!-- The height of the main scroll view in bluetooth dialog with auto on toggle. -->
+ <dimen name="bluetooth_dialog_scroll_view_min_height_with_auto_on">350dp</dimen>
<!-- Height percentage of the parent container occupied by the communal view -->
<item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 64c6cfa..e401c71 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -669,6 +669,10 @@
<string name="accessibility_quick_settings_bluetooth_device_tap_to_disconnect">disconnect</string>
<!-- QuickSettings: Accessibility label to activate a device [CHAR LIMIT=NONE]-->
<string name="accessibility_quick_settings_bluetooth_device_tap_to_activate">activate</string>
+ <!-- QuickSettings: Bluetooth auto on tomorrow [CHAR LIMIT=NONE]-->
+ <string name="turn_on_bluetooth_auto_tomorrow">Automatically turn on again tomorrow</string>
+ <!-- QuickSettings: Bluetooth auto on info text [CHAR LIMIT=NONE]-->
+ <string name="turn_on_bluetooth_auto_info">Features like Quick Share, Find My Device, and device location use Bluetooth</string>
<!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
<string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index c17c8dc..6133a51c 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -40,6 +40,7 @@
val contentView: PromptContentView? = info.contentView
val logoRes: Int = info.logoRes
val logoBitmap: Bitmap? = info.logoBitmap
+ val logoDescription: String? = info.logoDescription
val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index efad21b..31aadf5 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -95,6 +95,7 @@
view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
val logoView = view.requireViewById<ImageView>(R.id.logo)
+ val logoDescriptionView = view.requireViewById<TextView>(R.id.logo_description)
val titleView = view.requireViewById<TextView>(R.id.title)
val subtitleView = view.requireViewById<TextView>(R.id.subtitle)
val descriptionView = view.requireViewById<TextView>(R.id.description)
@@ -104,6 +105,8 @@
// set selected to enable marquee unless a screen reader is enabled
logoView.isSelected =
!accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled
+ logoDescriptionView.isSelected =
+ !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled
titleView.isSelected =
!accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled
subtitleView.isSelected =
@@ -165,6 +168,7 @@
}
logoView.setImageDrawable(viewModel.logo.first())
+ logoDescriptionView.text = viewModel.logoDescription.first()
titleView.text = viewModel.title.first()
subtitleView.text = viewModel.subtitle.first()
descriptionView.text = viewModel.description.first()
@@ -197,6 +201,7 @@
viewsToHideWhenSmall =
listOf(
logoView,
+ logoDescriptionView,
titleView,
subtitleView,
descriptionView,
@@ -205,6 +210,7 @@
viewsToFadeInOnSizeChange =
listOf(
logoView,
+ logoDescriptionView,
titleView,
subtitleView,
descriptionView,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index ef5c37ea..788991d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics.ui.viewmodel
import android.content.Context
+import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable
@@ -280,8 +281,9 @@
it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap)
else ->
try {
- context.packageManager.getApplicationIcon(it.opPackageName)
- } catch (e: PackageManager.NameNotFoundException) {
+ val info = context.getApplicationInfo(it.opPackageName)
+ context.packageManager.getApplicationIcon(info)
+ } catch (e: Exception) {
Log.w(TAG, "Cannot find icon for package " + it.opPackageName, e)
null
}
@@ -289,6 +291,25 @@
}
.distinctUntilChanged()
+ /** Logo description for the prompt. */
+ val logoDescription: Flow<String> =
+ promptSelectorInteractor.prompt
+ .map {
+ when {
+ !customBiometricPrompt() || it == null -> ""
+ it.logoDescription != null -> it.logoDescription
+ else ->
+ try {
+ val info = context.getApplicationInfo(it.opPackageName)
+ context.packageManager.getApplicationLabel(info).toString()
+ } catch (e: Exception) {
+ Log.w(TAG, "Cannot find name for package " + it.opPackageName, e)
+ ""
+ }
+ }
+ }
+ .distinctUntilChanged()
+
/** Title for the prompt. */
val title: Flow<String> =
promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged()
@@ -682,6 +703,12 @@
}
}
+private fun Context.getApplicationInfo(packageName: String): ApplicationInfo =
+ packageManager.getApplicationInfo(
+ packageName,
+ PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER
+ )
+
/** How the fingerprint sensor was started for the prompt. */
enum class FingerprintStartMode {
/** Fingerprint sensor has not started. */
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
index 40d2d16..febfd4c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt
@@ -28,6 +28,8 @@
import com.android.systemui.media.controls.ui.MediaHost
import com.android.systemui.media.controls.ui.MediaHostState
import com.android.systemui.media.dagger.MediaModule
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.CoroutineScope
@@ -51,6 +53,7 @@
@Application private val scope: CoroutineScope,
private val communalInteractor: CommunalInteractor,
tutorialInteractor: CommunalTutorialInteractor,
+ shadeInteractor: ShadeInteractor,
@Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
@CommunalLog logBuffer: LogBuffer,
) : BaseCommunalViewModel(communalInteractor, mediaHost) {
@@ -81,6 +84,9 @@
override val isPopupOnDismissCtaShowing: Flow<Boolean> =
_isPopupOnDismissCtaShowing.asStateFlow()
+ /** Whether touches should be disabled in communal */
+ val touchesAllowed: Flow<Boolean> = not(shadeInteractor.isAnyFullyExpanded)
+
init {
// Initialize our media host for the UMO. This only needs to happen once and must be done
// before the MediaHierarchyManager attempts to move the UMO to the hub.
@@ -114,6 +120,7 @@
}
private var delayedHidePopupJob: Job? = null
+
private fun schedulePopupHiding() {
cancelDelayedPopupHiding()
delayedHidePopupJob =
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 9a4dfdd..4e23ecd9 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -25,6 +25,7 @@
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
import com.android.systemui.keyguard.shared.model.LockscreenSceneBlueprint
@@ -116,7 +117,7 @@
): View
/** Creates a container that hosts the communal UI and handles gesture transitions. */
- fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View
+ fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View
/** Creates a [View] that represents the Lockscreen. */
fun createLockscreen(
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
index 6f7dcb1..297ad84 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt
@@ -17,7 +17,7 @@
package com.android.systemui.dreams.homecontrols
import android.app.Activity
-import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.content.Intent
import android.graphics.Rect
import android.os.Binder
@@ -122,15 +122,14 @@
/** Creates the task fragment */
fun createTaskFragment() {
- val taskBounds = Rect(activity.resources.configuration.windowConfiguration.bounds)
val fragmentOptions =
TaskFragmentCreationParams.Builder(
organizer.organizerToken,
fragmentToken,
activity.activityToken!!
)
- .setInitialRelativeBounds(taskBounds)
- .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW)
+ .setInitialRelativeBounds(Rect())
+ .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
.build()
organizer.applyTransaction(
WindowContainerTransaction().createTaskFragment(fragmentOptions),
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 41ce3fd..83b2ee2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -22,8 +22,10 @@
import com.android.server.notification.Flags.crossAppPoliteNotifications
import com.android.server.notification.Flags.politeNotifications
import com.android.server.notification.Flags.vibrateWhileUnlocked
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR
import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT
+import com.android.systemui.Flags.communalHub
import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
@@ -63,6 +65,9 @@
ComposeLockscreen.token dependsOn KeyguardShadeMigrationNssl.token
ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor
ComposeLockscreen.token dependsOn migrateClocksToBlueprint
+
+ // CommunalHub dependencies
+ communalHub dependsOn KeyguardShadeMigrationNssl.token
}
private inline val politeNotifications
@@ -75,4 +80,6 @@
get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor())
private inline val migrateClocksToBlueprint
get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint())
+ private inline val communalHub
+ get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub())
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 9e7c70d..1b7a507 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -48,6 +48,7 @@
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.clocks.ClockController
@@ -112,6 +113,10 @@
}
val burnInParams = MutableStateFlow(BurnInParameters())
+ val viewState =
+ ViewStateAccessor(
+ alpha = { view.alpha },
+ )
val disposableHandle =
view.repeatWhenAttached {
@@ -134,7 +139,7 @@
if (keyguardBottomAreaRefactor()) {
launch {
- viewModel.alpha.collect { alpha ->
+ viewModel.alpha(viewState).collect { alpha ->
view.alpha = alpha
childViews[statusViewId]?.alpha = alpha
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 1144efe..f95efaa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -43,6 +43,10 @@
import android.window.InputTransferToken
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
+import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
import androidx.core.view.isInvisible
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch
@@ -393,7 +397,7 @@
),
)
- setUpUdfps(previewContext, rootView)
+ setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView)
if (keyguardBottomAreaRefactor()) {
setupShortcuts(keyguardRootView)
@@ -468,15 +472,6 @@
return
}
- // Place the UDFPS view in the proper sensor location
- val fingerprintLayoutParams =
- FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height())
- fingerprintLayoutParams.setMarginsRelative(
- sensorBounds.left,
- sensorBounds.top,
- sensorBounds.right,
- sensorBounds.bottom
- )
val finger =
LayoutInflater.from(previewContext)
.inflate(
@@ -484,7 +479,31 @@
parentView,
false,
) as View
- parentView.addView(finger, fingerprintLayoutParams)
+
+ // Place the UDFPS view in the proper sensor location
+ if (migrateClocksToBlueprint()) {
+ finger.id = R.id.lock_icon_view
+ parentView.addView(finger)
+ val cs = ConstraintSet()
+ cs.clone(parentView as ConstraintLayout)
+ cs.apply {
+ constrainWidth(R.id.lock_icon_view, sensorBounds.width())
+ constrainHeight(R.id.lock_icon_view, sensorBounds.height())
+ connect(R.id.lock_icon_view, TOP, PARENT_ID, TOP, sensorBounds.top)
+ connect(R.id.lock_icon_view, START, PARENT_ID, START, sensorBounds.left)
+ }
+ cs.applyTo(parentView)
+ } else {
+ val fingerprintLayoutParams =
+ FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height())
+ fingerprintLayoutParams.setMarginsRelative(
+ sensorBounds.left,
+ sensorBounds.top,
+ sensorBounds.right,
+ sensorBounds.bottom
+ )
+ parentView.addView(finger, fingerprintLayoutParams)
+ }
}
private fun setUpClock(previewContext: Context, parentView: ViewGroup) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index d75a72f..75132a5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -24,11 +24,13 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.NotificationPanelView
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -36,6 +38,7 @@
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -52,6 +55,7 @@
ambientState: AmbientState,
controller: NotificationStackScrollLayoutController,
notificationStackSizeCalculator: NotificationStackSizeCalculator,
+ private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
@Main mainDispatcher: CoroutineDispatcher,
) :
NotificationStackScrollLayoutSection(
@@ -74,12 +78,27 @@
val bottomMargin =
context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin)
if (migrateClocksToBlueprint()) {
+ val useLargeScreenHeader =
+ context.resources.getBoolean(R.bool.config_use_large_screen_shade_header)
+ val marginTopLargeScreen =
+ if (centralizedStatusBarDimensRefactor()) {
+ largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+ } else {
+ context.resources.getDimensionPixelSize(
+ R.dimen.large_screen_shade_header_height
+ )
+ }
connect(
R.id.nssl_placeholder,
TOP,
R.id.smart_space_barrier_bottom,
BOTTOM,
- bottomMargin
+ bottomMargin +
+ if (useLargeScreenHeader) {
+ marginTopLargeScreen
+ } else {
+ 0
+ }
)
} else {
connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 756a4cc..3e35ae4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -23,13 +23,11 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.systemui.Flags.centralizedStatusBarDimensRefactor
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.NotificationPanelView
import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -37,7 +35,6 @@
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
-import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -56,7 +53,6 @@
notificationStackSizeCalculator: NotificationStackSizeCalculator,
private val smartspaceViewModel: KeyguardSmartspaceViewModel,
@Main mainDispatcher: CoroutineDispatcher,
- private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
) :
NotificationStackScrollLayoutSection(
context,
@@ -75,16 +71,13 @@
return
}
constraintSet.apply {
- val splitShadeTopMargin =
- if (centralizedStatusBarDimensRefactor()) {
- largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
- } else {
- context.resources.getDimensionPixelSize(
- R.dimen.large_screen_shade_header_height
- )
- }
- connect(R.id.nssl_placeholder, TOP, PARENT_ID, TOP, splitShadeTopMargin)
-
+ connect(
+ R.id.nssl_placeholder,
+ TOP,
+ PARENT_ID,
+ TOP,
+ context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)
+ )
connect(R.id.nssl_placeholder, START, PARENT_ID, START)
connect(R.id.nssl_placeholder, END, PARENT_ID, END)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index ec13228..83be651 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -60,18 +60,21 @@
private val deviceEntryInteractor: DeviceEntryInteractor,
private val dozeParameters: DozeParameters,
private val keyguardInteractor: KeyguardInteractor,
- communalInteractor: CommunalInteractor,
+ private val communalInteractor: CommunalInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
- aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
- lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
- alternateBouncerToGoneTransitionViewModel: AlternateBouncerToGoneTransitionViewModel,
- primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
- lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
- glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
- screenOffAnimationController: ScreenOffAnimationController,
+ private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
+ private val lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel,
+ private val alternateBouncerToGoneTransitionViewModel:
+ AlternateBouncerToGoneTransitionViewModel,
+ private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
+ private val lockscreenToGlanceableHubTransitionViewModel:
+ LockscreenToGlanceableHubTransitionViewModel,
+ private val glanceableHubToLockscreenTransitionViewModel:
+ GlanceableHubToLockscreenTransitionViewModel,
+ private val screenOffAnimationController: ScreenOffAnimationController,
private val aodBurnInViewModel: AodBurnInViewModel,
- aodAlphaViewModel: AodAlphaViewModel,
+ private val aodAlphaViewModel: AodAlphaViewModel,
) {
val burnInLayerVisibility: Flow<Int> =
@@ -101,8 +104,8 @@
val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds
/** An observable for the alpha level for the entire keyguard root view. */
- val alpha: Flow<Float> =
- combine(
+ fun alpha(viewState: ViewStateAccessor): Flow<Float> {
+ return combine(
communalInteractor.isIdleOnCommunal,
// The transitions are mutually exclusive, so they are safe to merge to get the last
// value emitted by any of them. Do not add flows that cannot make this guarantee.
@@ -110,7 +113,7 @@
aodAlphaViewModel.alpha,
lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
- lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+ lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState),
primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
)
@@ -125,6 +128,7 @@
}
}
.distinctUntilChanged()
+ }
/** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */
val lockscreenStateAlpha: Flow<Float> = aodToLockscreenTransitionViewModel.lockscreenAlpha
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
index d981650..15459f4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
@@ -16,10 +16,12 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.util.MathUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow.FlowBuilder
import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -37,7 +39,7 @@
animationFlow: KeyguardTransitionAnimationFlow,
) : DeviceEntryIconTransition {
- private val transitionAnimation =
+ private val transitionAnimation: FlowBuilder =
animationFlow.setup(
duration = FromLockscreenTransitionInteractor.TO_GONE_DURATION,
from = KeyguardState.LOCKSCREEN,
@@ -52,7 +54,26 @@
onCancel = { 1f },
)
- val lockscreenAlpha: Flow<Float> = shortcutsAlpha
+ fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+ var startAlpha: Float? = null
+ return transitionAnimation.sharedFlow(
+ duration = 200.milliseconds,
+ onStep = {
+ if (startAlpha == null) {
+ startAlpha = viewState.alpha()
+ }
+ MathUtils.lerp(startAlpha!!, 0f, it)
+ },
+ onFinish = {
+ startAlpha = null
+ 0f
+ },
+ onCancel = {
+ startAlpha = null
+ 1f
+ },
+ )
+ }
override val deviceEntryParentViewAlpha: Flow<Float> =
transitionAnimation.immediatelyTransitionTo(0f)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
index 67c42f0..00e5d35 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
@@ -67,8 +67,8 @@
@Inject
constructor(
private val context: Context,
- private val biometricStatusInteractor: BiometricStatusInteractor,
- private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
+ biometricStatusInteractor: BiometricStatusInteractor,
+ deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
private val sfpsSensorInteractor: SideFpsSensorInteractor,
// todo (b/317432075) Injecting DozeServiceHost directly instead of using it through
// DozeInteractor as DozeServiceHost already depends on DozeInteractor.
@@ -89,9 +89,6 @@
_progress.value = 0.0f
}
- private val additionalSensorLengthPadding =
- context.resources.getDimension(R.dimen.sfps_progress_bar_length_extra_padding).toInt()
-
// Merged [FingerprintAuthenticationStatus] from BiometricPrompt acquired messages and
// device entry authentication messages
private val mergedFingerprintAuthenticationStatus =
@@ -114,9 +111,7 @@
val progress: Flow<Float> = _progress.asStateFlow()
val progressBarLength: Flow<Int> =
- sfpsSensorInteractor.sensorLocation
- .map { it.length + additionalSensorLengthPadding }
- .distinctUntilChanged()
+ sfpsSensorInteractor.sensorLocation.map { it.length }.distinctUntilChanged()
val progressBarThickness =
context.resources.getDimension(R.dimen.sfps_progress_bar_thickness).toInt()
@@ -128,7 +123,6 @@
context.resources
.getDimension(R.dimen.sfps_progress_bar_padding_from_edge)
.toInt()
- val lengthOfTheProgressBar = sensorLocation.length + additionalSensorLengthPadding
val viewLeftTop = Point(sensorLocation.left, sensorLocation.top)
val totalDistanceFromTheEdge = paddingFromEdge + progressBarThickness
@@ -139,7 +133,7 @@
// Sensor is vertical to the current orientation, we rotate it 270 deg
// around the (left,top) point as the pivot. We need to push it down the
// length of the progress bar so that it is still aligned to the sensor
- viewLeftTop.y += lengthOfTheProgressBar
+ viewLeftTop.y += sensorLocation.length
val isSensorOnTheNearEdge =
rotation == DisplayRotation.ROTATION_180 ||
rotation == DisplayRotation.ROTATION_90
@@ -164,7 +158,6 @@
// We want to push it up from the bottom edge by the padding and
// the thickness of the progressbar.
viewLeftTop.y -= totalDistanceFromTheEdge
- viewLeftTop.x -= additionalSensorLengthPadding
}
}
viewLeftTop
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt
new file mode 100644
index 0000000..cb5db86
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/ViewStateAccessor.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.ui.viewmodel
+
+/** View-level state information to be shared between ui and viewmodel. */
+data class ViewStateAccessor(
+ val alpha: () -> Float = { 0f },
+ val translationY: () -> Int = { 0 },
+ val translationX: () -> Int = { 0 },
+)
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
index 40a9b9c..13d743f 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/SeekBarViewModel.kt
@@ -141,6 +141,7 @@
if (field != value) {
field = value
checkIfPollingNeeded()
+ _data = _data.copy(listening = value)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
new file mode 100644
index 0000000..dcae088
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractor.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** Interactor class responsible for interacting with the Bluetooth Auto-On feature. */
+@SysUISingleton
+class BluetoothAutoOnInteractor
+@Inject
+constructor(
+ private val bluetoothAutoOnRepository: BluetoothAutoOnRepository,
+) {
+
+ val isEnabled = bluetoothAutoOnRepository.getValue.map { it == ENABLED }.distinctUntilChanged()
+
+ /**
+ * Checks if the auto on value is present in the repository.
+ *
+ * @return `true` if a value is present (i.e, the feature is enabled by the Bluetooth server).
+ */
+ suspend fun isValuePresent(): Boolean = bluetoothAutoOnRepository.isValuePresent()
+
+ /**
+ * Sets enabled or disabled based on the provided value.
+ *
+ * @param value `true` to enable the feature, `false` to disable it.
+ */
+ suspend fun setEnabled(value: Boolean) {
+ if (!isValuePresent()) {
+ Log.e(TAG, "Trying to set toggle value while feature not available.")
+ } else {
+ val newValue = if (value) ENABLED else DISABLED
+ bluetoothAutoOnRepository.setValue(newValue)
+ }
+ }
+
+ companion object {
+ private const val TAG = "BluetoothAutoOnInteractor"
+ const val DISABLED = 0
+ const val ENABLED = 1
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
new file mode 100644
index 0000000..e17b4d3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepository.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.systemui.qs.tiles.dialog.bluetooth
+
+import android.os.UserHandle
+import android.util.Log
+import com.android.systemui.dagger.SysUISingleton
+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.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.withContext
+
+/** Repository class responsible for managing the Bluetooth Auto-On feature settings. */
+// TODO(b/316822488): Handle multi-user
+@SysUISingleton
+class BluetoothAutoOnRepository
+@Inject
+constructor(
+ private val secureSettings: SecureSettings,
+ private val userRepository: UserRepository,
+ @Application private val coroutineScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ // Flow representing the auto on setting value
+ internal val getValue: Flow<Int> =
+ secureSettings
+ .observerFlow(UserHandle.USER_SYSTEM, SETTING_NAME)
+ .onStart { emit(Unit) }
+ .map {
+ if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+ Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+ return@map UNSET
+ }
+ secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM)
+ }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
+
+ /**
+ * Checks if the auto on setting value is ever set for the current user.
+ *
+ * @return `true` if the setting value is not UNSET, `false` otherwise.
+ */
+ suspend fun isValuePresent(): Boolean =
+ withContext(backgroundDispatcher) {
+ if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+ Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+ false
+ } else {
+ secureSettings.getIntForUser(SETTING_NAME, UNSET, UserHandle.USER_SYSTEM) != UNSET
+ }
+ }
+
+ /**
+ * Sets the Bluetooth Auto-On setting value for the current user.
+ *
+ * @param value The new setting value to be applied.
+ */
+ suspend fun setValue(value: Int) {
+ withContext(backgroundDispatcher) {
+ if (userRepository.getSelectedUserInfo().id != UserHandle.USER_SYSTEM) {
+ Log.i(TAG, "Current user is not USER_SYSTEM. Multi-user is not supported")
+ } else {
+ secureSettings.putIntForUser(SETTING_NAME, value, UserHandle.USER_SYSTEM)
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "BluetoothAutoOnRepository"
+ const val SETTING_NAME = "bluetooth_automatic_turn_on"
+ const val UNSET = -1
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
index 1a06c38..6b53c7a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt
@@ -56,7 +56,7 @@
internal class BluetoothTileDialog
constructor(
private val bluetoothToggleInitialValue: Boolean,
- private val subtitleResIdInitialValue: Int,
+ private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties,
private val cachedContentHeight: Int,
private val bluetoothTileDialogCallback: BluetoothTileDialogCallback,
@Main private val mainDispatcher: CoroutineDispatcher,
@@ -71,6 +71,10 @@
internal val bluetoothStateToggle
get() = mutableBluetoothStateToggle.asStateFlow()
+ private val mutableBluetoothAutoOnToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null)
+ internal val bluetoothAutoOnToggle
+ get() = mutableBluetoothAutoOnToggle.asStateFlow()
+
private val mutableDeviceItemClick: MutableSharedFlow<DeviceItem> =
MutableSharedFlow(extraBufferCapacity = 1)
internal val deviceItemClick
@@ -89,6 +93,8 @@
private lateinit var toggleView: Switch
private lateinit var subtitleTextView: TextView
+ private lateinit var autoOnToggle: Switch
+ private lateinit var autoOnToggleView: View
private lateinit var doneButton: View
private lateinit var seeAllButton: View
private lateinit var pairNewDeviceButton: View
@@ -108,6 +114,8 @@
toggleView = requireViewById(R.id.bluetooth_toggle)
subtitleTextView = requireViewById(R.id.bluetooth_tile_dialog_subtitle) as TextView
+ autoOnToggle = requireViewById(R.id.bluetooth_auto_on_toggle)
+ autoOnToggleView = requireViewById(R.id.bluetooth_auto_on_toggle_layout)
doneButton = requireViewById(R.id.done_button)
seeAllButton = requireViewById(R.id.see_all_button)
pairNewDeviceButton = requireViewById(R.id.pair_new_device_button)
@@ -116,7 +124,7 @@
setupToggle()
setupRecyclerView()
- subtitleTextView.text = context.getString(subtitleResIdInitialValue)
+ subtitleTextView.text = context.getString(initialUiProperties.subTitleResId)
doneButton.setOnClickListener { dismiss() }
seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) }
pairNewDeviceButton.setOnClickListener {
@@ -124,7 +132,9 @@
}
requireViewById<View>(R.id.scroll_view).apply {
scrollViewContent = this
- layoutParams.height = cachedContentHeight
+ minimumHeight =
+ resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId)
+ layoutParams.height = maxOf(cachedContentHeight, minimumHeight)
}
progressBarAnimation = requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
progressBarBackground = requireViewById(R.id.bluetooth_tile_dialog_progress_background)
@@ -178,13 +188,27 @@
}
}
- internal fun onBluetoothStateUpdated(isEnabled: Boolean, subtitleResId: Int) {
+ internal fun onBluetoothStateUpdated(
+ isEnabled: Boolean,
+ uiProperties: BluetoothTileDialogViewModel.UiProperties
+ ) {
toggleView.apply {
isChecked = isEnabled
setEnabled(true)
alpha = ENABLED_ALPHA
}
- subtitleTextView.text = context.getString(subtitleResId)
+ subtitleTextView.text = context.getString(uiProperties.subTitleResId)
+ autoOnToggleView.visibility = uiProperties.autoOnToggleVisibility
+ }
+
+ internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean) {
+ if (::autoOnToggle.isInitialized) {
+ autoOnToggle.apply {
+ isChecked = isEnabled
+ setEnabled(true)
+ alpha = ENABLED_ALPHA
+ }
+ }
}
private fun setupToggle() {
@@ -198,6 +222,16 @@
logger.logBluetoothState(BluetoothStateStage.USER_TOGGLED, isChecked.toString())
uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED)
}
+
+ autoOnToggleView.visibility = initialUiProperties.autoOnToggleVisibility
+ autoOnToggle.setOnCheckedChangeListener { view, isChecked ->
+ mutableBluetoothAutoOnToggle.value = isChecked
+ view.apply {
+ isEnabled = false
+ alpha = DISABLED_ALPHA
+ }
+ uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_AUTO_ON_TOGGLE_CLICKED)
+ }
}
private fun setupRecyclerView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
index 86e5dde..cd52e0d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogUiEvent.kt
@@ -31,7 +31,8 @@
@UiEvent(doc = "Saved clicked to connect") SAVED_DEVICE_CONNECT(1500),
@UiEvent(doc = "Active device clicked to disconnect") ACTIVE_DEVICE_DISCONNECT(1507),
@UiEvent(doc = "Connected other device clicked to disconnect")
- CONNECTED_OTHER_DEVICE_DISCONNECT(1508);
+ CONNECTED_OTHER_DEVICE_DISCONNECT(1508),
+ @UiEvent(doc = "The auto on toggle is clicked") BLUETOOTH_AUTO_ON_TOGGLE_CLICKED(1617);
override fun getId() = metricId
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
index 54bb95c..5a14e5f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt
@@ -21,9 +21,15 @@
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
import android.view.ViewGroup
+import androidx.annotation.DimenRes
+import androidx.annotation.StringRes
+import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.flags.Flags.bluetoothQsTileDialogAutoOnToggle
import com.android.systemui.Prefs
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
@@ -58,6 +64,7 @@
constructor(
private val deviceItemInteractor: DeviceItemInteractor,
private val bluetoothStateInteractor: BluetoothStateInteractor,
+ private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val activityStarter: ActivityStarter,
private val systemClock: SystemClock,
@@ -143,7 +150,10 @@
bluetoothStateInteractor.bluetoothStateUpdate
.filterNotNull()
.onEach {
- dialog.onBluetoothStateUpdated(it, getSubtitleResId(it))
+ dialog.onBluetoothStateUpdated(
+ it,
+ UiProperties.build(it, isAutoOnToggleFeatureAvailable())
+ )
updateDeviceItemJob?.cancel()
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(
@@ -177,6 +187,21 @@
}
.launchIn(this)
+ if (isAutoOnToggleFeatureAvailable()) {
+ // bluetoothAutoOnUpdate is emitted when bluetooth auto on on/off state is
+ // changed.
+ bluetoothAutoOnInteractor.isEnabled
+ .onEach { dialog.onBluetoothAutoOnUpdated(it) }
+ .launchIn(this)
+
+ // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on
+ // switch, send the new value to the bluetoothAutoOnInteractor.
+ dialog.bluetoothAutoOnToggle
+ .filterNotNull()
+ .onEach { bluetoothAutoOnInteractor.setEnabled(it) }
+ .launchIn(this)
+ }
+
produce<Unit> { awaitClose { dialog.cancel() } }
}
}
@@ -192,7 +217,10 @@
return BluetoothTileDialog(
bluetoothStateInteractor.isBluetoothEnabled,
- getSubtitleResId(bluetoothStateInteractor.isBluetoothEnabled),
+ UiProperties.build(
+ bluetoothStateInteractor.isBluetoothEnabled,
+ isAutoOnToggleFeatureAvailable()
+ ),
cachedContentHeight,
this@BluetoothTileDialogViewModel,
mainDispatcher,
@@ -244,6 +272,10 @@
}
}
+ @VisibleForTesting
+ internal suspend fun isAutoOnToggleFeatureAvailable() =
+ bluetoothQsTileDialogAutoOnToggle() && bluetoothAutoOnInteractor.isValuePresent()
+
companion object {
private const val INTERACTION_JANK_TAG = "bluetooth_tile_dialog"
private const val CONTENT_HEIGHT_PREF_KEY = Prefs.Key.BLUETOOTH_TILE_DIALOG_CONTENT_HEIGHT
@@ -251,6 +283,29 @@
if (isBluetoothEnabled) R.string.quick_settings_bluetooth_tile_subtitle
else R.string.bt_is_off
}
+
+ internal data class UiProperties(
+ @StringRes val subTitleResId: Int,
+ val autoOnToggleVisibility: Int,
+ @DimenRes val scrollViewMinHeightResId: Int,
+ ) {
+ companion object {
+ internal fun build(
+ isBluetoothEnabled: Boolean,
+ isAutoOnToggleFeatureAvailable: Boolean
+ ) =
+ UiProperties(
+ subTitleResId = getSubtitleResId(isBluetoothEnabled),
+ autoOnToggleVisibility =
+ if (isAutoOnToggleFeatureAvailable && !isBluetoothEnabled) VISIBLE
+ else GONE,
+ scrollViewMinHeightResId =
+ if (isAutoOnToggleFeatureAvailable)
+ R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on
+ else R.dimen.bluetooth_dialog_scroll_view_min_height
+ )
+ }
+ }
}
internal interface BluetoothTileDialogCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
new file mode 100644
index 0000000..f7b45e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.shared.model
+
+import kotlinx.coroutines.flow.StateFlow
+
+/** Defines interface for classes that provide access to scene state. */
+interface SceneDataSource {
+
+ /**
+ * The current scene, as seen by the real data source in the UI layer.
+ *
+ * During a transition between two scenes, the original scene will still be reflected in
+ * [currentScene] until a time when the UI layer decides to commit the change, which is when
+ * [currentScene] will have the value of the target/new scene.
+ */
+ val currentScene: StateFlow<SceneKey>
+
+ /**
+ * Asks for an asynchronous scene switch to [toScene], which will use the corresponding
+ * installed transition or the one specified by [transitionKey], if provided.
+ */
+ fun changeScene(
+ toScene: SceneKey,
+ transitionKey: TransitionKey? = null,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
new file mode 100644
index 0000000..a50830c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.shared.model
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.stateIn
+
+/**
+ * Delegates calls to a runtime-provided [SceneDataSource] or to a no-op implementation if a
+ * delegate isn't set.
+ */
+@SysUISingleton
+class SceneDataSourceDelegator
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ config: SceneContainerConfig,
+) : SceneDataSource {
+
+ private val noOpDelegate = NoOpSceneDataSource(config.initialSceneKey)
+ private val delegateMutable = MutableStateFlow<SceneDataSource>(noOpDelegate)
+
+ override val currentScene: StateFlow<SceneKey> =
+ delegateMutable
+ .flatMapLatest { delegate -> delegate.currentScene }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = config.initialSceneKey,
+ )
+
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
+ delegateMutable.value.changeScene(
+ toScene = toScene,
+ transitionKey = transitionKey,
+ )
+ }
+
+ /**
+ * Binds the current, dependency injection provided [SceneDataSource] to the given object.
+ *
+ * In other words: once this is invoked, the state and functionality of the [SceneDataSource]
+ * will be served by the given [delegate].
+ *
+ * If `null` is passed in, the delegator will use a no-op implementation of [SceneDataSource].
+ *
+ * This removes any previously set delegate.
+ */
+ fun setDelegate(delegate: SceneDataSource?) {
+ delegateMutable.value = delegate ?: noOpDelegate
+ }
+
+ private class NoOpSceneDataSource(
+ initialSceneKey: SceneKey,
+ ) : SceneDataSource {
+ override val currentScene: StateFlow<SceneKey> =
+ MutableStateFlow(initialSceneKey).asStateFlow()
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt
new file mode 100644
index 0000000..87332ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/TransitionKey.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.shared.model
+
+/**
+ * Key for a transition. This can be used to specify which transition spec should be used when
+ * starting the transition between two scenes.
+ */
+data class TransitionKey(
+ val debugName: String,
+ val identity: Any = Object(),
+)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt
new file mode 100644
index 0000000..b93f837
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionDistance.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.shared.model
+
+interface UserActionDistance {
+
+ /**
+ * Return the **absolute** distance of the user action (in pixels) given the size of the scene
+ * we are animating from and the orientation.
+ */
+ fun absoluteDistance(fromSceneWidth: Int, fromSceneHeight: Int, isHorizontal: Boolean): Float
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt
new file mode 100644
index 0000000..e1b96e4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/UserActionResult.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.shared.model
+
+data class UserActionResult(
+
+ /** The scene we should be transitioning due to the [UserAction]. */
+ val toScene: SceneKey,
+
+ /**
+ * The distance the action takes to animate from 0% to 100%.
+ *
+ * If `null`, a default distance will be used depending on the [UserAction] performed.
+ */
+ val distance: UserActionDistance? = null,
+
+ /**
+ * The key of the transition that should be used, if a specific one should be used.
+ *
+ * If `null`, the transition used will be the corresponding transition from the collection
+ * passed into the UI layer.
+ */
+ val transitionKey: TransitionKey? = null,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
index 90abec1..80c3551 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/RemoteInputNotificationRebuilder.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
+import static android.app.Flags.lifetimeExtensionRefactor;
+
import android.annotation.NonNull;
import android.app.Notification;
import android.app.RemoteInputHistoryItem;
@@ -29,6 +31,7 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
@@ -68,7 +71,7 @@
@NonNull
public StatusBarNotification rebuildForCanceledSmartReplies(
NotificationEntry entry) {
- return rebuildWithRemoteInputInserted(entry, null /* remoteInputTest */,
+ return rebuildWithRemoteInputInserted(entry, null /* remoteInputText */,
false /* showSpinner */, null /* mimeType */, null /* uri */);
}
@@ -97,22 +100,50 @@
StatusBarNotification rebuildWithRemoteInputInserted(NotificationEntry entry,
CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
StatusBarNotification sbn = entry.getSbn();
-
Notification.Builder b = Notification.Builder
.recoverBuilder(mContext, sbn.getNotification().clone());
- if (remoteInputText != null || uri != null) {
- RemoteInputHistoryItem newItem = uri != null
- ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
- : new RemoteInputHistoryItem(remoteInputText);
+
+ if (lifetimeExtensionRefactor()) {
+ if (entry.remoteInputs == null) {
+ entry.remoteInputs = new ArrayList<RemoteInputHistoryItem>();
+ }
+
+ // Append new remote input information to remoteInputs list
+ if (remoteInputText != null || uri != null) {
+ RemoteInputHistoryItem newItem = uri != null
+ ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+ : new RemoteInputHistoryItem(remoteInputText);
+ // The list is latest-first, so new elements should be added as the first element.
+ entry.remoteInputs.add(0, newItem);
+ }
+
+ // Read the whole remoteInputs list from the entry, then append all of those to the sbn.
Parcelable[] oldHistoryItems = sbn.getNotification().extras
.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+
RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
? Stream.concat(
- Stream.of(newItem),
- Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+ entry.remoteInputs.stream(),
+ Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
.toArray(RemoteInputHistoryItem[]::new)
- : new RemoteInputHistoryItem[] { newItem };
+ : entry.remoteInputs.toArray(RemoteInputHistoryItem[]::new);
b.setRemoteInputHistory(newHistoryItems);
+
+ } else {
+ if (remoteInputText != null || uri != null) {
+ RemoteInputHistoryItem newItem = uri != null
+ ? new RemoteInputHistoryItem(mimeType, uri, remoteInputText)
+ : new RemoteInputHistoryItem(remoteInputText);
+ Parcelable[] oldHistoryItems = sbn.getNotification().extras
+ .getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
+ RemoteInputHistoryItem[] newHistoryItems = oldHistoryItems != null
+ ? Stream.concat(
+ Stream.of(newItem),
+ Arrays.stream(oldHistoryItems).map(p -> (RemoteInputHistoryItem) p))
+ .toArray(RemoteInputHistoryItem[]::new)
+ : new RemoteInputHistoryItem[]{newItem};
+ b.setRemoteInputHistory(newHistoryItems);
+ }
}
b.setShowRemoteInputSpinner(showSpinner);
b.setHideSmartReplies(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index cdacb10..8678f0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -40,6 +40,7 @@
import android.app.NotificationManager.Policy;
import android.app.Person;
import android.app.RemoteInput;
+import android.app.RemoteInputHistoryItem;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.net.Uri;
@@ -127,6 +128,7 @@
public int targetSdk;
private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
public CharSequence remoteInputText;
+ public List<RemoteInputHistoryItem> remoteInputs = null;
public String remoteInputMimeType;
public Uri remoteInputUri;
public ContentInfo remoteInputAttachment;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
index 918bf08..28fff15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinator.kt
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.collection.coordinator
+import android.app.Flags.lifetimeExtensionRefactor
+import android.app.Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
import android.os.Handler
import android.service.notification.NotificationListenerService.REASON_CANCEL
import android.service.notification.NotificationListenerService.REASON_CLICK
@@ -88,11 +90,21 @@
override fun attach(pipeline: NotifPipeline) {
mNotificationRemoteInputManager.setRemoteInputListener(this)
- mRemoteInputLifetimeExtenders.forEach { pipeline.addNotificationLifetimeExtender(it) }
+ if (lifetimeExtensionRefactor()) {
+ pipeline.addNotificationLifetimeExtender(mRemoteInputActiveExtender)
+ } else {
+ mRemoteInputLifetimeExtenders.forEach {
+ pipeline.addNotificationLifetimeExtender(it)
+ }
+ }
mNotifUpdater = pipeline.getInternalNotifUpdater(TAG)
pipeline.addCollectionListener(mCollectionListener)
}
+ /*
+ * Listener that updates the appearance of the notification if it has been lifetime extended
+ * by a a direct reply or a smart reply, and cancelled.
+ */
val mCollectionListener = object : NotifCollectionListener {
override fun onEntryUpdated(entry: NotificationEntry, fromSystem: Boolean) {
if (DEBUG) {
@@ -100,9 +112,32 @@
" fromSystem=$fromSystem)")
}
if (fromSystem) {
- // Mark smart replies as sent whenever a notification is updated by the app,
- // otherwise the smart replies are never marked as sent.
- mSmartReplyController.stopSending(entry)
+ if (lifetimeExtensionRefactor()) {
+ if ((entry.getSbn().getNotification().flags
+ and FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+ if (mNotificationRemoteInputManager.shouldKeepForRemoteInputHistory(
+ entry)) {
+ val newSbn = mRebuilder.rebuildForRemoteInputReply(entry)
+ entry.onRemoteInputInserted()
+ mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ "Extending lifetime of notification with remote input")
+ } else if (mNotificationRemoteInputManager.shouldKeepForSmartReplyHistory(
+ entry)) {
+ val newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry)
+ mSmartReplyController.stopSending(entry)
+ mNotifUpdater.onInternalNotificationUpdate(newSbn,
+ "Extending lifetime of notification with smart reply")
+ }
+ } else {
+ // Notifications updated without FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY
+ // should have their remote inputs list cleared.
+ entry.remoteInputs = null
+ }
+ } else {
+ // Mark smart replies as sent whenever a notification is updated by the app,
+ // otherwise the smart replies are never marked as sent.
+ mSmartReplyController.stopSending(entry)
+ }
}
}
@@ -130,8 +165,10 @@
// NOTE: This is some trickery! By removing the lifetime extensions when we know they should
// be immediately re-upped, we ensure that the side-effects of the lifetime extenders get to
// fire again, thus ensuring that we add subsequent replies to the notification.
- mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
- mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+ if (!lifetimeExtensionRefactor()) {
+ mRemoteInputHistoryExtender.endLifetimeExtension(entry.key)
+ mSmartReplyHistoryExtender.endLifetimeExtension(entry.key)
+ }
// If we're extending for remote input being active, then from the apps point of
// view it is already canceled, so we'll need to cancel it on the apps behalf
@@ -160,15 +197,19 @@
}
override fun isNotificationKeptForRemoteInputHistory(key: String) =
+ if (!lifetimeExtensionRefactor()) {
mRemoteInputHistoryExtender.isExtending(key) ||
mSmartReplyHistoryExtender.isExtending(key)
+ } else false
override fun releaseNotificationIfKeptForRemoteInputHistory(entry: NotificationEntry) {
if (DEBUG) Log.d(TAG, "releaseNotificationIfKeptForRemoteInputHistory(entry=${entry.key})")
- mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
- mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
- REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ if (!lifetimeExtensionRefactor()) {
+ mRemoteInputHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ mSmartReplyHistoryExtender.endLifetimeExtensionAfterDelay(entry.key,
+ REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
+ }
mRemoteInputActiveExtender.endLifetimeExtensionAfterDelay(entry.key,
REMOTE_INPUT_EXTENDER_RELEASE_DELAY)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index 3cdb2cd..d1aff80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar.notification.collection.render;
+import android.util.Log;
+
import androidx.annotation.NonNull;
import com.android.systemui.Dumpable;
@@ -40,6 +42,8 @@
*/
@SysUISingleton
public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpable {
+ private static final String TAG = "GroupExpansionaManagerImpl";
+
private final DumpManager mDumpManager;
private final GroupMembershipManager mGroupMembershipManager;
private final Set<OnGroupExpansionChangeListener> mOnGroupChangeListeners = new HashSet<>();
@@ -100,7 +104,7 @@
NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
if (entry.getParent() == null) {
if (expanded) {
- throw new IllegalArgumentException("Cannot expand group that is not attached");
+ Log.wtf(TAG, "Cannot expand group that is not attached");
} else {
// The entry is no longer attached, but we still want to make sure we don't have
// a stale expansion state.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index de3a626..c8d6abe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -311,12 +311,13 @@
boundViewsByNotifKey[it.notifKey]?.first
}
val childCount = view.childCount
+ val toRemove = mutableListOf<View>()
for (i in 0 until childCount) {
val actual = view.getChildAt(i)
val expected = expectedChildren.getOrNull(i)
if (expected == null) {
Log.wtf(TAG, "[$logTag] Unexpected child $actual")
- view.removeView(actual)
+ toRemove.add(actual)
continue
}
if (actual === expected) {
@@ -325,6 +326,9 @@
view.removeView(expected)
view.addView(expected, i)
}
+ for (child in toRemove) {
+ view.removeView(child)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt
new file mode 100644
index 0000000..dd81d42
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationThrottleHun.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notification throttle hun flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationThrottleHun {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATION_THROTTLE_HUN
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.notificationThrottleHun()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 47daf49..830b8c1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -1335,6 +1335,10 @@
}
}
+ public float getAlpha() {
+ return mView.getAlpha();
+ }
+
public void setSuppressChildrenMeasureAndLayout(boolean suppressLayout) {
mView.suppressChildrenMeasureAndLayout(suppressLayout);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
index b4f578f..ffab9ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt
@@ -76,14 +76,10 @@
}
val nsslId = R.id.notification_stack_scroller
constraintSet.apply {
- connect(nsslId, START, startConstraintId, START)
- connect(nsslId, END, PARENT_ID, END)
- connect(nsslId, BOTTOM, PARENT_ID, BOTTOM)
- connect(nsslId, TOP, PARENT_ID, TOP)
- setMargin(nsslId, START, marginStart)
- setMargin(nsslId, END, marginEnd)
- setMargin(nsslId, TOP, marginTop)
- setMargin(nsslId, BOTTOM, marginBottom)
+ connect(nsslId, START, startConstraintId, START, marginStart)
+ connect(nsslId, END, PARENT_ID, END, marginEnd)
+ connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom)
+ connect(nsslId, TOP, PARENT_ID, TOP, marginTop)
}
constraintSet.applyTo(this)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 97db9b6..daea8af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -24,6 +24,7 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
@@ -75,6 +76,10 @@
}
val burnInParams = MutableStateFlow(BurnInParameters())
+ val viewState =
+ ViewStateAccessor(
+ alpha = { controller.getAlpha() },
+ )
/*
* For animation sensitive coroutines, immediately run just like applicationScope does
@@ -141,7 +146,7 @@
if (!sceneContainerFlags.isEnabled()) {
launch {
- viewModel.expansionAlpha.collect {
+ viewModel.expansionAlpha(viewState).collect {
controller.setMaxAlphaForExpansion(it)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 811da51..ff00cb3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -50,6 +50,7 @@
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
@@ -67,7 +68,6 @@
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
@@ -100,21 +100,35 @@
setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
private val edgeToAlphaViewModel =
- mapOf<Edge?, Flow<Float>>(
+ mapOf<Edge?, (ViewStateAccessor) -> Flow<Float>>(
Edge(from = LOCKSCREEN, to = DREAMING) to
- lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ lockscreenToDreamingTransitionViewModel.lockscreenAlpha
+ },
Edge(from = LOCKSCREEN, to = GONE) to
- lockscreenToGoneTransitionViewModel.lockscreenAlpha,
+ { viewState: ViewStateAccessor ->
+ lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState)
+ },
Edge(from = ALTERNATE_BOUNCER, to = GONE) to
- alternateBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ alternateBouncerToGoneTransitionViewModel.lockscreenAlpha
+ },
Edge(from = PRIMARY_BOUNCER, to = GONE) to
- primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ primaryBouncerToGoneTransitionViewModel.lockscreenAlpha
+ },
Edge(from = DREAMING, to = LOCKSCREEN) to
- dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ dreamingToLockscreenTransitionViewModel.lockscreenAlpha
+ },
Edge(from = LOCKSCREEN, to = OCCLUDED) to
- lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ lockscreenToOccludedTransitionViewModel.lockscreenAlpha
+ },
Edge(from = OCCLUDED, to = LOCKSCREEN) to
- occludedToLockscreenTransitionViewModel.lockscreenAlpha,
+ { _: ViewStateAccessor ->
+ occludedToLockscreenTransitionViewModel.lockscreenAlpha
+ },
)
private val lockscreenTransitionInProgress: Flow<Edge?> =
@@ -151,21 +165,20 @@
val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
interactor.configurationBasedDimensions
.map {
+ val marginTop =
+ if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop
ConfigurationBasedDimensions(
marginStart = if (it.useSplitShade) 0 else it.marginHorizontal,
marginEnd = it.marginHorizontal,
marginBottom = it.marginBottom,
- marginTop =
- if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop,
+ marginTop = marginTop,
useSplitShade = it.useSplitShade,
paddingTop =
if (it.useSplitShade) {
- // When in split shade, the margin is applied twice as the legacy shade
- // code uses it to calculate padding.
- it.keyguardSplitShadeTopMargin - 2 * it.marginTopLargeScreen
+ marginTop
} else {
0
- }
+ },
)
}
.distinctUntilChanged()
@@ -255,13 +268,15 @@
isOnLockscreenWithoutShade,
keyguardInteractor.notificationContainerBounds,
configurationBasedDimensions,
- interactor.topPosition.sampleCombine(
- keyguardTransitionInteractor.isInTransitionToAnyState,
- shadeInteractor.qsExpansion,
- ),
+ interactor.topPosition
+ .sampleCombine(
+ keyguardTransitionInteractor.isInTransitionToAnyState,
+ shadeInteractor.qsExpansion,
+ )
+ .onStart { emit(Triple(0f, false, 0f)) }
) { onLockscreen, bounds, config, (top, isInTransitionToAnyState, qsExpansion) ->
if (onLockscreen) {
- bounds.copy(top = bounds.top + config.paddingTop)
+ bounds.copy(top = bounds.top - config.paddingTop)
} else {
// When QS expansion > 0, it should directly set the top padding so do not
// animate it
@@ -278,46 +293,63 @@
initialValue = NotificationContainerBounds(),
)
- /** As QS is expanding, fade out notifications unless in splitshade */
- private val alphaForQsExpansion: Flow<Float> =
- interactor.configurationBasedDimensions.flatMapLatest {
- if (it.useSplitShade) {
- flowOf(1f)
- } else {
- shadeInteractor.qsExpansion.map { 1f - it }
+ /**
+ * Ensure view is visible when the shade/qs are expanded. Also, as QS is expanding, fade out
+ * notifications unless in splitshade.
+ */
+ private val alphaForShadeAndQsExpansion: Flow<Float> =
+ interactor.configurationBasedDimensions
+ .flatMapLatest { configurationBasedDimensions ->
+ combine(
+ shadeInteractor.shadeExpansion,
+ shadeInteractor.qsExpansion,
+ ) { shadeExpansion, qsExpansion ->
+ if (shadeExpansion > 0f || qsExpansion > 0f) {
+ if (configurationBasedDimensions.useSplitShade) {
+ 1f
+ } else {
+ // Fade as QS shade expands
+ 1f - qsExpansion
+ }
+ } else {
+ // Not visible unless the shade/qs is visible
+ 0f
+ }
+ }
}
- }
+ .distinctUntilChanged()
- val expansionAlpha: Flow<Float> =
+ fun expansionAlpha(viewState: ViewStateAccessor): Flow<Float> {
// Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
// such as when the shade resets. This can happen while the transition to/from LOCKSCREEN
// is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
// those transitions are in progress. Without this, the alpha value will produce a visible
// flicker.
- lockscreenTransitionInProgress
+ return lockscreenTransitionInProgress
.flatMapLatest { edge ->
- edgeToAlphaViewModel.getOrElse(
+ edgeToAlphaViewModel.getOrDefault(
edge,
- {
+ { _: ViewStateAccessor ->
isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade ->
combineTransform(
keyguardInteractor.keyguardAlpha,
shadeCollpaseFadeIn,
- alphaForQsExpansion,
- ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion ->
+ alphaForShadeAndQsExpansion,
+ ) { alpha, shadeCollpaseFadeIn, alphaForShadeAndQsExpansion ->
if (isOnLockscreenWithoutShade) {
if (!shadeCollpaseFadeIn) {
emit(alpha)
}
} else {
- emit(alphaForQsExpansion)
+ emit(alphaForShadeAndQsExpansion)
}
}
}
}
- )
+ )(viewState)
}
.distinctUntilChanged()
+ }
/**
* Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index ca3e3c6..db55da7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -43,6 +43,7 @@
*/
public class KeyguardClockPositionAlgorithm {
private static final String TAG = "KeyguardClockPositionAlgorithm";
+ private static final boolean DEBUG = false;
/**
* Margin between the bottom of the status view and the notification shade.
@@ -318,24 +319,26 @@
}
float fullyDarkBurnInOffset = burnInPreventionOffsetY(burnInPreventionOffsetY);
- float clockYDark = clockY
- + fullyDarkBurnInOffset
- + shift;
+ float clockYDark = clockY + fullyDarkBurnInOffset + shift;
mCurrentBurnInOffsetY = MathUtils.lerp(0, fullyDarkBurnInOffset, darkAmount);
- final String inputs = "panelExpansion: " + panelExpansion + " darkAmount: " + darkAmount;
- final String outputs = "clockY: " + clockY
- + " burnInPreventionOffsetY: " + burnInPreventionOffsetY
- + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset
- + " shift: " + shift
- + " mOverStretchAmount: " + mOverStretchAmount
- + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY;
- mLogger.i(msg -> {
- return msg.getStr1() + " -> " + msg.getStr2();
- }, msg -> {
- msg.setStr1(inputs);
- msg.setStr2(outputs);
- return kotlin.Unit.INSTANCE;
- });
+
+ if (DEBUG) {
+ final float finalShift = shift;
+ final float finalBurnInPreventionOffsetY = burnInPreventionOffsetY;
+ mLogger.i(msg -> {
+ final String inputs = "panelExpansion: " + panelExpansion
+ + " darkAmount: " + darkAmount;
+ final String outputs = "clockY: " + clockY
+ + " burnInPreventionOffsetY: " + finalBurnInPreventionOffsetY
+ + " fullyDarkBurnInOffset: " + fullyDarkBurnInOffset
+ + " shift: " + finalShift
+ + " mOverStretchAmount: " + mOverStretchAmount
+ + " mCurrentBurnInOffsetY: " + mCurrentBurnInOffsetY;
+ return inputs + " -> " + outputs;
+ }, msg -> {
+ return kotlin.Unit.INSTANCE;
+ });
+ }
return (int) (MathUtils.lerp(clockY, clockYDark, darkAmount) + mOverStretchAmount);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 45bdae8..8ac3b4a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -132,9 +132,9 @@
if (updateDisplayParameters()) {
updateLayoutForCutout();
requestLayout();
- if (truncatedStatusBarIconsFix()) {
- updateWindowHeight();
- }
+ }
+ if (truncatedStatusBarIconsFix()) {
+ updateWindowHeight();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 9f4a906..f397627 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -444,7 +444,8 @@
UserHandle.of(userId))) {
boolean hasCACerts = !(conn.getService().getUserCaAliases().getList().isEmpty());
idWithCert = new Pair<Integer, Boolean>(userId, hasCACerts);
- } catch (RemoteException | InterruptedException | AssertionError e) {
+ } catch (RemoteException | InterruptedException | AssertionError
+ | IllegalStateException e) {
Log.i(TAG, "failed to get CA certs", e);
idWithCert = new Pair<Integer, Boolean>(userId, null);
} finally {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
index d4c180d..2b0a92c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -16,11 +16,14 @@
package com.android.systemui.statusbar.policy;
+import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
+
import static com.android.server.notification.Flags.screenshareNotificationHiding;
import android.annotation.MainThread;
import android.app.IActivityManager;
import android.content.Context;
+import android.database.ExecutorContentObserver;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
import android.os.Handler;
@@ -37,6 +40,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.util.Assert;
import com.android.systemui.util.ListenerSet;
+import com.android.systemui.util.settings.GlobalSettings;
import java.util.concurrent.Executor;
@@ -50,6 +54,7 @@
private final ArraySet<String> mExemptPackages = new ArraySet<>();
private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
private volatile MediaProjectionInfo mProjection;
+ boolean mDisableScreenShareProtections = false;
@VisibleForTesting
final MediaProjectionManager.Callback mMediaProjectionCallback =
@@ -58,6 +63,12 @@
public void onStart(MediaProjectionInfo info) {
Trace.beginSection("SNPC.onProjectionStart");
try {
+ if (mDisableScreenShareProtections) {
+ Log.w(LOG_TAG,
+ "Screen share protections disabled, ignoring projectionstart");
+ return;
+ }
+
// Only enable sensitive content protection if sharing full screen
// Launch cookie only set (non-null) if sharing single app/task
updateProjectionStateAndNotifyListeners(
@@ -81,6 +92,7 @@
@Inject
public SensitiveNotificationProtectionControllerImpl(
Context context,
+ GlobalSettings settings,
MediaProjectionManager mediaProjectionManager,
IActivityManager activityManager,
@Main Handler mainHandler,
@@ -89,6 +101,25 @@
return;
}
+ ExecutorContentObserver developerOptionsObserver = new ExecutorContentObserver(bgExecutor) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ boolean disableScreenShareProtections = settings.getInt(
+ DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS,
+ 0) != 0;
+ mainHandler.post(() -> {
+ mDisableScreenShareProtections = disableScreenShareProtections;
+ });
+ }
+ };
+ settings.registerContentObserver(
+ DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS,
+ developerOptionsObserver);
+
+ // Get current setting value
+ bgExecutor.execute(() -> developerOptionsObserver.onChange(true));
+
bgExecutor.execute(() -> {
ArraySet<String> exemptPackages = new ArraySet<>();
// Exempt SystemUI
diff --git a/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
index 0fe2283..f23fbee 100644
--- a/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
@@ -34,7 +34,7 @@
@RunWithLooper
class AnimatorTestRuleIsolationTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Test
fun testA() {
diff --git a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
index cc7f7e4..fd5f157 100644
--- a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
@@ -31,7 +31,7 @@
@RunWithLooper
class AnimatorTestRulePrecisionTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
var value1: Float = -1f
var value2: Float = -1f
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index e6637e6..cd19259 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -22,6 +22,7 @@
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -259,6 +260,7 @@
@Test
fun keyguardCallback_visibilityChanged_clockDozeCalled() =
runBlocking(IMMEDIATE) {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
verify(keyguardUpdateMonitor).registerCallback(capture(captor))
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index fad8552..e893eb1 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -56,7 +56,7 @@
public class KeyguardStatusViewControllerTest extends KeyguardStatusViewControllerBaseTest {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Test
public void dozeTimeTick_updatesSlice() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
index ba27fcd..dd428f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ExpandHelperTest.java
@@ -48,7 +48,7 @@
public class ExpandHelperTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
private ExpandableNotificationRow mRow;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
index e006d59..64936862 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java
@@ -82,7 +82,7 @@
public class WindowMagnificationAnimationControllerTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
private static final float DEFAULT_SCALE = 4.0f;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
index 2225ad6..f1b0c18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java
@@ -129,7 +129,8 @@
public class WindowMagnificationControllerTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ // NOTE: pass 'null' to allow this test advances time on the main thread.
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(null);
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
index a35a509..08b49e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java
@@ -132,7 +132,7 @@
public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase {
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Rule
public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
index 2b51ac5..f07932c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/AnimatorTestRuleOrderTest.kt
@@ -19,7 +19,6 @@
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import androidx.core.animation.doOnEnd
-import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.doOnEnd
@@ -31,10 +30,9 @@
@RunWith(AndroidTestingRunner::class)
@SmallTest
@RunWithLooper
-@FlakyTest(bugId = 302149604)
class AnimatorTestRuleOrderTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
var value1: Float = -1f
var value2: Float = -1f
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
index a46167a42..8fab233 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt
@@ -26,6 +26,7 @@
@Test
fun biometricRequestFromPromptInfo() {
val logoRes = R.drawable.ic_cake
+ val logoDescription = "test cake"
val title = "what"
val subtitle = "a"
val description = "request"
@@ -41,6 +42,7 @@
BiometricPromptRequest.Biometric(
promptInfo(
logoRes = logoRes,
+ logoDescription = logoDescription,
title = title,
subtitle = subtitle,
description = description,
@@ -53,6 +55,7 @@
)
assertThat(request.logoRes).isEqualTo(logoRes)
+ assertThat(request.logoDescription).isEqualTo(logoDescription)
assertThat(request.title).isEqualTo(title)
assertThat(request.subtitle).isEqualTo(subtitle)
assertThat(request.description).isEqualTo(description)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 2e94d38..ff68fe3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.biometrics.ui.viewmodel
+import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.Bitmap
@@ -74,6 +75,8 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
@@ -95,6 +98,8 @@
@Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
@Mock private lateinit var udfpsUtils: UdfpsUtils
@Mock private lateinit var packageManager: PackageManager
+ @Mock private lateinit var applicationInfoWithIcon: ApplicationInfo
+ @Mock private lateinit var applicationInfoNoIcon: ApplicationInfo
private val fakeExecutor = FakeExecutor(FakeSystemClock())
private val testScope = TestScope()
@@ -102,6 +107,8 @@
private val logoResFromApp = R.drawable.ic_cake
private val logoFromApp = context.getDrawable(logoResFromApp)
private val logoBitmapFromApp = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565)
+ private val defaultLogoDescription = "Test Android App"
+ private val logoDescriptionFromApp = "Test Cake App"
private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository
private lateinit var promptRepository: FakePromptRepository
@@ -166,7 +173,14 @@
iconViewModel = viewModel.iconViewModel
// Set up default logo icon and app customized icon
- whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon)
+ whenever(packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME_NO_ICON), anyInt()))
+ .thenReturn(applicationInfoNoIcon)
+ whenever(packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME), anyInt()))
+ .thenReturn(applicationInfoWithIcon)
+ whenever(packageManager.getApplicationIcon(applicationInfoWithIcon))
+ .thenReturn(defaultLogoIcon)
+ whenever(packageManager.getApplicationLabel(applicationInfoWithIcon))
+ .thenReturn(defaultLogoDescription)
context.setMockPackageManager(packageManager)
val resources = context.getOrCreateTestableResources()
resources.addOverride(logoResFromApp, logoFromApp)
@@ -1277,6 +1291,29 @@
assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp)
}
+ @Test
+ fun logoDescriptionIsEmptyIfPackageNameNotFound() =
+ runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ val logoDescription by collectLastValue(viewModel.logoDescription)
+ assertThat(logoDescription).isEqualTo("")
+ }
+
+ @Test
+ fun defaultLogoDescriptionIfNoLogoDescriptionSet() = runGenericTest {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ val logoDescription by collectLastValue(viewModel.logoDescription)
+ assertThat(logoDescription).isEqualTo(defaultLogoDescription)
+ }
+
+ @Test
+ fun logoDescriptionSetByApp() =
+ runGenericTest(logoDescription = logoDescriptionFromApp) {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
+ val logoDescription by collectLastValue(viewModel.logoDescription)
+ assertThat(logoDescription).isEqualTo(logoDescriptionFromApp)
+ }
+
/** Asserts that the selected buttons are visible now. */
private suspend fun TestScope.assertButtonsVisible(
tryAgain: Boolean = false,
@@ -1300,6 +1337,7 @@
contentView: PromptContentView? = null,
logoRes: Int = -1,
logoBitmap: Bitmap? = null,
+ logoDescription: String? = null,
packageName: String = OP_PACKAGE_NAME,
block: suspend TestScope.() -> Unit,
) {
@@ -1312,6 +1350,7 @@
contentViewFromApp = contentView,
logoResFromApp = logoRes,
logoBitmapFromApp = logoBitmap,
+ logoDescriptionFromApp = logoDescription,
packageName = packageName,
)
@@ -1492,12 +1531,14 @@
contentViewFromApp: PromptContentView? = null,
logoResFromApp: Int = -1,
logoBitmapFromApp: Bitmap? = null,
+ logoDescriptionFromApp: String? = null,
packageName: String = OP_PACKAGE_NAME,
) {
val info =
PromptInfo().apply {
logoRes = logoResFromApp
logoBitmap = logoBitmapFromApp
+ logoDescription = logoDescriptionFromApp
title = "t"
subtitle = "s"
description = descriptionFromApp
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
index 5b29a86..7787a7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardSurfaceBehindParamsApplierTest.kt
@@ -43,7 +43,7 @@
@RunWithLooper(setAsMainLooper = true)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class KeyguardSurfaceBehindParamsApplierTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
private lateinit var underTest: KeyguardSurfaceBehindParamsApplier
private lateinit var executor: FakeExecutor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
index 8b05a54..e7aaddd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
@@ -19,6 +19,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -56,6 +57,41 @@
deviceEntryParentViewAlpha.forEach { assertThat(it).isEqualTo(0f) }
}
+ @Test
+ fun lockscreenAlphaStartsFromViewStateAccessorAlpha() =
+ testScope.runTest {
+ val viewState = ViewStateAccessor(alpha = { 0.5f })
+ val alpha by collectLastValue(underTest.lockscreenAlpha(viewState))
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+ repository.sendTransitionStep(step(0f))
+ assertThat(alpha).isEqualTo(0.5f)
+
+ repository.sendTransitionStep(step(0.25f))
+ assertThat(alpha).isEqualTo(0.25f)
+
+ repository.sendTransitionStep(step(.5f))
+ assertThat(alpha).isEqualTo(0f)
+ }
+
+ @Test
+ fun lockscreenAlphaWithNoViewStateAccessorValue() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.lockscreenAlpha(ViewStateAccessor()))
+
+ repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+ repository.sendTransitionStep(step(0f))
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(0.25f))
+ assertThat(alpha).isEqualTo(0f)
+
+ repository.sendTransitionStep(step(0.5f))
+ assertThat(alpha).isEqualTo(0f)
+ }
+
private fun step(
value: Float,
state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
index 100e579..4ec29ce 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/models/player/SeekBarObserverTest.kt
@@ -164,6 +164,18 @@
}
@Test
+ fun seekbarNotListeningNotScrubbingPlaying() {
+ // WHEN playing
+ val isPlaying = true
+ val isScrubbing = false
+ val data =
+ SeekBarViewModel.Progress(true, true, isPlaying, isScrubbing, 3000, 120000, false)
+ observer.onChanged(data)
+ // THEN progress drawable is not animating
+ verify(mockSquigglyProgress).animate = false
+ }
+
+ @Test
fun seekBarPlayingScrubbing() {
// WHEN playing & scrubbing
val isPlaying = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
index bbae0c9..ae2a9ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
@@ -40,7 +40,7 @@
@SmallTest
class QSIconViewImplTest_311121830 : SysuiTestCase() {
- @get:Rule val animatorRule = AnimatorTestRule()
+ @get:Rule val animatorRule = AnimatorTestRule(this)
@Test
fun alwaysLastIcon() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
new file mode 100644
index 0000000..3710713
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnInteractorTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.qs.tiles.dialog.bluetooth
+
+import android.content.pm.UserInfo
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BluetoothAutoOnInteractorTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private var secureSettings: FakeSettings = FakeSettings()
+ private val userRepository: FakeUserRepository = FakeUserRepository()
+ private lateinit var bluetoothAutoOnInteractor: BluetoothAutoOnInteractor
+
+ @Before
+ fun setUp() {
+ bluetoothAutoOnInteractor =
+ BluetoothAutoOnInteractor(
+ BluetoothAutoOnRepository(
+ secureSettings,
+ userRepository,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+ )
+ }
+
+ @Test
+ fun testSet_bluetoothAutoOnUnset_doNothing() {
+ testScope.runTest {
+ bluetoothAutoOnInteractor.setEnabled(true)
+
+ val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
+
+ runCurrent()
+
+ Truth.assertThat(actualValue).isEqualTo(false)
+ }
+ }
+
+ @Test
+ fun testSet_bluetoothAutoOnSet_setNewValue() {
+ testScope.runTest {
+ userRepository.setUserInfos(listOf(SYSTEM_USER))
+ secureSettings.putIntForUser(
+ BluetoothAutoOnRepository.SETTING_NAME,
+ BluetoothAutoOnInteractor.DISABLED,
+ SYSTEM_USER_ID
+ )
+ bluetoothAutoOnInteractor.setEnabled(true)
+
+ val actualValue by collectLastValue(bluetoothAutoOnInteractor.isEnabled)
+
+ runCurrent()
+
+ Truth.assertThat(actualValue).isEqualTo(true)
+ }
+ }
+
+ companion object {
+ private const val SYSTEM_USER_ID = 0
+ private val SYSTEM_USER =
+ UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.kt
new file mode 100644
index 0000000..8986d99
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothAutoOnRepositoryTest.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.systemui.qs.tiles.dialog.bluetooth
+
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.DISABLED
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnInteractor.Companion.ENABLED
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.SETTING_NAME
+import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothAutoOnRepository.Companion.UNSET
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class BluetoothAutoOnRepositoryTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private var secureSettings: FakeSettings = FakeSettings()
+ private val userRepository: FakeUserRepository = FakeUserRepository()
+
+ private lateinit var bluetoothAutoOnRepository: BluetoothAutoOnRepository
+
+ @Before
+ fun setUp() {
+ bluetoothAutoOnRepository =
+ BluetoothAutoOnRepository(
+ secureSettings,
+ userRepository,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+
+ userRepository.setUserInfos(listOf(SECONDARY_USER, SYSTEM_USER))
+ }
+
+ @Test
+ fun testGetValue_valueUnset() {
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(SYSTEM_USER)
+ val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+ runCurrent()
+
+ assertThat(actualValue).isEqualTo(UNSET)
+ assertThat(bluetoothAutoOnRepository.isValuePresent()).isFalse()
+ }
+ }
+
+ @Test
+ fun testGetValue_valueFalse() {
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(SYSTEM_USER)
+ val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+ secureSettings.putIntForUser(SETTING_NAME, DISABLED, UserHandle.USER_SYSTEM)
+ runCurrent()
+
+ assertThat(actualValue).isEqualTo(DISABLED)
+ }
+ }
+
+ @Test
+ fun testGetValue_valueTrue() {
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(SYSTEM_USER)
+ val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+ secureSettings.putIntForUser(SETTING_NAME, ENABLED, UserHandle.USER_SYSTEM)
+ runCurrent()
+
+ assertThat(actualValue).isEqualTo(ENABLED)
+ }
+ }
+
+ @Test
+ fun testGetValue_valueTrue_secondaryUser_returnUnset() {
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(SECONDARY_USER)
+ val actualValue by collectLastValue(bluetoothAutoOnRepository.getValue)
+
+ secureSettings.putIntForUser(SETTING_NAME, ENABLED, SECONDARY_USER_ID)
+ runCurrent()
+
+ assertThat(actualValue).isEqualTo(UNSET)
+ }
+ }
+
+ companion object {
+ private const val SYSTEM_USER_ID = 0
+ private const val SECONDARY_USER_ID = 1
+ private val SYSTEM_USER =
+ UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+ private val SECONDARY_USER =
+ UserInfo(/* id= */ SECONDARY_USER_ID, /* name= */ "secondary user", /* flags= */ 0)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
index 154aa1c..70b0417 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt
@@ -71,7 +71,11 @@
@Mock private lateinit var logger: BluetoothTileDialogLogger
- private val subtitleResId = R.string.quick_settings_bluetooth_tile_subtitle
+ private val uiProperties =
+ BluetoothTileDialogViewModel.UiProperties.build(
+ isBluetoothEnabled = ENABLED,
+ isAutoOnToggleFeatureAvailable = ENABLED
+ )
private val fakeSystemClock = FakeSystemClock()
@@ -90,7 +94,7 @@
bluetoothTileDialog =
BluetoothTileDialog(
ENABLED,
- subtitleResId,
+ uiProperties,
CONTENT_HEIGHT,
bluetoothTileDialogCallback,
dispatcher,
@@ -131,7 +135,7 @@
bluetoothTileDialog =
BluetoothTileDialog(
ENABLED,
- subtitleResId,
+ uiProperties,
CONTENT_HEIGHT,
bluetoothTileDialogCallback,
dispatcher,
@@ -166,7 +170,7 @@
val viewHolder =
BluetoothTileDialog(
ENABLED,
- subtitleResId,
+ uiProperties,
CONTENT_HEIGHT,
bluetoothTileDialogCallback,
dispatcher,
@@ -194,7 +198,7 @@
val viewHolder =
BluetoothTileDialog(
ENABLED,
- subtitleResId,
+ uiProperties,
CONTENT_HEIGHT,
bluetoothTileDialogCallback,
dispatcher,
@@ -219,7 +223,7 @@
bluetoothTileDialog =
BluetoothTileDialog(
ENABLED,
- subtitleResId,
+ uiProperties,
CONTENT_HEIGHT,
bluetoothTileDialogCallback,
dispatcher,
@@ -253,12 +257,36 @@
}
@Test
- fun testShowDialog_displayFromCachedHeight() {
+ fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() {
+ testScope.runTest {
+ val cachedHeight = Int.MAX_VALUE
+ bluetoothTileDialog =
+ BluetoothTileDialog(
+ ENABLED,
+ uiProperties,
+ cachedHeight,
+ bluetoothTileDialogCallback,
+ dispatcher,
+ fakeSystemClock,
+ uiEventLogger,
+ logger,
+ mContext
+ )
+ bluetoothTileDialog.show()
+ assertThat(
+ bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height
+ )
+ .isEqualTo(cachedHeight)
+ }
+ }
+
+ @Test
+ fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() {
testScope.runTest {
bluetoothTileDialog =
BluetoothTileDialog(
ENABLED,
- subtitleResId,
+ uiProperties,
MATCH_PARENT,
bluetoothTileDialogCallback,
dispatcher,
@@ -271,7 +299,32 @@
assertThat(
bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height
)
- .isEqualTo(MATCH_PARENT)
+ .isGreaterThan(MATCH_PARENT)
+ }
+ }
+
+ @Test
+ fun testShowDialog_bluetoothEnabled_autoOnToggleGone() {
+ testScope.runTest {
+ bluetoothTileDialog =
+ BluetoothTileDialog(
+ ENABLED,
+ BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED),
+ MATCH_PARENT,
+ bluetoothTileDialogCallback,
+ dispatcher,
+ fakeSystemClock,
+ uiEventLogger,
+ logger,
+ mContext
+ )
+ bluetoothTileDialog.show()
+ assertThat(
+ bluetoothTileDialog
+ .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout)
+ .visibility
+ )
+ .isEqualTo(GONE)
}
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
index 98ac17b..cb9f4b4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt
@@ -17,20 +17,27 @@
package com.android.systemui.qs.tiles.dialog.bluetooth
import android.content.SharedPreferences
+import android.content.pm.UserInfo
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.flags.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.nullable
+import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -84,16 +91,36 @@
private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var testScope: TestScope
+ private lateinit var secureSettings: FakeSettings
+ private lateinit var userRepository: FakeUserRepository
@Before
fun setUp() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
scheduler = TestCoroutineScheduler()
dispatcher = UnconfinedTestDispatcher(scheduler)
testScope = TestScope(dispatcher)
+ secureSettings = FakeSettings()
+ userRepository = FakeUserRepository()
+ userRepository.setUserInfos(listOf(SYSTEM_USER))
+ secureSettings.putIntForUser(
+ BluetoothAutoOnRepository.SETTING_NAME,
+ BluetoothAutoOnInteractor.ENABLED,
+ SYSTEM_USER_ID
+ )
bluetoothTileDialogViewModel =
BluetoothTileDialogViewModel(
deviceItemInteractor,
bluetoothStateInteractor,
+ // TODO(b/316822488): Create FakeBluetoothAutoOnInteractor.
+ BluetoothAutoOnInteractor(
+ BluetoothAutoOnRepository(
+ secureSettings,
+ userRepository,
+ testScope.backgroundScope,
+ dispatcher
+ )
+ ),
mDialogTransitionAnimator,
activityStarter,
fakeSystemClock,
@@ -174,4 +201,64 @@
verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable())
}
}
+
+ @Test
+ fun testBuildUiProperties_bluetoothOn_shouldHideAutoOn() {
+ testScope.runTest {
+ val actual =
+ BluetoothTileDialogViewModel.UiProperties.build(
+ isBluetoothEnabled = true,
+ isAutoOnToggleFeatureAvailable = true
+ )
+ assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun testBuildUiProperties_bluetoothOff_shouldShowAutoOn() {
+ testScope.runTest {
+ val actual =
+ BluetoothTileDialogViewModel.UiProperties.build(
+ isBluetoothEnabled = false,
+ isAutoOnToggleFeatureAvailable = true
+ )
+ assertThat(actual.autoOnToggleVisibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun testBuildUiProperties_bluetoothOff_autoOnFeatureUnavailable_shouldHideAutoOn() {
+ testScope.runTest {
+ val actual =
+ BluetoothTileDialogViewModel.UiProperties.build(
+ isBluetoothEnabled = false,
+ isAutoOnToggleFeatureAvailable = false
+ )
+ assertThat(actual.autoOnToggleVisibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun testIsAutoOnToggleFeatureAvailable_flagOn_settingValueSet_returnTrue() {
+ testScope.runTest {
+ val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
+ assertThat(actual).isTrue()
+ }
+ }
+
+ @Test
+ fun testIsAutoOnToggleFeatureAvailable_flagOff_settingValueSet_returnFalse() {
+ testScope.runTest {
+ mSetFlagsRule.disableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
+
+ val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable()
+ assertThat(actual).isFalse()
+ }
+ }
+
+ companion object {
+ private const val SYSTEM_USER_ID = 0
+ private val SYSTEM_USER =
+ UserInfo(/* id= */ SYSTEM_USER_ID, /* name= */ "system user", /* flags= */ 0)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index 0c4bf81..ab5e51c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -207,6 +207,7 @@
@Test
fun testDragDownHelperCalledWhenDraggingDown() =
testScope.runTest {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
whenever(dragDownHelper.isDraggingDown).thenReturn(true)
val now = SystemClock.elapsedRealtime()
val ev = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0f, 0f, 0 /* meta */)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
index c226121..4cc1234 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt
@@ -26,6 +26,7 @@
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR
import com.android.systemui.SysuiTestCase
import com.android.systemui.fragments.FragmentHostManager
@@ -400,6 +401,7 @@
@Test
fun testSplitShadeLayout_isAlignedToGuideline() {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
enableSplitShade()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline)
@@ -409,6 +411,7 @@
@Test
fun testSinglePaneLayout_childrenHaveEqualMargins() {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
disableSplitShade()
underTest.updateResources()
val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin
@@ -425,6 +428,7 @@
@Test
fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
enableSplitShade()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0)
@@ -443,6 +447,7 @@
@Test
fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResource() {
mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
setLargeScreen()
val largeScreenHeaderResourceHeight = 100
val largeScreenHeaderHelperHeight = 200
@@ -465,6 +470,7 @@
@Test
fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelper() {
mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR)
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
setLargeScreen()
val largeScreenHeaderResourceHeight = 100
val largeScreenHeaderHelperHeight = 200
@@ -486,6 +492,7 @@
@Test
fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
setSmallScreen()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0)
@@ -506,6 +513,7 @@
@Test
fun testSinglePaneShadeLayout_isAlignedToParent() {
+ mSetFlagsRule.disableFlags(AConfigFlags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL)
disableSplitShade()
underTest.updateResources()
assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
index 8be2ef0..452302d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt
@@ -51,7 +51,7 @@
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
private lateinit var controller: SystemEventChipAnimationController
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Mock private lateinit var sbWindowController: StatusBarWindowController
@Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
index 875fe58..cacfa8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -76,7 +76,7 @@
private lateinit var chipAnimationController: SystemEventChipAnimationController
private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Before
fun setup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
index 039fef9..82093ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt
@@ -54,7 +54,7 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class NotificationWakeUpCoordinatorTest : SysuiTestCase() {
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
private val kosmos = Kosmos()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
index 7073cc7..85b8b03 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/RemoteInputCoordinatorTest.kt
@@ -15,7 +15,13 @@
*/
package com.android.systemui.statusbar.notification.collection.coordinator
+import android.app.Flags.lifetimeExtensionRefactor
+import android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR
+import android.app.Notification
+import android.app.RemoteInputHistoryItem
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
@@ -34,6 +40,7 @@
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.captureMany
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -42,6 +49,7 @@
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.never
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks
@@ -57,6 +65,7 @@
private lateinit var entry2: NotificationEntry
@Mock private lateinit var lifetimeExtensionCallback: OnEndLifetimeExtensionCallback
+
@Mock private lateinit var rebuilder: RemoteInputNotificationRebuilder
@Mock private lateinit var remoteInputManager: NotificationRemoteInputManager
@Mock private lateinit var mainHandler: Handler
@@ -84,9 +93,6 @@
listener = withArgCaptor {
verify(remoteInputManager).setRemoteInputListener(capture())
}
- collectionListener = withArgCaptor {
- verify(pipeline).addCollectionListener(capture())
- }
entry1 = NotificationEntryBuilder().setId(1).build()
entry2 = NotificationEntryBuilder().setId(2).build()
`when`(rebuilder.rebuildForCanceledSmartReplies(any())).thenReturn(sbn)
@@ -98,16 +104,23 @@
val remoteInputHistoryExtender get() = coordinator.mRemoteInputHistoryExtender
val smartReplyHistoryExtender get() = coordinator.mSmartReplyHistoryExtender
+ val collectionListeners get() = captureMany {
+ verify(pipeline, times(1)).addCollectionListener(capture())
+ }
+
@Test
fun testRemoteInputActive() {
`when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
- assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
- assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+ if (!lifetimeExtensionRefactor()) {
+ assertThat(remoteInputHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+ assertThat(smartReplyHistoryExtender.maybeExtendLifetime(entry1, 0)).isFalse()
+ }
assertThat(listener.isNotificationKeptForRemoteInputHistory(entry1.key)).isFalse()
}
@Test
+ @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
fun testRemoteInputHistory() {
`when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry1)).thenReturn(true)
assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -117,6 +130,7 @@
}
@Test
+ @DisableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
fun testSmartReplyHistory() {
`when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry1)).thenReturn(true)
assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isFalse()
@@ -142,4 +156,81 @@
verify(lifetimeExtensionCallback).onEndLifetimeExtension(remoteInputActiveExtender, entry1)
assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
}
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testOnlyRemoteInputActiveLifetimeExtenderExtends() {
+ `when`(remoteInputManager.isRemoteInputActive(entry1)).thenReturn(true)
+ assertThat(remoteInputActiveExtender.maybeExtendLifetime(entry1, 0)).isTrue()
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isTrue()
+
+ listener.onPanelCollapsed()
+ assertThat(remoteInputActiveExtender.isExtending(entry1.key)).isFalse()
+
+ // Checks that lifetimeExtensionCallback is only called the expected number of times,
+ // by the remoteInputActiveExtender.
+ // Checks that the remote input history extender and smart reply history extenders
+ // aren't attached to the pipeline.
+ verify(lifetimeExtensionCallback, times(1)).onEndLifetimeExtension(any(), any())
+ }
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testRemoteInputLifetimeExtensionListenerTrigger() {
+ // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+ val entry = NotificationEntryBuilder()
+ .setId(3)
+ .setTag("entry")
+ .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+ .build()
+ `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(true)
+ `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
+
+ collectionListeners.forEach {
+ it.onEntryUpdated(entry, true)
+ }
+
+ verify(rebuilder, times(1)).rebuildForRemoteInputReply(entry)
+ }
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testSmartReplyLifetimeExtensionListenerTrigger() {
+ // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+ val entry = NotificationEntryBuilder()
+ .setId(3)
+ .setTag("entry")
+ .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, true)
+ .build()
+ `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
+ `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(true)
+ collectionListeners.forEach {
+ it.onEntryUpdated(entry, true)
+ }
+
+
+ verify(rebuilder, times(1)).rebuildForCanceledSmartReplies(entry)
+ verify(smartReplyController, times(1)).stopSending(entry)
+ }
+
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ fun testLifetimeExtensionListenerClearsRemoteInputs() {
+ // Create notification with LIFETIME_EXTENDED_BY_DIRECT_REPLY flag.
+ val entry = NotificationEntryBuilder()
+ .setId(3)
+ .setTag("entry")
+ .setFlag(mContext, Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY, false)
+ .build()
+ entry.remoteInputs = ArrayList<RemoteInputHistoryItem>()
+ entry.remoteInputs.add(RemoteInputHistoryItem("Test Text"))
+ `when`(remoteInputManager.shouldKeepForRemoteInputHistory(entry)).thenReturn(false)
+ `when`(remoteInputManager.shouldKeepForSmartReplyHistory(entry)).thenReturn(false)
+
+ collectionListeners.forEach {
+ it.onEntryUpdated(entry, true)
+ }
+
+ assertThat(entry.remoteInputs).isNull()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index ff882b1..9055ba4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -138,7 +138,8 @@
configurationRepository.onAnyConfigurationChange()
- assertThat(dimens!!.paddingTop).isEqualTo(30)
+ // Should directly use the header height (flagged off value)
+ assertThat(dimens!!.paddingTop).isEqualTo(10)
}
@Test
@@ -154,7 +155,8 @@
configurationRepository.onAnyConfigurationChange()
- assertThat(dimens!!.paddingTop).isEqualTo(40)
+ // Should directly use the header height (flagged on value)
+ assertThat(dimens!!.paddingTop).isEqualTo(5)
}
@Test
@@ -456,8 +458,8 @@
)
runCurrent()
- // Top should be equal to bounds (1) + padding adjustment (30)
- assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 31f, bottom = 2f))
+ // Top should be equal to bounds (1) - padding adjustment (10)
+ assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -9f, bottom = 2f))
}
@Test
@@ -483,8 +485,8 @@
)
runCurrent()
- // Top should be equal to bounds (1) + padding adjustment (40)
- assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 41f, bottom = 2f))
+ // Top should be equal to bounds (1) - padding adjustment (5)
+ assertThat(bounds).isEqualTo(NotificationContainerBounds(top = -4f, bottom = 2f))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
index bbf9a6b..38698f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java
@@ -45,6 +45,7 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -80,6 +81,7 @@
public void setUp() {
MockitoAnnotations.initMocks(this);
mStaticMockSession = mockitoSession()
+ .strictness(Strictness.WARN)
.mockStatic(BurnInHelperKt.class)
.mockStatic(LargeScreenHeaderHelper.class)
.startMocking();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
index b3708ba..41b959e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImplTest.java
@@ -26,6 +26,7 @@
import androidx.test.filters.SmallTest;
+import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.flags.FeatureFlags;
@@ -118,6 +119,7 @@
@Test
public void testAppearResetsTranslation() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL);
mController.setupAodIcons(mAodIcons);
when(mDozeParameters.shouldControlScreenOff()).thenReturn(false);
mController.appearAodIcons();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
index 6eb1c1a..269b70f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewTest.kt
@@ -42,6 +42,7 @@
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
import org.mockito.Mockito.verify
@SmallTest
@@ -145,6 +146,18 @@
}
@Test
+ fun onConfigurationChanged_multipleCalls_flagEnabled_updatesWindowHeightMultipleTimes() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX)
+
+ view.onConfigurationChanged(Configuration())
+ view.onConfigurationChanged(Configuration())
+ view.onConfigurationChanged(Configuration())
+ view.onConfigurationChanged(Configuration())
+
+ verify(windowController, times(4)).refreshStatusBarHeight()
+ }
+
+ @Test
fun onConfigurationChanged_flagDisabled_doesNotUpdateWindowHeight() {
mSetFlagsRule.disableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX)
@@ -154,6 +167,18 @@
}
@Test
+ fun onConfigurationChanged_multipleCalls_flagDisabled_doesNotUpdateWindowHeight() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_TRUNCATED_STATUS_BAR_ICONS_FIX)
+
+ view.onConfigurationChanged(Configuration())
+ view.onConfigurationChanged(Configuration())
+ view.onConfigurationChanged(Configuration())
+ view.onConfigurationChanged(Configuration())
+
+ verify(windowController, never()).refreshStatusBarHeight()
+ }
+
+ @Test
fun onAttachedToWindow_updatesLeftTopRightPaddingsBasedOnInsets() {
val insets = Insets.of(/* left = */ 10, /* top = */ 20, /* right = */ 30, /* bottom = */ 40)
whenever(contentInsetsProvider.getStatusBarContentInsetsForCurrentRotation())
@@ -232,6 +257,10 @@
/* privacyIndicatorBounds = */ PrivacyIndicatorBounds(),
/* displayShape = */ DisplayShape.NONE,
/* compatInsetsTypes = */ 0,
- /* compatIgnoreVisibility = */ false
+ /* compatIgnoreVisibility = */ false,
+ /* typeBoundingRectsMap = */ arrayOf(),
+ /* typeMaxBoundingRectsMap = */ arrayOf(),
+ /* frameWidth = */ 0,
+ /* frameHeight = */ 0
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 54d3607..3da5ab9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -131,7 +131,7 @@
@Mock
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
index 2ce060c..997c00c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/MultiSourceMinAlphaControllerTest.kt
@@ -42,7 +42,7 @@
private val multiSourceMinAlphaController =
MultiSourceMinAlphaController(view, initialAlpha = INITIAL_ALPHA)
- @get:Rule val animatorTestRule = AnimatorTestRule()
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
@Before
fun setup() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 658e6b0..13167b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -112,7 +112,7 @@
private final UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Before
public void setUp() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
index 98be163..7592356 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt
@@ -25,6 +25,7 @@
import com.android.server.notification.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.settings.FakeGlobalSettings
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
import org.junit.Test
@@ -49,6 +50,7 @@
controller =
SensitiveNotificationProtectionControllerImpl(
mContext,
+ FakeGlobalSettings(),
mediaProjectionManager,
activityManager,
handler,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
index a1aff48..1dac642 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -22,6 +22,7 @@
import android.media.projection.MediaProjectionInfo
import android.media.projection.MediaProjectionManager
import android.platform.test.annotations.EnableFlags
+import android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS
import android.service.notification.StatusBarNotification
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
@@ -33,6 +34,7 @@
import com.android.systemui.util.concurrency.mockExecutorHandler
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.settings.FakeGlobalSettings
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
@@ -61,6 +63,8 @@
@Mock private lateinit var listener2: Runnable
@Mock private lateinit var listener3: Runnable
+ private lateinit var executor: FakeExecutor
+ private lateinit var globalSettings: FakeGlobalSettings
private lateinit var mediaProjectionCallback: MediaProjectionManager.Callback
private lateinit var controller: SensitiveNotificationProtectionControllerImpl
@@ -73,18 +77,19 @@
whenever(activityManager.bugreportWhitelistedPackages)
.thenReturn(listOf(BUGREPORT_PACKAGE_NAME))
- val executor = FakeExecutor(FakeSystemClock())
-
+ executor = FakeExecutor(FakeSystemClock())
+ globalSettings = FakeGlobalSettings()
controller =
SensitiveNotificationProtectionControllerImpl(
mContext,
+ globalSettings,
mediaProjectionManager,
activityManager,
mockExecutorHandler(executor),
executor
)
- // Process exemption processing
+ // Process pending work (getting global setting and list of exemptions)
executor.runAllReady()
// Obtain useful MediaProjectionCallback
@@ -229,6 +234,14 @@
}
@Test
+ fun isSensitiveStateActive_projectionActive_disabledViaDevOption_false() {
+ setDisabledViaDeveloperOption()
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ assertFalse(controller.isSensitiveStateActive)
+ }
+
+ @Test
fun shouldProtectNotification_projectionInactive_false() {
val notificationEntry = mock(NotificationEntry::class.java)
@@ -294,6 +307,23 @@
assertFalse(controller.shouldProtectNotification(notificationEntry))
}
+ @Test
+ fun shouldProtectNotification_projectionActive_disabledViaDevOption_false() {
+ setDisabledViaDeveloperOption()
+ mediaProjectionCallback.onStart(mediaProjectionInfo)
+
+ val notificationEntry = setupNotificationEntry(TEST_PROJECTION_PACKAGE_NAME)
+
+ assertFalse(controller.shouldProtectNotification(notificationEntry))
+ }
+
+ private fun setDisabledViaDeveloperOption() {
+ globalSettings.putInt(DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 1)
+
+ // Process pending work that gets current developer option global setting
+ executor.runAllReady()
+ }
+
private fun setShareFullScreen() {
whenever(mediaProjectionInfo.packageName).thenReturn(TEST_PROJECTION_PACKAGE_NAME)
whenever(mediaProjectionInfo.launchCookie).thenReturn(null)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt
new file mode 100644
index 0000000..7c36a85
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/loadingeffect/LoadingEffectTest.kt
@@ -0,0 +1,267 @@
+/*
+ * 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.surfaceeffects.loadingeffect
+
+import android.graphics.Paint
+import android.graphics.RenderEffect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.model.SysUiStateTest
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_IN
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.EASE_OUT
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.MAIN
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationState.NOT_PLAYING
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.AnimationStateChangedCallback
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.PaintDrawCallback
+import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.Companion.RenderEffectDrawCallback
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig
+import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class LoadingEffectTest : SysUiStateTest() {
+
+ private val fakeSystemClock = FakeSystemClock()
+ private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+ @Test
+ fun play_paintCallback_triggersDrawCallback() {
+ var paintFromCallback: Paint? = null
+ val drawCallback =
+ object : PaintDrawCallback {
+ override fun onDraw(loadingPaint: Paint) {
+ paintFromCallback = loadingPaint
+ }
+ }
+ val loadingEffect =
+ LoadingEffect(
+ baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+ TurbulenceNoiseAnimationConfig(),
+ paintCallback = drawCallback,
+ animationStateChangedCallback = null
+ )
+
+ fakeExecutor.execute {
+ assertThat(paintFromCallback).isNull()
+
+ loadingEffect.play()
+ fakeSystemClock.advanceTime(500L)
+
+ assertThat(paintFromCallback).isNotNull()
+ }
+ }
+
+ @Test
+ fun play_renderEffectCallback_triggersDrawCallback() {
+ var renderEffectFromCallback: RenderEffect? = null
+ val drawCallback =
+ object : RenderEffectDrawCallback {
+ override fun onDraw(loadingRenderEffect: RenderEffect) {
+ renderEffectFromCallback = loadingRenderEffect
+ }
+ }
+ val loadingEffect =
+ LoadingEffect(
+ baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+ TurbulenceNoiseAnimationConfig(),
+ renderEffectCallback = drawCallback,
+ animationStateChangedCallback = null
+ )
+
+ fakeExecutor.execute {
+ assertThat(renderEffectFromCallback).isNull()
+
+ loadingEffect.play()
+ fakeSystemClock.advanceTime(500L)
+
+ assertThat(renderEffectFromCallback).isNotNull()
+ }
+ }
+
+ @Test
+ fun play_animationStateChangesInOrder() {
+ val config = TurbulenceNoiseAnimationConfig()
+ val expectedStates = arrayOf(NOT_PLAYING, EASE_IN, MAIN, EASE_OUT, NOT_PLAYING)
+ val actualStates = mutableListOf(NOT_PLAYING)
+ val stateChangedCallback =
+ object : AnimationStateChangedCallback {
+ override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+ actualStates.add(newState)
+ }
+ }
+ val drawCallback =
+ object : PaintDrawCallback {
+ override fun onDraw(loadingPaint: Paint) {}
+ }
+ val loadingEffect =
+ LoadingEffect(
+ baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+ config,
+ paintCallback = drawCallback,
+ stateChangedCallback
+ )
+
+ val timeToAdvance =
+ config.easeInDuration + config.maxDuration + config.easeOutDuration + 100
+
+ fakeExecutor.execute {
+ loadingEffect.play()
+
+ fakeSystemClock.advanceTime(timeToAdvance.toLong())
+
+ assertThat(actualStates).isEqualTo(expectedStates)
+ }
+ }
+
+ @Test
+ fun play_alreadyPlaying_playsOnlyOnce() {
+ val config = TurbulenceNoiseAnimationConfig()
+ var numPlay = 0
+ val stateChangedCallback =
+ object : AnimationStateChangedCallback {
+ override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+ if (oldState == NOT_PLAYING && newState == EASE_IN) {
+ numPlay++
+ }
+ }
+ }
+ val drawCallback =
+ object : PaintDrawCallback {
+ override fun onDraw(loadingPaint: Paint) {}
+ }
+ val loadingEffect =
+ LoadingEffect(
+ baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+ config,
+ paintCallback = drawCallback,
+ stateChangedCallback
+ )
+
+ fakeExecutor.execute {
+ assertThat(numPlay).isEqualTo(0)
+
+ loadingEffect.play()
+ loadingEffect.play()
+ loadingEffect.play()
+ loadingEffect.play()
+ loadingEffect.play()
+
+ assertThat(numPlay).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun finish_finishesLoadingEffect() {
+ val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
+ val drawCallback =
+ object : PaintDrawCallback {
+ override fun onDraw(loadingPaint: Paint) {}
+ }
+ var isFinished = false
+ val stateChangedCallback =
+ object : AnimationStateChangedCallback {
+ override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+ if (oldState == MAIN && newState == NOT_PLAYING) {
+ isFinished = true
+ }
+ }
+ }
+ val loadingEffect =
+ LoadingEffect(
+ baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+ config,
+ paintCallback = drawCallback,
+ stateChangedCallback
+ )
+
+ fakeExecutor.execute {
+ assertThat(isFinished).isFalse()
+
+ loadingEffect.play()
+ fakeSystemClock.advanceTime(config.easeInDuration.toLong() + 500L)
+
+ assertThat(isFinished).isFalse()
+
+ loadingEffect.finish()
+
+ assertThat(isFinished).isTrue()
+ }
+ }
+
+ @Test
+ fun finish_notMainState_hasNoEffect() {
+ val config = TurbulenceNoiseAnimationConfig(maxDuration = 1000f)
+ val drawCallback =
+ object : PaintDrawCallback {
+ override fun onDraw(loadingPaint: Paint) {}
+ }
+ var isFinished = false
+ val stateChangedCallback =
+ object : AnimationStateChangedCallback {
+ override fun onStateChanged(oldState: AnimationState, newState: AnimationState) {
+ if (oldState == MAIN && newState == NOT_PLAYING) {
+ isFinished = true
+ }
+ }
+ }
+ val loadingEffect =
+ LoadingEffect(
+ baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+ config,
+ paintCallback = drawCallback,
+ stateChangedCallback
+ )
+
+ fakeExecutor.execute {
+ assertThat(isFinished).isFalse()
+
+ loadingEffect.finish()
+
+ assertThat(isFinished).isFalse()
+ }
+ }
+
+ @Test
+ fun getNoiseOffset_returnsNoiseOffset() {
+ val expectedNoiseOffset = arrayOf(0.1f, 0.2f, 0.3f)
+ val config =
+ TurbulenceNoiseAnimationConfig(
+ noiseOffsetX = expectedNoiseOffset[0],
+ noiseOffsetY = expectedNoiseOffset[1],
+ noiseOffsetZ = expectedNoiseOffset[2]
+ )
+ val drawCallback =
+ object : PaintDrawCallback {
+ override fun onDraw(loadingPaint: Paint) {}
+ }
+ val loadingEffect =
+ LoadingEffect(
+ baseType = TurbulenceNoiseShader.Companion.Type.SIMPLEX_NOISE,
+ config,
+ paintCallback = drawCallback,
+ animationStateChangedCallback = null
+ )
+
+ assertThat(loadingEffect.getNoiseOffset()).isEqualTo(expectedNoiseOffset)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 8a33778..25a0bc4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -157,7 +157,7 @@
private FakeSettings mSecureSettings;
@Rule
- public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
+ public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this);
@Before
public void setup() throws Exception {
diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
index 1afe56f..5860c2d 100644
--- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
@@ -16,14 +16,21 @@
package android.animation;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
import android.animation.AnimationHandler.AnimationFrameCallback;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Looper;
import android.os.SystemClock;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunnableWithException;
import android.util.AndroidRuntimeException;
import android.util.Singleton;
import android.view.Choreographer;
+import android.view.animation.AnimationUtils;
+
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.util.Preconditions;
@@ -74,25 +81,31 @@
return new TestHandler();
}
};
+ private final Object mTest;
private final long mStartTime;
private long mTotalTimeDelta = 0;
+ private volatile boolean mCanLockAnimationClock;
+ private Looper mLooperWithLockedAnimationClock;
/**
- * Construct an AnimatorTestRule with a custom start time.
- * @see #AnimatorTestRule()
+ * Construct an AnimatorTestRule with access to the test instance and a custom start time.
+ * @see #AnimatorTestRule(Object)
*/
- public AnimatorTestRule(long startTime) {
+ public AnimatorTestRule(Object test, long startTime) {
+ mTest = test;
mStartTime = startTime;
}
/**
- * Construct an AnimatorTestRule with a start time of {@link SystemClock#uptimeMillis()}.
- * Initializing the start time with this clock reduces the discrepancies with various internals
- * of classes like ValueAnimator which can sometimes read that clock via
- * {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
+ * Construct an AnimatorTestRule for the given test instance with a start time of
+ * {@link SystemClock#uptimeMillis()}. Initializing the start time with this clock reduces the
+ * discrepancies with various internals of classes like ValueAnimator which can sometimes read
+ * that clock via {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
+ *
+ * @param test the test instance used to access the {@link TestableLooper} used by the class.
*/
- public AnimatorTestRule() {
- this(SystemClock.uptimeMillis());
+ public AnimatorTestRule(Object test) {
+ this(test, SystemClock.uptimeMillis());
}
@NonNull
@@ -102,10 +115,16 @@
@Override
public void evaluate() throws Throwable {
final TestHandler testHandler = mTestHandler.get();
- AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler);
+ final AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler);
+ final RunnableWithException lockClock =
+ wrapWithRunBlocking(new LockAnimationClockRunnable());
+ final RunnableWithException unlockClock =
+ wrapWithRunBlocking(new UnlockAnimationClockRunnable());
try {
+ lockClock.run();
base.evaluate();
} finally {
+ unlockClock.run();
AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
if (testHandler != objAtEnd) {
// pass or fail, inner logic not restoring the handler needs to be reported.
@@ -118,6 +137,78 @@
};
}
+ private RunnableWithException wrapWithRunBlocking(RunnableWithException runnable) {
+ RunnableWithException wrapped = TestableLooper.wrapWithRunBlocking(mTest, runnable);
+ if (wrapped != null) {
+ return wrapped;
+ }
+ return () -> runOnMainThrowing(runnable);
+ }
+
+ private static void runOnMainThrowing(RunnableWithException runnable) throws Exception {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ runnable.run();
+ } else {
+ final Throwable[] throwableBox = new Throwable[1];
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ try {
+ runnable.run();
+ } catch (Throwable t) {
+ throwableBox[0] = t;
+ }
+ });
+ if (throwableBox[0] == null) {
+ return;
+ } else if (throwableBox[0] instanceof RuntimeException ex) {
+ throw ex;
+ } else if (throwableBox[0] instanceof Error err) {
+ throw err;
+ } else {
+ throw new RuntimeException(throwableBox[0]);
+ }
+ }
+ }
+
+ private class LockAnimationClockRunnable implements RunnableWithException {
+ @Override
+ public void run() {
+ mLooperWithLockedAnimationClock = Looper.myLooper();
+ mCanLockAnimationClock = true;
+ lockAnimationClockToCurrentTime();
+ }
+ }
+
+ private class UnlockAnimationClockRunnable implements RunnableWithException {
+ @Override
+ public void run() {
+ mCanLockAnimationClock = false;
+ mLooperWithLockedAnimationClock = null;
+ AnimationUtils.unlockAnimationClock();
+ }
+ }
+
+ private void lockAnimationClockToCurrentTime() {
+ if (!mCanLockAnimationClock) {
+ throw new AssertionError("Unable to lock the animation clock; "
+ + "has the test started? already finished?");
+ }
+ if (mLooperWithLockedAnimationClock != Looper.myLooper()) {
+ throw new AssertionError("Animation clock being locked on " + Looper.myLooper()
+ + " but should only be locked on " + mLooperWithLockedAnimationClock);
+ }
+ long desiredTime = getCurrentTime();
+ AnimationUtils.lockAnimationClock(desiredTime);
+ if (!mCanLockAnimationClock) {
+ AnimationUtils.unlockAnimationClock();
+ throw new AssertionError("Threading error when locking the animation clock");
+ }
+ long outputTime = AnimationUtils.currentAnimationTimeMillis();
+ if (outputTime != desiredTime) {
+ throw new AssertionError("currentAnimationTimeMillis() is " + outputTime
+ + " after locking to " + desiredTime);
+ }
+ }
+
/**
* If any new {@link Animator}s have been registered since the last time the frame time was
* advanced, initialize them with the current frame time. Failing to do this will result in the
@@ -178,6 +269,7 @@
// advance time
mTotalTimeDelta += timeDelta;
}
+ lockAnimationClockToCurrentTime();
if (preFrameAction != null) {
preFrameAction.accept(timeDelta);
// After letting other code run, clear any new callbacks to avoid double-ticking them
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt
index ba9c5ed..e2fc44f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/AnimatorTestRule.kt
@@ -26,12 +26,16 @@
* A rule that wraps both [androidx.core.animation.AnimatorTestRule] and
* [android.animation.AnimatorTestRule] such that the clocks of the two animation handlers can be
* advanced together.
+ *
+ * @param test the instance of the test used to look up the TestableLooper. If a TestableLooper is
+ * found, the time can only be advanced on that thread; otherwise the time must be advanced on the
+ * main thread.
*/
-class AnimatorTestRule : TestRule {
+class AnimatorTestRule(test: Any?) : TestRule {
// Create the androidx rule, which initializes start time to SystemClock.uptimeMillis(),
// then copy that time to the platform rule so that the two clocks are in sync.
private val androidxRule = androidx.core.animation.AnimatorTestRule()
- private val platformRule = android.animation.AnimatorTestRule(androidxRule.startTime)
+ private val platformRule = android.animation.AnimatorTestRule(test, androidxRule.startTime)
private val advanceAndroidXTimeBy =
Consumer<Long> { timeDelta -> androidxRule.advanceTimeBy(timeDelta) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
new file mode 100644
index 0000000..c208aad
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.shared.model
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeSceneDataSource(
+ initialSceneKey: SceneKey,
+) : SceneDataSource {
+
+ private val _currentScene = MutableStateFlow(initialSceneKey)
+ override val currentScene: StateFlow<SceneKey> = _currentScene.asStateFlow()
+
+ var isPaused = false
+ private set
+ var pendingScene: SceneKey? = null
+ private set
+
+ override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
+ if (isPaused) {
+ pendingScene = toScene
+ } else {
+ _currentScene.value = toScene
+ }
+ }
+
+ /**
+ * Pauses scene changes.
+ *
+ * Any following calls to [changeScene] will be conflated and the last one will be remembered.
+ */
+ fun pause() {
+ check(!isPaused) { "Can't pause what's already paused!" }
+
+ isPaused = true
+ }
+
+ /**
+ * Unpauses scene changes.
+ *
+ * If there were any calls to [changeScene] since [pause] was called, the latest of the bunch
+ * will be replayed.
+ *
+ * If [force] is `true`, there will be no check that [isPaused] is true.
+ *
+ * If [expectedScene] is provided, will assert that it's indeed the latest called.
+ */
+ fun unpause(
+ force: Boolean = false,
+ expectedScene: SceneKey? = null,
+ ) {
+ check(force || isPaused) { "Can't unpause what's already not paused!" }
+
+ isPaused = false
+ pendingScene?.let { _currentScene.value = it }
+ pendingScene = null
+
+ check(expectedScene == null || currentScene.value == expectedScene) {
+ """
+ Unexpected scene while unpausing.
+ Expected $expectedScene but was $currentScene.
+ """
+ .trimIndent()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt
new file mode 100644
index 0000000..f519686
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.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.systemui.scene.shared.model
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.scene.initialSceneKey
+import com.android.systemui.scene.sceneContainerConfig
+
+val Kosmos.fakeSceneDataSource by Fixture {
+ FakeSceneDataSource(
+ initialSceneKey = initialSceneKey,
+ )
+}
+
+val Kosmos.sceneDataSourceDelegator by Fixture {
+ SceneDataSourceDelegator(
+ applicationScope = applicationCoroutineScope,
+ config = sceneContainerConfig,
+ )
+ .apply { setDelegate(fakeSceneDataSource) }
+}
+
+val Kosmos.sceneDataSource by Fixture { sceneDataSourceDelegator }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 1ac69f6..35ce481 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -58,11 +58,14 @@
visibility: ["//visibility:public"],
}
-java_host_for_device {
- name: "core-xml-for-device",
- libs: [
- "core-xml-for-host",
+java_library {
+ // Prefixed with "200" to ensure it's sorted early in Tradefed classpath
+ // so that we provide a concrete implementation before Mainline stubs
+ name: "200-kxml2-android",
+ static_libs: [
+ "kxml2-android",
],
+ visibility: ["//frameworks/base"],
}
java_host_for_device {
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 0319848..a5b28ad 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -2,6 +2,9 @@
"presubmit": [
{
"name": "RavenwoodMockitoTest_device"
+ },
+ {
+ "name": "RavenwoodBivalentTest_device"
}
],
"ravenwood-presubmit": [
@@ -16,6 +19,14 @@
{
"name": "CtsUtilTestCasesRavenwood",
"host": true
+ },
+ {
+ "name": "RavenwoodCoreTest",
+ "host": true
+ },
+ {
+ "name": "RavenwoodBivalentTest",
+ "host": true
}
]
}
diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/bivalenttest/Android.bp
new file mode 100644
index 0000000..acf8533
--- /dev/null
+++ b/ravenwood/bivalenttest/Android.bp
@@ -0,0 +1,47 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_ravenwood_test {
+ name: "RavenwoodBivalentTest",
+
+ static_libs: [
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ ],
+ srcs: [
+ "test/**/*.java",
+ ],
+ sdk_version: "test_current",
+ auto_gen_config: true,
+}
+
+android_test {
+ name: "RavenwoodBivalentTest_device",
+
+ srcs: [
+ "test/**/*.java",
+ ],
+ static_libs: [
+ "junit",
+ "truth",
+
+ "androidx.annotation_annotation",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+
+ "ravenwood-junit",
+ ],
+ test_suites: [
+ "device-tests",
+ ],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/ravenwood/bivalenttest/AndroidManifest.xml b/ravenwood/bivalenttest/AndroidManifest.xml
new file mode 100644
index 0000000..474c03f
--- /dev/null
+++ b/ravenwood/bivalenttest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.ravenwood.bivalenttest">
+
+ <application android:debuggable="true" >
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.ravenwood.bivalenttest"
+ />
+</manifest>
diff --git a/ravenwood/bivalenttest/AndroidTest.xml b/ravenwood/bivalenttest/AndroidTest.xml
new file mode 100644
index 0000000..ac4695b
--- /dev/null
+++ b/ravenwood/bivalenttest/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Frameworks Services Tests.">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="apct-instrumentation" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="RavenwoodBivalentTest_device.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.ravenwood.bivalenttest" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+</configuration>
diff --git a/ravenwood/bivalenttest/README.md b/ravenwood/bivalenttest/README.md
new file mode 100644
index 0000000..7153555
--- /dev/null
+++ b/ravenwood/bivalenttest/README.md
@@ -0,0 +1,3 @@
+# Ravenwood bivalent test
+
+This test contains bivalent tests for Ravenwood itself.
\ No newline at end of file
diff --git a/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java
new file mode 100644
index 0000000..4b650b4
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/platform/test/ravenwood/bivalenttest/RavenwoodRuleTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.platform.test.ravenwood.bivalenttest;
+
+import android.platform.test.annotations.DisabledOnNonRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRuleTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Test
+ @DisabledOnRavenwood
+ public void testDeviceOnly() {
+ Assert.assertFalse(RavenwoodRule.isOnRavenwood());
+ }
+
+ @Test
+ @DisabledOnNonRavenwood
+ public void testRavenwoodOnly() {
+ Assert.assertTrue(RavenwoodRule.isOnRavenwood());
+ }
+
+ // TODO: Add more tests
+}
diff --git a/ravenwood/coretest/Android.bp b/ravenwood/coretest/Android.bp
index 9b7f8f7..a78c5c1 100644
--- a/ravenwood/coretest/Android.bp
+++ b/ravenwood/coretest/Android.bp
@@ -12,9 +12,9 @@
static_libs: [
"androidx.annotation_annotation",
+ "androidx.test.ext.junit",
"androidx.test.rules",
],
-
srcs: [
"test/**/*.java",
],
diff --git a/ravenwood/coretest/README.md b/ravenwood/coretest/README.md
new file mode 100644
index 0000000..b60bfbf
--- /dev/null
+++ b/ravenwood/coretest/README.md
@@ -0,0 +1,3 @@
+# Ravenwood core test
+
+This test contains (non-bivalent) tests for Ravenwood itself -- e.g. tests for the ravenwood rules.
\ No newline at end of file
diff --git a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
index e58c282..2cd585f 100644
--- a/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
+++ b/ravenwood/coretest/test/com/android/platform/test/ravenwood/coretest/RavenwoodTestRunnerValidationTest.java
@@ -17,7 +17,7 @@
import android.platform.test.ravenwood.RavenwoodRule;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.runner.AndroidJUnit4; // Intentionally use the deprecated one.
import org.junit.Assume;
import org.junit.Rule;
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index 0229611..2eef0cd 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -89,6 +89,7 @@
# Internals
class com.android.internal.util.FileRotator stubclass
+class com.android.internal.util.FastXmlSerializer stubclass
class com.android.internal.util.HexDump stubclass
class com.android.internal.util.MessageUtils stubclass
class com.android.internal.util.Preconditions stubclass
diff --git a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
new file mode 100644
index 0000000..8ca34ba
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
@@ -0,0 +1,47 @@
+/*
+ * 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 android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Tests marked with this annotation are only executed when running on Ravenwood, but not
+ * on a device.
+ *
+ * This is basically equivalent to the opposite of {@link DisabledOnRavenwood}, but in order to
+ * avoid complex structure, and there's no equivalent to the opposite {@link EnabledOnRavenwood},
+ * which means if a test class has this annotation, you can't negate it in subclasses or
+ * on a per-method basis.
+ *
+ * The {@code RAVENWOOD_RUN_DISABLED_TESTS} environmental variable won't work because it won't be
+ * propagated to the device. (We may support it in the future, possibly using a debug. sysprop.)
+ *
+ * @hide
+ */
+@Inherited
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DisabledOnNonRavenwood {
+ /**
+ * General free-form description of why this test is being ignored.
+ */
+ String reason() default "";
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
index 8d76970..9a4d488 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
@@ -18,6 +18,7 @@
import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED;
import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD;
+import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnDevice;
import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood;
import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode;
@@ -42,6 +43,7 @@
public Statement apply(Statement base, Description description) {
// No special treatment when running outside Ravenwood; run tests as-is
if (!IS_ON_RAVENWOOD) {
+ Assume.assumeTrue(shouldEnableOnDevice(description));
return base;
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 764573d..b90f112 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -22,6 +22,7 @@
import static org.junit.Assert.fail;
+import android.platform.test.annotations.DisabledOnNonRavenwood;
import android.platform.test.annotations.DisabledOnRavenwood;
import android.platform.test.annotations.EnabledOnRavenwood;
import android.platform.test.annotations.IgnoreUnderRavenwood;
@@ -211,6 +212,15 @@
return IS_ON_RAVENWOOD;
}
+ static boolean shouldEnableOnDevice(Description description) {
+ if (description.isTest()) {
+ if (description.getAnnotation(DisabledOnNonRavenwood.class) != null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/**
* Determine if the given {@link Description} should be enabled when running on the
* Ravenwood test environment.
@@ -271,6 +281,7 @@
public Statement apply(Statement base, Description description) {
// No special treatment when running outside Ravenwood; run tests as-is
if (!IS_ON_RAVENWOOD) {
+ Assume.assumeTrue(shouldEnableOnDevice(description));
return base;
}
diff --git a/ravenwood/minimum-test/README.md b/ravenwood/minimum-test/README.md
new file mode 100644
index 0000000..6b0abe9
--- /dev/null
+++ b/ravenwood/minimum-test/README.md
@@ -0,0 +1,3 @@
+# Ravenwood minimum test
+
+This directory contains a minimum possible ravenwood test -- no extra dependencies, etc.
\ No newline at end of file
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
index 03cfad5..b477117 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
@@ -28,7 +28,7 @@
@RunWith(AndroidJUnit4.class)
public class RavenwoodMinimumTest {
@Rule
- public RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
.setProcessApp()
.build();
diff --git a/ravenwood/mockito/Android.bp b/ravenwood/mockito/Android.bp
index a74bca4..95c7394 100644
--- a/ravenwood/mockito/Android.bp
+++ b/ravenwood/mockito/Android.bp
@@ -13,6 +13,9 @@
srcs: [
"test/**/*.java",
],
+ exclude_srcs: [
+ "test/**/*DeviceOnly*.java",
+ ],
static_libs: [
"junit",
"truth",
@@ -31,6 +34,9 @@
srcs: [
"test/**/*.java",
],
+ exclude_srcs: [
+ "test/**/*RavenwoodOnly*.java",
+ ],
static_libs: [
"junit",
"truth",
diff --git a/ravenwood/mockito/AndroidTest.xml b/ravenwood/mockito/AndroidTest.xml
index 96bc275..5ba9b1f 100644
--- a/ravenwood/mockito/AndroidTest.xml
+++ b/ravenwood/mockito/AndroidTest.xml
@@ -22,8 +22,6 @@
<option name="test-file-name" value="RavenwoodMockitoTest_device.apk" />
</target_preparer>
- <option name="test-tag" value="FrameworksMockingServicesTests" />
-
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.ravenwood.mockitotest" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
diff --git a/ravenwood/mockito/README.md b/ravenwood/mockito/README.md
new file mode 100644
index 0000000..4ceb795
--- /dev/null
+++ b/ravenwood/mockito/README.md
@@ -0,0 +1,3 @@
+# Ravenwood mockito test
+
+This directory contains a sample bivalent test using Mockito.
\ No newline at end of file
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java
new file mode 100644
index 0000000..d02fe69
--- /dev/null
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoDeviceOnlyTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ravenwood.mockito;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.ActivityManager;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.quality.Strictness;
+
+public class RavenwoodMockitoDeviceOnlyTest {
+ @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Test
+ public void testStaticMockOnDevice() {
+ var mockingSession = ExtendedMockito.mockitoSession()
+ .strictness(Strictness.LENIENT)
+ .mockStatic(ActivityManager.class)
+ .startMocking();
+ try {
+ ExtendedMockito.doReturn(true).when(ActivityManager::isUserAMonkey);
+
+ assertThat(ActivityManager.isUserAMonkey()).isEqualTo(true);
+ } finally {
+ mockingSession.finishMocking();
+ }
+ }
+}
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java
new file mode 100644
index 0000000..0c137d5
--- /dev/null
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoRavenwoodOnlyTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.ravenwood.mockito;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.ActivityManager;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+public class RavenwoodMockitoRavenwoodOnlyTest {
+ @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ @Test
+ public void testStaticMockOnRavenwood() {
+ try (MockedStatic<ActivityManager> am = Mockito.mockStatic(ActivityManager.class)) {
+ am.when(ActivityManager::isUserAMonkey).thenReturn(true);
+ assertThat(ActivityManager.isUserAMonkey()).isEqualTo(true);
+ }
+ }
+}
diff --git a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
index 1284d64..9566710 100644
--- a/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
+++ b/ravenwood/mockito/test/com/android/ravenwood/mockito/RavenwoodMockitoTest.java
@@ -31,28 +31,6 @@
public class RavenwoodMockitoTest {
@Rule public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-// Use this to mock static methods, which isn't supported by mockito 2.
-// Mockito supports static mocking since 3.4.0:
-// See: https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#48
-
-// private MockitoSession mMockingSession;
-//
-// @Before
-// public void setUp() {
-// mMockingSession = mockitoSession()
-// .strictness(Strictness.LENIENT)
-// .mockStatic(RavenwoodMockitoTest.class)
-// .startMocking();
-// }
-//
-// @After
-// public void tearDown() {
-// if (mMockingSession != null) {
-// mMockingSession.finishMocking();
-// }
-// }
-
@Test
public void testMockJdkClass() {
Process object = mock(Process.class);
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index 01c0074..927ddd7 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -13,11 +13,13 @@
com.android.internal.os.Clock
com.android.internal.os.LongArrayMultiStateCounter
com.android.internal.os.LongArrayMultiStateCounter$LongArrayContainer
+com.android.internal.os.LongMultiStateCounter
com.android.internal.os.MonotonicClock
com.android.internal.os.PowerProfile
com.android.internal.os.PowerStats
com.android.internal.os.PowerStats$Descriptor
com.android.internal.os.RuntimeInit
+com.android.internal.power.EnergyConsumerStats
com.android.internal.power.ModemPowerProfile
android.util.AtomicFile
@@ -54,6 +56,7 @@
android.os.BatteryUsageStatsQuery
android.os.Binder
android.os.Binder$IdentitySupplier
+android.os.BluetoothBatteryStats
android.os.Broadcaster
android.os.Build
android.os.BundleMerger
@@ -83,12 +86,15 @@
android.os.ThreadLocalWorkSource
android.os.TimestampedValue
android.os.Trace
+android.os.UserBatteryConsumer
+android.os.UserBatteryConsumer$Builder
android.os.UidBatteryConsumer
android.os.UidBatteryConsumer$Builder
android.os.UserHandle
android.os.UserManager
android.os.VibrationAttributes
android.os.VibrationAttributes$Builder
+android.os.WakeLockStats
android.os.WorkSource
android.content.ClipData
@@ -159,6 +165,7 @@
android.metrics.LogMaker
+android.view.Display
android.view.Display$HdrCapabilities
android.view.Display$Mode
android.view.DisplayInfo
@@ -169,7 +176,6 @@
android.telephony.ServiceState
com.android.server.LocalServices
-com.android.server.power.stats.BatteryStatsImpl
com.android.internal.util.BitUtils
com.android.internal.util.BitwiseInputStream
@@ -192,6 +198,7 @@
com.android.internal.util.RingBuffer
com.android.internal.util.StringPool
+com.android.internal.os.BackgroundThread
com.android.internal.os.BinderCallHeavyHitterWatcher
com.android.internal.os.BinderDeathDispatcher
com.android.internal.os.BinderfsStatsReader
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index f902439..015c35e 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -10,6 +10,13 @@
}
flag {
+ name: "resettable_dynamic_properties"
+ namespace: "accessibility"
+ description: "Maintains initial copies of a11yServiceInfo dynamic properties so they can reset on disconnect."
+ bug: "312386990"
+}
+
+flag {
name: "cleanup_a11y_overlays"
namespace: "accessibility"
description: "Removes all attached accessibility overlays when a service is removed."
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 1d73843..af47ed2 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -42,10 +42,12 @@
import android.accessibilityservice.AccessibilityTrace;
import android.accessibilityservice.IAccessibilityServiceClient;
import android.accessibilityservice.IAccessibilityServiceConnection;
+import android.accessibilityservice.IBrailleDisplayController;
import android.accessibilityservice.MagnificationConfig;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
@@ -58,6 +60,7 @@
import android.hardware.HardwareBuffer;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManagerInternal;
+import android.hardware.usb.UsbDevice;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
@@ -1685,6 +1688,9 @@
}
public void resetLocked() {
+ if (Flags.resettableDynamicProperties()) {
+ mAccessibilityServiceInfo.resetDynamicallyConfigurableProperties();
+ }
mSystemSupport.getKeyEventDispatcher().flush(this);
try {
// Clear the proxy in the other process so this
@@ -2773,4 +2779,23 @@
t.close();
mOverlays.clear();
}
+
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+ public void connectBluetoothBrailleDisplay(String bluetoothAddress,
+ IBrailleDisplayController controller) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void connectUsbBrailleDisplay(UsbDevice usbDevice,
+ IBrailleDisplayController controller) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ @SuppressLint("AndroidFrameworkRequiresPermission") // Unsupported in Abstract class
+ public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+ throw new UnsupportedOperationException();
+ }
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index c58cb72..87b57b0 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -23,9 +23,9 @@
import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_SERVICE_CLIENT;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_FINGERPRINT;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_INPUT_FILTER;
+import static android.accessibilityservice.AccessibilityTrace.FLAGS_MAGNIFICATION_CONNECTION;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_PACKAGE_BROADCAST_RECEIVER;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_USER_BROADCAST_RECEIVER;
-import static android.accessibilityservice.AccessibilityTrace.FLAGS_MAGNIFICATION_CONNECTION;
import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL;
import static android.companion.virtual.VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED;
import static android.companion.virtual.VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID;
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index 5ebe161..b90a66a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -26,16 +26,25 @@
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayController;
import android.accessibilityservice.TouchInteractionController;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
+import android.bluetooth.BluetoothAdapter;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
import android.os.Binder;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
@@ -44,6 +53,7 @@
import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings;
+import android.text.TextUtils;
import android.util.Slog;
import android.view.Display;
import android.view.MotionEvent;
@@ -55,6 +65,8 @@
import com.android.server.wm.WindowManagerInternal;
import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Objects;
import java.util.Set;
/**
@@ -82,6 +94,9 @@
final Intent mIntent;
final ActivityTaskManagerInternal mActivityTaskManagerService;
+ private BrailleDisplayConnection mBrailleDisplayConnection;
+ private List<Bundle> mTestBrailleDisplays = null;
+
private final Handler mMainHandler;
private static final class AccessibilityInputMethodSessionCallback
@@ -448,6 +463,16 @@
}
}
+ @Override
+ public void resetLocked() {
+ super.resetLocked();
+ if (android.view.accessibility.Flags.brailleDisplayHid()) {
+ if (mBrailleDisplayConnection != null) {
+ mBrailleDisplayConnection.disconnect();
+ }
+ }
+ }
+
public boolean isAccessibilityButtonAvailableLocked(AccessibilityUserState userState) {
// If the service does not request the accessibility button, it isn't available
if (!mRequestAccessibilityButton) {
@@ -640,4 +665,123 @@
}
}
}
+
+ private void checkAccessibilityAccessLocked() {
+ if (!hasRightsToCurrentUserLocked()
+ || !mSecurityPolicy.checkAccessibilityAccess(this)) {
+ throw new SecurityException("Caller does not have accessibility access");
+ }
+ }
+
+ /**
+ * Sets up a BrailleDisplayConnection interface for the requested Bluetooth-connected
+ * Braille display.
+ *
+ * @param bluetoothAddress The address from
+ * {@link android.bluetooth.BluetoothDevice#getAddress()}.
+ */
+ @Override
+ @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
+ public void connectBluetoothBrailleDisplay(
+ @NonNull String bluetoothAddress, @NonNull IBrailleDisplayController controller) {
+ if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+ throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+ }
+ Objects.requireNonNull(bluetoothAddress);
+ Objects.requireNonNull(controller);
+ mContext.enforceCallingPermission(Manifest.permission.BLUETOOTH_CONNECT,
+ "Missing BLUETOOTH_CONNECT permission");
+ if (!BluetoothAdapter.checkBluetoothAddress(bluetoothAddress)) {
+ throw new IllegalArgumentException(
+ bluetoothAddress + " is not a valid Bluetooth address");
+ }
+ synchronized (mLock) {
+ checkAccessibilityAccessLocked();
+ if (mBrailleDisplayConnection != null) {
+ throw new IllegalStateException(
+ "This service already has a connected Braille display");
+ }
+ BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+ if (mTestBrailleDisplays != null) {
+ connection.setTestData(mTestBrailleDisplays);
+ }
+ connection.connectLocked(
+ bluetoothAddress, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+ }
+ }
+
+ /**
+ * Sets up a BrailleDisplayConnection interface for the requested USB-connected
+ * Braille display.
+ *
+ * <p>The caller package must already have USB permission for this {@link UsbDevice}.
+ */
+ @SuppressLint("MissingPermission") // system_server has the required MANAGE_USB permission
+ @Override
+ @NonNull
+ public void connectUsbBrailleDisplay(@NonNull UsbDevice usbDevice,
+ @NonNull IBrailleDisplayController controller) {
+ if (!android.view.accessibility.Flags.brailleDisplayHid()) {
+ throw new IllegalStateException("Flag BRAILLE_DISPLAY_HID not enabled");
+ }
+ Objects.requireNonNull(usbDevice);
+ Objects.requireNonNull(controller);
+ final UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
+ final String usbSerialNumber;
+ final int uid = Binder.getCallingUid();
+ final int pid = Binder.getCallingPid();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ if (usbManager == null || !usbManager.hasPermission(
+ usbDevice, mComponentName.getPackageName(), /*pid=*/ pid, /*uid=*/ uid)) {
+ throw new SecurityException(
+ "Caller does not have permission to access this UsbDevice");
+ }
+ usbSerialNumber = usbDevice.getSerialNumber();
+ if (TextUtils.isEmpty(usbSerialNumber)) {
+ // If the UsbDevice does not report a serial number for locating the HIDRAW
+ // node then notify connection error ERROR_BRAILLE_DISPLAY_NOT_FOUND.
+ try {
+ controller.onConnectionFailed(BrailleDisplayController.BrailleDisplayCallback
+ .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+ } catch (RemoteException e) {
+ Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+ }
+ return;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ synchronized (mLock) {
+ checkAccessibilityAccessLocked();
+ if (mBrailleDisplayConnection != null) {
+ throw new IllegalStateException(
+ "This service already has a connected Braille display");
+ }
+ BrailleDisplayConnection connection = new BrailleDisplayConnection(mLock, this);
+ if (mTestBrailleDisplays != null) {
+ connection.setTestData(mTestBrailleDisplays);
+ }
+ connection.connectLocked(
+ usbSerialNumber, BrailleDisplayConnection.BUS_USB, controller);
+ }
+ }
+
+ @Override
+ @RequiresPermission(Manifest.permission.MANAGE_ACCESSIBILITY)
+ public void setTestBrailleDisplayData(List<Bundle> brailleDisplays) {
+ // Enforce that this TestApi is only called by trusted (test) callers.
+ mContext.enforceCallingPermission(Manifest.permission.MANAGE_ACCESSIBILITY,
+ "Missing MANAGE_ACCESSIBILITY permission");
+ mTestBrailleDisplays = brailleDisplays;
+ }
+
+ void onBrailleDisplayConnectedLocked(BrailleDisplayConnection connection) {
+ mBrailleDisplayConnection = connection;
+ }
+
+ // Reset state when the BrailleDisplayConnection object disconnects itself.
+ void onBrailleDisplayDisconnectedLocked() {
+ mBrailleDisplayConnection = null;
+ }
}
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
new file mode 100644
index 0000000..1f18e15
--- /dev/null
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -0,0 +1,534 @@
+/*
+ * 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.accessibility;
+
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+import static android.accessibilityservice.BrailleDisplayController.BrailleDisplayCallback.FLAG_ERROR_CANNOT_ACCESS;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.PermissionManuallyEnforced;
+import android.annotation.RequiresNoPermission;
+import android.bluetooth.BluetoothDevice;
+import android.hardware.usb.UsbDevice;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Pair;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * This class represents the connection between {@code system_server} and a connected
+ * Braille Display using the Braille Display HID standard (usage page 0x41).
+ */
+class BrailleDisplayConnection extends IBrailleDisplayConnection.Stub {
+ private static final String LOG_TAG = "BrailleDisplayConnection";
+
+ /**
+ * Represents the connection type of a Braille display.
+ *
+ * <p>The integer values must match the kernel's bus type values because this bus type is
+ * used to locate the correct HIDRAW node using data from the kernel. These values come
+ * from the UAPI header file bionic/libc/kernel/uapi/linux/input.h, which is guaranteed
+ * to stay constant.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = {"BUS_"}, value = {
+ BUS_UNKNOWN,
+ BUS_USB,
+ BUS_BLUETOOTH
+ })
+ @interface BusType {
+ }
+ static final int BUS_UNKNOWN = -1;
+ static final int BUS_USB = 0x03;
+ static final int BUS_BLUETOOTH = 0x05;
+
+ // Access to this static object must be guarded by a lock that is shared for all instances
+ // of this class: the singular Accessibility system_server lock (mLock).
+ private static final Set<File> sConnectedNodes = new ArraySet<>();
+
+ // Used to guard to AIDL methods from concurrent calls.
+ // Lock must match the one used by AccessibilityServiceConnection, which itself
+ // comes from AccessibilityManagerService.
+ private final Object mLock;
+ private final AccessibilityServiceConnection mServiceConnection;
+
+
+ private File mHidrawNode;
+ private IBrailleDisplayController mController;
+
+ private Thread mInputThread;
+ private OutputStream mOutputStream;
+ private HandlerThread mOutputThread;
+
+ // mScanner is not final because tests may modify this to use a test-only scanner.
+ private BrailleDisplayScanner mScanner;
+
+ BrailleDisplayConnection(@NonNull Object lock,
+ @NonNull AccessibilityServiceConnection serviceConnection) {
+ this.mLock = Objects.requireNonNull(lock);
+ this.mScanner = getDefaultNativeScanner(getDefaultNativeInterface());
+ this.mServiceConnection = Objects.requireNonNull(serviceConnection);
+ }
+
+ /**
+ * Interface to scan for properties of connected Braille displays.
+ *
+ * <p>Helps simplify testing Braille Display APIs using test data without requiring
+ * a real Braille display to be connected to the device, by using a test implementation
+ * of this interface.
+ *
+ * @see #getDefaultNativeScanner
+ * @see #setTestData
+ */
+ @VisibleForTesting
+ interface BrailleDisplayScanner {
+ Collection<Path> getHidrawNodePaths();
+
+ byte[] getDeviceReportDescriptor(@NonNull Path path);
+
+ String getUniqueId(@NonNull Path path);
+
+ @BusType
+ int getDeviceBusType(@NonNull Path path);
+ }
+
+ /**
+ * Finds the Braille display HIDRAW node associated with the provided unique ID.
+ *
+ * <p>If found, saves instance state for this connection and starts a thread to
+ * read from the Braille display.
+ *
+ * @param expectedUniqueId The expected unique ID of the device to connect, from
+ * {@link UsbDevice#getSerialNumber()}
+ * or {@link BluetoothDevice#getAddress()}
+ * @param expectedBusType The expected bus type from {@link BusType}.
+ * @param controller Interface containing oneway callbacks used to communicate with the
+ * {@link android.accessibilityservice.BrailleDisplayController}.
+ */
+ void connectLocked(
+ @NonNull String expectedUniqueId,
+ @BusType int expectedBusType,
+ @NonNull IBrailleDisplayController controller) {
+ Objects.requireNonNull(expectedUniqueId);
+ this.mController = Objects.requireNonNull(controller);
+
+ final List<Pair<File, byte[]>> result = new ArrayList<>();
+ final Collection<Path> hidrawNodePaths = mScanner.getHidrawNodePaths();
+ if (hidrawNodePaths == null) {
+ Slog.w(LOG_TAG, "Unable to access the HIDRAW node directory");
+ sendConnectionErrorLocked(FLAG_ERROR_CANNOT_ACCESS);
+ return;
+ }
+ boolean unableToGetDescriptor = false;
+ // For every present HIDRAW device node:
+ for (Path path : hidrawNodePaths) {
+ final byte[] descriptor = mScanner.getDeviceReportDescriptor(path);
+ if (descriptor == null) {
+ unableToGetDescriptor = true;
+ continue;
+ }
+ final String uniqueId = mScanner.getUniqueId(path);
+ if (isBrailleDisplay(descriptor)
+ && mScanner.getDeviceBusType(path) == expectedBusType
+ && expectedUniqueId.equalsIgnoreCase(uniqueId)) {
+ result.add(Pair.create(path.toFile(), descriptor));
+ }
+ }
+
+ // Return success only when exactly one matching device node is found.
+ if (result.size() != 1) {
+ @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode =
+ FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND;
+ // If we were unable to get some /dev/hidraw* descriptor then tell the accessibility
+ // service that the device may not have proper access to these device nodes.
+ if (unableToGetDescriptor) {
+ Slog.w(LOG_TAG, "Unable to access some HIDRAW node's descriptor");
+ errorCode |= FLAG_ERROR_CANNOT_ACCESS;
+ } else {
+ Slog.w(LOG_TAG,
+ "Unable to find a unique Braille display matching the provided device");
+ }
+ sendConnectionErrorLocked(errorCode);
+ return;
+ }
+
+ this.mHidrawNode = result.get(0).first;
+ final byte[] reportDescriptor = result.get(0).second;
+
+ // Only one connection instance should exist for this hidraw node, across
+ // all currently running accessibility services.
+ if (sConnectedNodes.contains(this.mHidrawNode)) {
+ Slog.w(LOG_TAG,
+ "Unable to find an unused Braille display matching the provided device");
+ sendConnectionErrorLocked(FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+ return;
+ }
+ sConnectedNodes.add(this.mHidrawNode);
+
+ startReadingLocked();
+
+ try {
+ mServiceConnection.onBrailleDisplayConnectedLocked(this);
+ mController.onConnected(this, reportDescriptor);
+ } catch (RemoteException e) {
+ Slog.e(LOG_TAG, "Error calling onConnected", e);
+ disconnect();
+ }
+ }
+
+ private void sendConnectionErrorLocked(
+ @BrailleDisplayController.BrailleDisplayCallback.ErrorCode int errorCode) {
+ try {
+ mController.onConnectionFailed(errorCode);
+ } catch (RemoteException e) {
+ Slog.e(LOG_TAG, "Error calling onConnectionFailed", e);
+ }
+ }
+
+ /** Returns true if this descriptor includes usages for the Braille display usage page 0x41. */
+ private static boolean isBrailleDisplay(byte[] descriptor) {
+ // TODO: b/316036493 - Check that descriptor includes 0x41 reports.
+ return true;
+ }
+
+ /**
+ * Checks that the AccessibilityService that owns this BrailleDisplayConnection
+ * is still connected to the system.
+ *
+ * @throws IllegalStateException if not connected
+ */
+ private void assertServiceIsConnectedLocked() {
+ if (!mServiceConnection.isConnectedLocked()) {
+ throw new IllegalStateException("Accessibility service is not connected");
+ }
+ }
+
+ /**
+ * Disconnects from this Braille display. This object is no longer valid after
+ * this call returns.
+ */
+ @Override
+ // This is a cleanup method, so allow the call even if the calling service was disabled.
+ @RequiresNoPermission
+ public void disconnect() {
+ synchronized (mLock) {
+ closeInputLocked();
+ closeOutputLocked();
+ mServiceConnection.onBrailleDisplayDisconnectedLocked();
+ try {
+ mController.onDisconnected();
+ } catch (RemoteException e) {
+ Slog.e(LOG_TAG, "Error calling onDisconnected");
+ }
+ sConnectedNodes.remove(this.mHidrawNode);
+ }
+ }
+
+ /**
+ * Writes the provided HID bytes to this Braille display.
+ *
+ * <p>Writes are posted to a background thread handler.
+ *
+ * @param buffer The bytes to write to the Braille display. These bytes should be formatted
+ * according to the report descriptor.
+ */
+ @Override
+ @PermissionManuallyEnforced // by assertServiceIsConnectedLocked()
+ public void write(@NonNull byte[] buffer) {
+ Objects.requireNonNull(buffer);
+ if (buffer.length > IBinder.getSuggestedMaxIpcSizeBytes()) {
+ Slog.e(LOG_TAG, "Requested write of size " + buffer.length
+ + " which is larger than maximum " + IBinder.getSuggestedMaxIpcSizeBytes());
+ return;
+ }
+ synchronized (mLock) {
+ assertServiceIsConnectedLocked();
+ if (mOutputThread == null) {
+ try {
+ mOutputStream = new FileOutputStream(mHidrawNode);
+ } catch (FileNotFoundException e) {
+ Slog.e(LOG_TAG, "Unable to create write stream", e);
+ disconnect();
+ return;
+ }
+ mOutputThread = new HandlerThread("BrailleDisplayConnection output thread",
+ Process.THREAD_PRIORITY_BACKGROUND);
+ mOutputThread.setDaemon(true);
+ mOutputThread.start();
+ }
+ // TODO: b/316035785 - Proactively disconnect a misbehaving Braille display by calling
+ // disconnect() if the mOutputThread handler queue grows too large.
+ mOutputThread.getThreadHandler().post(() -> {
+ try {
+ mOutputStream.write(buffer);
+ } catch (IOException e) {
+ Slog.d(LOG_TAG, "Error writing to connected Braille display", e);
+ disconnect();
+ }
+ });
+ }
+ }
+
+ /**
+ * Starts reading HID bytes from this Braille display.
+ *
+ * <p>Reads are performed on a background thread.
+ */
+ private void startReadingLocked() {
+ mInputThread = new Thread(() -> {
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ try (InputStream inputStream = new FileInputStream(mHidrawNode)) {
+ final byte[] buffer = new byte[IBinder.getSuggestedMaxIpcSizeBytes()];
+ int readSize;
+ while (!Thread.interrupted()) {
+ if (!mHidrawNode.exists()) {
+ disconnect();
+ break;
+ }
+ // Reading from the HIDRAW character device node will block
+ // until bytes are available.
+ readSize = inputStream.read(buffer);
+ if (readSize > 0) {
+ try {
+ // Send the input to the AccessibilityService.
+ mController.onInput(Arrays.copyOfRange(buffer, 0, readSize));
+ } catch (RemoteException e) {
+ // Error communicating with the AccessibilityService.
+ Slog.e(LOG_TAG, "Error calling onInput", e);
+ disconnect();
+ break;
+ }
+ }
+ }
+ } catch (IOException e) {
+ Slog.d(LOG_TAG, "Error reading from connected Braille display", e);
+ disconnect();
+ }
+ }, "BrailleDisplayConnection input thread");
+ mInputThread.setDaemon(true);
+ mInputThread.start();
+ }
+
+ /** Stop the Input thread. */
+ private void closeInputLocked() {
+ if (mInputThread != null) {
+ mInputThread.interrupt();
+ }
+ mInputThread = null;
+ }
+
+ /** Stop the Output thread and close the Output stream. */
+ private void closeOutputLocked() {
+ if (mOutputThread != null) {
+ mOutputThread.quit();
+ }
+ mOutputThread = null;
+ if (mOutputStream != null) {
+ try {
+ mOutputStream.close();
+ } catch (IOException e) {
+ Slog.e(LOG_TAG, "Unable to close output stream", e);
+ }
+ }
+ mOutputStream = null;
+ }
+
+ /**
+ * Returns a {@link BrailleDisplayScanner} that opens {@link FileInputStream}s to read
+ * from HIDRAW nodes and perform ioctls using the provided {@link NativeInterface}.
+ */
+ @VisibleForTesting
+ BrailleDisplayScanner getDefaultNativeScanner(@NonNull NativeInterface nativeInterface) {
+ Objects.requireNonNull(nativeInterface);
+ return new BrailleDisplayScanner() {
+ private static final Path DEVICE_DIR = Path.of("/dev");
+ private static final String HIDRAW_DEVICE_GLOB = "hidraw*";
+
+ @Override
+ public Collection<Path> getHidrawNodePaths() {
+ final List<Path> result = new ArrayList<>();
+ try (DirectoryStream<Path> hidrawNodePaths = Files.newDirectoryStream(
+ DEVICE_DIR, HIDRAW_DEVICE_GLOB)) {
+ for (Path path : hidrawNodePaths) {
+ result.add(path);
+ }
+ return result;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private <T> T readFromFileDescriptor(Path path, Function<Integer, T> readFn) {
+ try (FileInputStream stream = new FileInputStream(path.toFile())) {
+ return readFn.apply(stream.getFD().getInt$());
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+ Objects.requireNonNull(path);
+ return readFromFileDescriptor(path, fd -> {
+ final int descSize = nativeInterface.getHidrawDescSize(fd);
+ if (descSize > 0) {
+ return nativeInterface.getHidrawDesc(fd, descSize);
+ }
+ return null;
+ });
+ }
+
+ @Override
+ public String getUniqueId(@NonNull Path path) {
+ Objects.requireNonNull(path);
+ return readFromFileDescriptor(path, nativeInterface::getHidrawUniq);
+ }
+
+ @Override
+ public int getDeviceBusType(@NonNull Path path) {
+ Objects.requireNonNull(path);
+ Integer busType = readFromFileDescriptor(path, nativeInterface::getHidrawBusType);
+ return busType != null ? busType : BUS_UNKNOWN;
+ }
+ };
+ }
+
+ /**
+ * Sets test data to be used by CTS tests.
+ *
+ * <p>Replaces the default {@link BrailleDisplayScanner} object for this connection,
+ * and also returns it to allow unit testing this test-only implementation.
+ *
+ * @see BrailleDisplayController#setTestBrailleDisplayData
+ */
+ BrailleDisplayScanner setTestData(@NonNull List<Bundle> brailleDisplays) {
+ Objects.requireNonNull(brailleDisplays);
+ final Map<Path, Bundle> brailleDisplayMap = new ArrayMap<>();
+ for (Bundle brailleDisplay : brailleDisplays) {
+ Path hidrawNodePath = Path.of(brailleDisplay.getString(
+ BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH));
+ brailleDisplayMap.put(hidrawNodePath, brailleDisplay);
+ }
+ synchronized (mLock) {
+ mScanner = new BrailleDisplayScanner() {
+ @Override
+ public Collection<Path> getHidrawNodePaths() {
+ return brailleDisplayMap.keySet();
+ }
+
+ @Override
+ public byte[] getDeviceReportDescriptor(@NonNull Path path) {
+ return brailleDisplayMap.get(path).getByteArray(
+ BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR);
+ }
+
+ @Override
+ public String getUniqueId(@NonNull Path path) {
+ return brailleDisplayMap.get(path).getString(
+ BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID);
+ }
+
+ @Override
+ public int getDeviceBusType(@NonNull Path path) {
+ return brailleDisplayMap.get(path).getBoolean(
+ BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH)
+ ? BUS_BLUETOOTH : BUS_USB;
+ }
+ };
+ return mScanner;
+ }
+ }
+
+ /**
+ * This interface exists to support unit testing {@link #getDefaultNativeScanner}.
+ */
+ @VisibleForTesting
+ interface NativeInterface {
+ int getHidrawDescSize(int fd);
+
+ byte[] getHidrawDesc(int fd, int descSize);
+
+ String getHidrawUniq(int fd);
+
+ int getHidrawBusType(int fd);
+ }
+
+ /** Native interface that actually calls native HIDRAW ioctls. */
+ private NativeInterface getDefaultNativeInterface() {
+ return new NativeInterface() {
+ @Override
+ public int getHidrawDescSize(int fd) {
+ return nativeGetHidrawDescSize(fd);
+ }
+
+ @Override
+ public byte[] getHidrawDesc(int fd, int descSize) {
+ return nativeGetHidrawDesc(fd, descSize);
+ }
+
+ @Override
+ public String getHidrawUniq(int fd) {
+ return nativeGetHidrawUniq(fd);
+ }
+
+ @Override
+ public int getHidrawBusType(int fd) {
+ return nativeGetHidrawBusType(fd);
+ }
+ };
+ }
+
+ private native int nativeGetHidrawDescSize(int fd);
+
+ private native byte[] nativeGetHidrawDesc(int fd, int descSize);
+
+ private native String nativeGetHidrawUniq(int fd);
+
+ private native int nativeGetHidrawBusType(int fd);
+}
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java
index 2c50389..df4e699 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionManagerService.java
@@ -35,6 +35,7 @@
import android.content.pm.ParceledListSlice;
import android.os.Binder;
import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.Process;
import android.os.ResultReceiver;
import android.os.ShellCallback;
@@ -162,6 +163,13 @@
(service) -> service.onDestroyPredictionSessionLocked(sessionId));
}
+ @Override
+ public void requestServiceFeatures(@NonNull AppPredictionSessionId sessionId,
+ IRemoteCallback callback) {
+ runForUserLocked("requestServiceFeatures", sessionId,
+ (service) -> service.requestServiceFeaturesLocked(sessionId, callback));
+ }
+
public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out,
@Nullable FileDescriptor err,
@NonNull String[] args, @Nullable ShellCallback callback,
diff --git a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
index 84707a8..a0198f2 100644
--- a/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
+++ b/services/appprediction/java/com/android/server/appprediction/AppPredictionPerUserService.java
@@ -31,6 +31,7 @@
import android.content.pm.ParceledListSlice;
import android.content.pm.ServiceInfo;
import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.provider.DeviceConfig;
@@ -237,6 +238,18 @@
sessionInfo.destroy();
}
+ /**
+ * Requests the service to provide AppPredictionService features info.
+ */
+ @GuardedBy("mLock")
+ public void requestServiceFeaturesLocked(@NonNull AppPredictionSessionId sessionId,
+ @NonNull IRemoteCallback callback) {
+ final AppPredictionSessionInfo sessionInfo = mSessionInfos.get(sessionId);
+ if (sessionInfo == null) return;
+ resolveService(sessionId, true, sessionInfo.mUsesPeopleService,
+ s -> s.requestServiceFeatures(sessionId, callback));
+ }
+
@Override
public void onFailureOrTimeout(boolean timedOut) {
if (isDebug()) {
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index f619ca3..44d0132 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -16,6 +16,8 @@
package com.android.server;
+import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
+
import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.NonNull;
@@ -24,11 +26,10 @@
import android.content.Context;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
-import android.os.Handler;
-import android.os.Looper;
import android.os.RemoteException;
import android.os.Trace;
import android.os.UserHandle;
+import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
@@ -117,7 +118,7 @@
// TODO(b/317250444): use MediaProjectionManagerService directly, reduces unnecessary
// handler, delegate, and binder death recipient
- mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper()));
+ mProjectionManager.addCallback(mProjectionCallback, getContext().getMainThreadHandler());
try {
mNotificationListener.registerAsSystemService(
@@ -148,6 +149,15 @@
}
private void onProjectionStart() {
+ // TODO(b/324447419): move GlobalSettings lookup to background thread
+ boolean disableScreenShareProtections =
+ Settings.Global.getInt(getContext().getContentResolver(),
+ DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
+ if (disableScreenShareProtections) {
+ Log.w(TAG, "Screen share protections disabled, ignoring projection start");
+ return;
+ }
+
synchronized (mSensitiveContentProtectionLock) {
mProjectionActive = true;
updateAppsThatShouldBlockScreenCapture();
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index cd45b03..b8f6b3f 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -480,9 +480,14 @@
/**
* The available ANR timers.
*/
+ // ActivityManagerConstants.SERVICE_TIMEOUT/ActivityManagerConstants.SERVICE_BACKGROUND_TIMEOUT
private final ProcessAnrTimer mActiveServiceAnrTimer;
+ // see ServiceRecord$ShortFgsInfo#getAnrTime()
private final ServiceAnrTimer mShortFGSAnrTimer;
+ // ActivityManagerConstants.DEFAULT_SERVICE_START_FOREGROUND_TIMEOUT_MS
private final ServiceAnrTimer mServiceFGAnrTimer;
+ // see ServiceRecord#getEarliestStopTypeAndTime()
+ private final ServiceAnrTimer mFGSAnrTimer;
// allowlisted packageName.
ArraySet<String> mAllowListWhileInUsePermissionInFgs = new ArraySet<>();
@@ -744,10 +749,13 @@
"SERVICE_TIMEOUT");
this.mShortFGSAnrTimer = new ServiceAnrTimer(service,
ActivityManagerService.SERVICE_SHORT_FGS_ANR_TIMEOUT_MSG,
- "FGS_TIMEOUT");
+ "SHORT_FGS_TIMEOUT");
this.mServiceFGAnrTimer = new ServiceAnrTimer(service,
ActivityManagerService.SERVICE_FOREGROUND_TIMEOUT_MSG,
"SERVICE_FOREGROUND_TIMEOUT");
+ this.mFGSAnrTimer = new ServiceAnrTimer(service,
+ ActivityManagerService.SERVICE_FGS_ANR_TIMEOUT_MSG,
+ "FGS_TIMEOUT");
}
void systemServicesReady() {
@@ -1570,6 +1578,7 @@
}
maybeStopShortFgsTimeoutLocked(service);
+ maybeStopFgsTimeoutLocked(service);
final int uid = service.appInfo.uid;
final String packageName = service.name.getPackageName();
@@ -1758,6 +1767,7 @@
}
maybeStopShortFgsTimeoutLocked(r);
+ maybeStopFgsTimeoutLocked(r);
final int uid = r.appInfo.uid;
final String packageName = r.name.getPackageName();
@@ -2243,6 +2253,8 @@
// Whether to extend the SHORT_SERVICE time out.
boolean extendShortServiceTimeout = false;
+ // Whether to extend the timeout for a time-limited FGS type.
+ boolean extendFgsTimeout = false;
// Whether setFgsRestrictionLocked() is called in here. Only used for logging.
boolean fgsRestrictionRecalculated = false;
@@ -2287,6 +2299,19 @@
final boolean isOldTypeShortFgsAndTimedOut =
r.shouldTriggerShortFgsTimeout(nowUptime);
+ // Calling startForeground on a FGS type which has a time limit will only be
+ // allowed if the app is in a state where it can normally start another FGS.
+ // The timeout will behave as follows:
+ // A) <TIME_LIMITED_TYPE> -> another <TIME_LIMITED_TYPE>
+ // - If the start succeeds, the timeout is reset.
+ // B) <TIME_LIMITED_TYPE> -> non-time-limited type
+ // - If the start succeeds, the timeout will stop.
+ // C) non-time-limited type -> <TIME_LIMITED_TYPE>
+ // - If the start succeeds, the timeout will start.
+ final boolean isOldTypeTimeLimited = r.isFgsTimeLimited();
+ final boolean isNewTypeTimeLimited =
+ r.canFgsTypeTimeOut(foregroundServiceType);
+
// If true, we skip the BFSL check.
boolean bypassBfslCheck = false;
@@ -2355,6 +2380,35 @@
// "if (r.mAllowStartForeground == REASON_DENIED...)" block below.
}
}
+ } else if (r.isForeground && isOldTypeTimeLimited) {
+
+ // See if the app could start an FGS or not.
+ r.clearFgsAllowStart();
+ setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(),
+ r.appInfo.uid, r.intent.getIntent(), r, r.userId,
+ BackgroundStartPrivileges.NONE, false /* isBindService */);
+ fgsRestrictionRecalculated = true;
+
+ final boolean fgsStartAllowed = !isBgFgsRestrictionEnabledForService
+ || r.isFgsAllowedStart();
+
+ if (fgsStartAllowed) {
+ if (isNewTypeTimeLimited) {
+ // Note: in the future, we may want to look into metrics to see if
+ // apps are constantly switching between a time-limited type and a
+ // non-time-limited type or constantly calling startForeground()
+ // opportunistically on the same type to gain runtime and apply the
+ // stricter timeout. For now, always extend the timeout if the app
+ // is in a state where it's allowed to start a FGS.
+ extendFgsTimeout = true;
+ } else {
+ // FGS type is changing from a time-restricted type to one without
+ // a time limit so proceed as normal.
+ // The timeout will stop later, in maybeUpdateFgsTrackingLocked().
+ }
+ } else {
+ // This case will be handled in the BFSL check below.
+ }
} else if (r.mStartForegroundCount == 0) {
/*
If the service was started with startService(), not
@@ -2596,6 +2650,7 @@
maybeUpdateShortFgsTrackingLocked(r,
extendShortServiceTimeout);
+ maybeUpdateFgsTrackingLocked(r, extendFgsTimeout);
} else {
if (DEBUG_FOREGROUND_SERVICE) {
Slog.d(TAG, "Suppressing startForeground() for FAS " + r);
@@ -2631,6 +2686,7 @@
}
maybeStopShortFgsTimeoutLocked(r);
+ maybeStopFgsTimeoutLocked(r);
// Adjust notification handling before setting isForeground to false, because
// that state is relevant to the notification policy side.
@@ -3608,6 +3664,116 @@
}
}
+ void onFgsTimeout(ServiceRecord sr) {
+ synchronized (mAm) {
+ final long nowUptime = SystemClock.uptimeMillis();
+ final int fgsType = sr.getTimedOutFgsType(nowUptime);
+ if (fgsType == -1) {
+ mFGSAnrTimer.discard(sr);
+ return;
+ }
+ Slog.e(TAG_SERVICE, "FGS (" + ServiceInfo.foregroundServiceTypeToLabel(fgsType)
+ + ") timed out: " + sr);
+ mFGSAnrTimer.accept(sr);
+ traceInstant("FGS timed out: ", sr);
+
+ logFGSStateChangeLocked(sr,
+ FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT,
+ nowUptime > sr.mFgsEnterTime ? (int) (nowUptime - sr.mFgsEnterTime) : 0,
+ FGS_STOP_REASON_UNKNOWN,
+ FGS_TYPE_POLICY_CHECK_UNKNOWN,
+ FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
+ false /* fgsRestrictionRecalculated */
+ );
+ try {
+ sr.app.getThread().scheduleTimeoutServiceForType(sr, sr.getLastStartId(), fgsType);
+ } catch (RemoteException e) {
+ Slog.w(TAG_SERVICE, "Exception from scheduleTimeoutServiceForType: " + e);
+ }
+
+ // ANR the service after giving the service some time to clean up.
+ // ServiceRecord.getEarliestStopTypeAndTime() is an absolute time with a reference that
+ // is not "now". Compute the time from "now" when starting the anr timer.
+ final long anrTime = sr.getEarliestStopTypeAndTime().second
+ + mAm.mConstants.mFgsAnrExtraWaitDuration - SystemClock.uptimeMillis();
+ mFGSAnrTimer.start(sr, anrTime);
+ }
+ }
+
+ private void maybeUpdateFgsTrackingLocked(ServiceRecord sr, boolean extendTimeout) {
+ if (!sr.isFgsTimeLimited()) {
+ // Reset timers if they exist.
+ sr.setIsFgsTimeLimited(false);
+ mFGSAnrTimer.cancel(sr);
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+ return;
+ }
+
+ if (extendTimeout || !sr.wasFgsPreviouslyTimeLimited()) {
+ traceInstant("FGS start: ", sr);
+ sr.setIsFgsTimeLimited(true);
+
+ // We'll restart the timeout.
+ mFGSAnrTimer.cancel(sr);
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+
+ final Message msg = mAm.mHandler.obtainMessage(
+ ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+ mAm.mHandler.sendMessageAtTime(msg, sr.getEarliestStopTypeAndTime().second);
+ }
+ }
+
+ private void maybeStopFgsTimeoutLocked(ServiceRecord sr) {
+ sr.setIsFgsTimeLimited(false); // reset fgs boolean holding time-limited type state.
+ if (!sr.isFgsTimeLimited()) {
+ return; // if none of the types are time-limited, return.
+ }
+ Slog.d(TAG_SERVICE, "Stop FGS timeout: " + sr);
+ mFGSAnrTimer.cancel(sr);
+ mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+ }
+
+ boolean hasServiceTimedOutLocked(ComponentName className, IBinder token) {
+ final int userId = UserHandle.getCallingUserId();
+ final long ident = mAm.mInjector.clearCallingIdentity();
+ try {
+ ServiceRecord sr = findServiceLocked(className, token, userId);
+ if (sr == null) {
+ return false;
+ }
+ final long nowUptime = SystemClock.uptimeMillis();
+ return sr.getTimedOutFgsType(nowUptime) != -1;
+ } finally {
+ mAm.mInjector.restoreCallingIdentity(ident);
+ }
+ }
+
+ void onFgsAnrTimeout(ServiceRecord sr) {
+ final long nowUptime = SystemClock.uptimeMillis();
+ final int fgsType = sr.getTimedOutFgsType(nowUptime);
+ if (fgsType == -1 || !sr.wasFgsPreviouslyTimeLimited()) {
+ return; // no timed out FGS type was found
+ }
+ final String reason = "A foreground service of type "
+ + ServiceInfo.foregroundServiceTypeToLabel(fgsType)
+ + " did not stop within a timeout: " + sr.getComponentName();
+
+ final TimeoutRecord tr = TimeoutRecord.forFgsTimeout(reason);
+
+ tr.mLatencyTracker.waitingOnAMSLockStarted();
+ synchronized (mAm) {
+ tr.mLatencyTracker.waitingOnAMSLockEnded();
+
+ Slog.e(TAG_SERVICE, "FGS ANR'ed: " + sr);
+ traceInstant("FGS ANR: ", sr);
+ mAm.appNotResponding(sr.app, tr);
+
+ // TODO: Can we close the ANR dialog here, if it's still shown? Currently, the ANR
+ // dialog really doesn't remember the "cause" (especially if there have been multiple
+ // ANRs), so it's not doable.
+ }
+ }
+
private void updateAllowlistManagerLocked(ProcessServiceRecord psr) {
psr.mAllowlistManager = false;
for (int i = psr.numberOfRunningServices() - 1; i >= 0; i--) {
@@ -3621,6 +3787,7 @@
private void stopServiceAndUpdateAllowlistManagerLocked(ServiceRecord service) {
maybeStopShortFgsTimeoutLocked(service);
+ maybeStopFgsTimeoutLocked(service);
final ProcessServiceRecord psr = service.app.mServices;
psr.stopService(service);
psr.updateBoundClientUids();
@@ -5893,6 +6060,7 @@
Slog.w(TAG_SERVICE, "Short FGS brought down without stopping: " + r);
maybeStopShortFgsTimeoutLocked(r);
}
+ maybeStopFgsTimeoutLocked(r);
// Report to all of the connections that the service is no longer
// available.
@@ -6015,6 +6183,7 @@
final boolean exitingFg = r.isForeground;
if (exitingFg) {
maybeStopShortFgsTimeoutLocked(r);
+ maybeStopFgsTimeoutLocked(r);
decActiveForegroundAppLocked(smap, r);
synchronized (mAm.mProcessStats.mLock) {
ServiceState stracker = r.getTracker();
@@ -8643,7 +8812,7 @@
event = EventLogTags.AM_FOREGROUND_SERVICE_DENIED;
} else if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT) {
event = EventLogTags.AM_FOREGROUND_SERVICE_TIMED_OUT;
- }else {
+ } else {
// Unknown event.
return;
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 72e62c3..d97731c 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -1052,6 +1052,27 @@
public volatile long mShortFgsProcStateExtraWaitDuration =
DEFAULT_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION;
+ /** Timeout for a mediaProcessing FGS, in milliseconds. */
+ private static final String KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION =
+ "media_processing_fgs_timeout_duration";
+
+ /** @see #KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION */
+ static final long DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION = 6 * 60 * 60_000; // 6 hours
+
+ /** @see #KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION */
+ public volatile long mMediaProcessingFgsTimeoutDuration =
+ DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION;
+
+ /** Timeout for a dataSync FGS, in milliseconds. */
+ private static final String KEY_DATA_SYNC_FGS_TIMEOUT_DURATION =
+ "data_sync_fgs_timeout_duration";
+
+ /** @see #KEY_DATA_SYNC_FGS_TIMEOUT_DURATION */
+ static final long DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION = 6 * 60 * 60_000; // 6 hours
+
+ /** @see #KEY_DATA_SYNC_FGS_TIMEOUT_DURATION */
+ public volatile long mDataSyncFgsTimeoutDuration = DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION;
+
/**
* If enabled, when starting an application, the system will wait for a
* {@link ActivityManagerService#finishAttachApplication} from the app before scheduling
@@ -1082,6 +1103,20 @@
public volatile long mShortFgsAnrExtraWaitDuration =
DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION;
+ /**
+ * If a service of a timeout-enforced type doesn't finish within this duration after its
+ * timeout, then we'll declare an ANR.
+ * i.e. if the time limit for a type is 1 hour, and this extra duration is 10 seconds, then
+ * the app will be ANR'ed 1 hour and 10 seconds after it started.
+ */
+ private static final String KEY_FGS_ANR_EXTRA_WAIT_DURATION = "fgs_anr_extra_wait_duration";
+
+ /** @see #KEY_FGS_ANR_EXTRA_WAIT_DURATION */
+ static final long DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION = 10_000;
+
+ /** @see #KEY_FGS_ANR_EXTRA_WAIT_DURATION */
+ public volatile long mFgsAnrExtraWaitDuration = DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION;
+
/** @see #KEY_USE_TIERED_CACHED_ADJ */
public boolean USE_TIERED_CACHED_ADJ = DEFAULT_USE_TIERED_CACHED_ADJ;
@@ -1264,9 +1299,18 @@
case KEY_SHORT_FGS_PROC_STATE_EXTRA_WAIT_DURATION:
updateShortFgsProcStateExtraWaitDuration();
break;
+ case KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION:
+ updateMediaProcessingFgsTimeoutDuration();
+ break;
+ case KEY_DATA_SYNC_FGS_TIMEOUT_DURATION:
+ updateDataSyncFgsTimeoutDuration();
+ break;
case KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION:
updateShortFgsAnrExtraWaitDuration();
break;
+ case KEY_FGS_ANR_EXTRA_WAIT_DURATION:
+ updateFgsAnrExtraWaitDuration();
+ break;
case KEY_PROACTIVE_KILLS_ENABLED:
updateProactiveKillsEnabled();
break;
@@ -2064,6 +2108,27 @@
DEFAULT_SHORT_FGS_ANR_EXTRA_WAIT_DURATION);
}
+ private void updateMediaProcessingFgsTimeoutDuration() {
+ mMediaProcessingFgsTimeoutDuration = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION,
+ DEFAULT_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION);
+ }
+
+ private void updateDataSyncFgsTimeoutDuration() {
+ mDataSyncFgsTimeoutDuration = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_DATA_SYNC_FGS_TIMEOUT_DURATION,
+ DEFAULT_DATA_SYNC_FGS_TIMEOUT_DURATION);
+ }
+
+ private void updateFgsAnrExtraWaitDuration() {
+ mFgsAnrExtraWaitDuration = DeviceConfig.getLong(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
+ KEY_FGS_ANR_EXTRA_WAIT_DURATION,
+ DEFAULT_FGS_ANR_EXTRA_WAIT_DURATION);
+ }
+
private void updateEnableWaitForFinishAttachApplication() {
mEnableWaitForFinishAttachApplication = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_ACTIVITY_MANAGER,
@@ -2295,6 +2360,13 @@
pw.print(" "); pw.print(KEY_SHORT_FGS_ANR_EXTRA_WAIT_DURATION);
pw.print("="); pw.println(mShortFgsAnrExtraWaitDuration);
+ pw.print(" "); pw.print(KEY_MEDIA_PROCESSING_FGS_TIMEOUT_DURATION);
+ pw.print("="); pw.println(mMediaProcessingFgsTimeoutDuration);
+ pw.print(" "); pw.print(KEY_DATA_SYNC_FGS_TIMEOUT_DURATION);
+ pw.print("="); pw.println(mDataSyncFgsTimeoutDuration);
+ pw.print(" "); pw.print(KEY_FGS_ANR_EXTRA_WAIT_DURATION);
+ pw.print("="); pw.println(mFgsAnrExtraWaitDuration);
+
pw.print(" "); pw.print(KEY_USE_TIERED_CACHED_ADJ);
pw.print("="); pw.println(USE_TIERED_CACHED_ADJ);
pw.print(" "); pw.print(KEY_TIERED_CACHED_ADJ_DECAY_TIME);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 2750344..bfdcb95 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1702,6 +1702,8 @@
static final int REMOVE_UID_FROM_OBSERVER_MSG = 81;
static final int BIND_APPLICATION_TIMEOUT_SOFT_MSG = 82;
static final int BIND_APPLICATION_TIMEOUT_HARD_MSG = 83;
+ static final int SERVICE_FGS_TIMEOUT_MSG = 84;
+ static final int SERVICE_FGS_ANR_TIMEOUT_MSG = 85;
static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -2064,6 +2066,12 @@
case BIND_APPLICATION_TIMEOUT_HARD_MSG: {
handleBindApplicationTimeoutHard((ProcessRecord) msg.obj);
} break;
+ case SERVICE_FGS_TIMEOUT_MSG: {
+ mServices.onFgsTimeout((ServiceRecord) msg.obj);
+ } break;
+ case SERVICE_FGS_ANR_TIMEOUT_MSG: {
+ mServices.onFgsAnrTimeout((ServiceRecord) msg.obj);
+ } break;
}
}
}
@@ -13794,6 +13802,13 @@
}
@Override
+ public boolean hasServiceTimeLimitExceeded(ComponentName className, IBinder token) {
+ synchronized (this) {
+ return mServices.hasServiceTimedOutLocked(className, token);
+ }
+ }
+
+ @Override
public int handleIncomingUser(int callingPid, int callingUid, int userId, boolean allowAll,
boolean requireFull, String name, String callerPackage) {
return mUserController.handleIncomingUser(callingPid, callingUid, userId, allowAll,
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 2771572..3c8d7fc 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -56,6 +56,7 @@
import android.os.UserHandle;
import android.provider.Settings;
import android.util.ArrayMap;
+import android.util.Pair;
import android.util.Slog;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -236,6 +237,8 @@
boolean mFgsNotificationShown;
// Whether FGS package has permissions to show notifications.
boolean mFgsHasNotificationPermission;
+ // Whether the FGS contains a type that is time limited.
+ private boolean mFgsIsTimeLimited;
// allow the service becomes foreground service? Service started from background may not be
// allowed to become a foreground service.
@@ -915,6 +918,7 @@
pw.print(prefix); pw.print("isForeground="); pw.print(isForeground);
pw.print(" foregroundId="); pw.print(foregroundId);
pw.printf(" types=%08X", foregroundServiceType);
+ pw.print(" fgsHasTimeLimitedType="); pw.print(mFgsIsTimeLimited);
pw.print(" foregroundNoti="); pw.println(foregroundNoti);
if (isShortFgs() && mShortFgsInfo != null) {
@@ -1789,6 +1793,83 @@
+ " " + (mShortFgsInfo == null ? "" : mShortFgsInfo.getDescription());
}
+ /**
+ * @return true if one of the types of this FGS has a time limit.
+ */
+ public boolean isFgsTimeLimited() {
+ return startRequested && isForeground && canFgsTypeTimeOut(foregroundServiceType);
+ }
+
+ /**
+ * Called when a FGS with a time-limited type starts ({@code true}) or stops ({@code false}).
+ */
+ public void setIsFgsTimeLimited(boolean fgsIsTimeLimited) {
+ this.mFgsIsTimeLimited = fgsIsTimeLimited;
+ }
+
+ /**
+ * @return whether {@link #mFgsIsTimeLimited} was previously set or not.
+ */
+ public boolean wasFgsPreviouslyTimeLimited() {
+ return mFgsIsTimeLimited;
+ }
+
+ /**
+ * @return the FGS type if the service has reached its time limit, otherwise -1.
+ */
+ public int getTimedOutFgsType(long nowUptime) {
+ if (!isAppAlive() || !isFgsTimeLimited()) {
+ return -1;
+ }
+
+ final Pair<Integer, Long> fgsTypeAndStopTime = getEarliestStopTypeAndTime();
+ if (fgsTypeAndStopTime.first != -1 && fgsTypeAndStopTime.second <= nowUptime) {
+ return fgsTypeAndStopTime.first;
+ }
+ return -1; // no fgs type exceeded time limit
+ }
+
+ /**
+ * @return a {@code Pair<fgs_type, stop_time>}, representing the earliest time at which the FGS
+ * should be stopped (fgs start time + time limit for most restrictive type)
+ */
+ Pair<Integer, Long> getEarliestStopTypeAndTime() {
+ int fgsType = -1;
+ long timeout = 0;
+ if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
+ fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING;
+ timeout = ams.mConstants.mMediaProcessingFgsTimeoutDuration;
+ }
+ if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
+ // update the timeout and type if this type has a more restrictive time limit
+ if (timeout == 0 || ams.mConstants.mDataSyncFgsTimeoutDuration < timeout) {
+ fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
+ timeout = ams.mConstants.mDataSyncFgsTimeoutDuration;
+ }
+ }
+ // Add the logic for time limits introduced in the future for other fgs types here.
+ return Pair.create(fgsType, timeout == 0 ? 0 : (mFgsEnterTime + timeout));
+ }
+
+ /**
+ * Check if the given types contain a type which is time restricted.
+ */
+ boolean canFgsTypeTimeOut(int fgsType) {
+ // The below conditionals are not simplified on purpose to help with readability.
+ if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
+ return true;
+ }
+ if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+ == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
+ return true;
+ }
+ // Additional types which have time limits should be added here in the future.
+ return false;
+ }
+
private boolean isAppAlive() {
if (app == null) {
return false;
diff --git a/services/core/java/com/android/server/am/TEST_MAPPING b/services/core/java/com/android/server/am/TEST_MAPPING
index bd3c8e0..feab2c05 100644
--- a/services/core/java/com/android/server/am/TEST_MAPPING
+++ b/services/core/java/com/android/server/am/TEST_MAPPING
@@ -157,7 +157,6 @@
]
},
{
- "file_patterns": ["Broadcast.*"],
"name": "CtsContentTestCases",
"options": [
{ "include-filter": "android.content.cts.BroadcastReceiverTest" },
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 96c6be8..55ac4cf 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -1350,41 +1350,57 @@
}
/**
- * For mDelayUserDataLocking mode, storage once unlocked is kept unlocked.
- * Total number of unlocked user storage is limited by mMaxRunningUsers.
- * If there are more unlocked users, evict and lock the least recently stopped user and
- * lock that user's data. Regardless of the mode, ephemeral user is always locked
- * immediately.
+ * Returns which user, if any, should be locked when the given user is stopped.
+ *
+ * For typical (non-mDelayUserDataLocking) devices and users, this will be the provided user.
+ *
+ * However, for some devices or users (based on {@link #canDelayDataLockingForUser(int)}),
+ * storage once unlocked is kept unlocked, even after the user is stopped, so the user to be
+ * locked (if any) may differ.
+ *
+ * For mDelayUserDataLocking devices, the total number of unlocked user storage is limited
+ * (currently by mMaxRunningUsers). If there are more unlocked users, evict and lock the least
+ * recently stopped user and lock that user's data.
+ *
+ * Regardless of the mode, ephemeral user is always locked immediately.
*
* @return user id to lock. UserHandler.USER_NULL will be returned if no user should be locked.
*/
@GuardedBy("mLock")
private int updateUserToLockLU(@UserIdInt int userId, boolean allowDelayedLocking) {
- int userIdToLock = userId;
- // TODO: Decouple the delayed locking flows from mMaxRunningUsers or rename the property to
- // state maximum running unlocked users specifically
- if (canDelayDataLockingForUser(userIdToLock) && allowDelayedLocking
- && !getUserInfo(userId).isEphemeral()
- && !hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, userId)) {
+ if (!canDelayDataLockingForUser(userId)
+ || !allowDelayedLocking
+ || getUserInfo(userId).isEphemeral()
+ || hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, userId)) {
+ return userId;
+ }
+
+ // Once we reach here, we are in a delayed locking scenario.
+ // Now, no user will be locked, unless the device's policy dictates we should based on the
+ // maximum of such users allowed for the device.
+ if (mDelayUserDataLocking) {
// arg should be object, not index
mLastActiveUsersForDelayedLocking.remove((Integer) userId);
mLastActiveUsersForDelayedLocking.add(0, userId);
int totalUnlockedUsers = mStartedUsers.size()
+ mLastActiveUsersForDelayedLocking.size();
+ // TODO: Decouple the delayed locking flows from mMaxRunningUsers. These users aren't
+ // running so this calculation shouldn't be based on this parameter. Also note that
+ // that if these devices ever support background running users (such as profiles), the
+ // implementation is incorrect since starting such users can cause the max to be
+ // exceeded.
if (totalUnlockedUsers > mMaxRunningUsers) { // should lock a user
- userIdToLock = mLastActiveUsersForDelayedLocking.get(
+ final int userIdToLock = mLastActiveUsersForDelayedLocking.get(
mLastActiveUsersForDelayedLocking.size() - 1);
mLastActiveUsersForDelayedLocking
.remove(mLastActiveUsersForDelayedLocking.size() - 1);
- Slogf.i(TAG, "finishUserStopped, stopping user:" + userId
- + " lock user:" + userIdToLock);
- } else {
- Slogf.i(TAG, "finishUserStopped, user:" + userId + ", skip locking");
- // do not lock
- userIdToLock = UserHandle.USER_NULL;
+ Slogf.i(TAG, "finishUserStopped: should stop user " + userId
+ + " but should lock user " + userIdToLock);
+ return userIdToLock;
}
}
- return userIdToLock;
+ Slogf.i(TAG, "finishUserStopped: should stop user " + userId + " but without any locking");
+ return UserHandle.USER_NULL;
}
/**
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index aa21248..53255a0 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -42,7 +42,6 @@
import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
import static com.android.media.audio.Flags.alarmMinVolumeZero;
-import static com.android.media.audio.Flags.bluetoothMacAddressAnonymization;
import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
@@ -4522,8 +4521,6 @@
+ autoPublicVolumeApiHardening());
pw.println("\tandroid.media.audio.focusFreezeTestApi:"
+ focusFreezeTestApi());
- pw.println("\tcom.android.media.audio.bluetoothMacAddressAnonymization:"
- + bluetoothMacAddressAnonymization());
pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
+ disablePrescaleAbsoluteVolume());
pw.println("\tandroid.media.audiopolicy.enableFadeManagerConfiguration:"
@@ -10650,9 +10647,6 @@
}
private boolean isBluetoothPrividged() {
- if (!bluetoothMacAddressAnonymization()) {
- return true;
- }
return PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
android.Manifest.permission.BLUETOOTH_CONNECT)
|| Binder.getCallingUid() == Process.SYSTEM_UID;
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index 3417f65..3b1c011 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -259,6 +259,7 @@
/** used for VOL_ADJUST_VOL_UID,
* VOL_ADJUST_SUGG_VOL,
* VOL_ADJUST_STREAM_VOL,
+ * VOL_SET_LE_AUDIO_VOL
*/
VolumeEvent(int op, int stream, int val1, int val2, String caller) {
mOp = op;
@@ -434,6 +435,8 @@
.set(MediaMetrics.Property.EVENT, "setLeAudioVolume")
.set(MediaMetrics.Property.INDEX, mVal1)
.set(MediaMetrics.Property.MAX_INDEX, mVal2)
+ .set(MediaMetrics.Property.STREAM_TYPE,
+ AudioSystem.streamToString(mStream))
.record();
return;
case VOL_SET_AVRCP_VOL:
@@ -519,7 +522,8 @@
.append(" gain dB:").append(mVal2)
.toString();
case VOL_SET_LE_AUDIO_VOL:
- return new StringBuilder("setLeAudioVolume:")
+ return new StringBuilder("setLeAudioVolume(stream:")
+ .append(AudioSystem.streamToString(mStream))
.append(" index:").append(mVal1)
.append(" maxIndex:").append(mVal2)
.toString();
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index f51043d..0f3f807 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -470,7 +470,8 @@
+ index + " volume=" + volume);
}
AudioService.sVolumeLogger.enqueue(new AudioServiceEvents.VolumeEvent(
- AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, index, maxIndex));
+ AudioServiceEvents.VolumeEvent.VOL_SET_LE_AUDIO_VOL, streamType, index,
+ maxIndex, /*caller=*/null));
try {
mLeAudio.setVolume(volume);
} catch (Exception e) {
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index cb15abc..cd064ae 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -19,11 +19,11 @@
import static android.Manifest.permission.CONTROL_DEVICE_STATE;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
-import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_BASE_STATE;
import static com.android.server.devicestate.OverrideRequest.OVERRIDE_REQUEST_TYPE_EMULATED_STATE;
import static com.android.server.devicestate.OverrideRequestController.FLAG_POWER_SAVE_ENABLED;
@@ -40,6 +40,7 @@
import android.app.ActivityManagerInternal;
import android.app.IProcessObserver;
import android.content.Context;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateInfo;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateManagerInternal;
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
index 8c6068d..02c9bb3 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerShellCommand.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateManager;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.Binder;
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index d5945f4..65b393a 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -21,6 +21,7 @@
import android.annotation.IntDef;
import android.annotation.IntRange;
+import android.hardware.devicestate.DeviceState;
import android.util.Dumpable;
import java.lang.annotation.Retention;
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequest.java b/services/core/java/com/android/server/devicestate/OverrideRequest.java
index 20485c1..d92629f 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequest.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequest.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.IBinder;
diff --git a/services/core/java/com/android/server/devicestate/OverrideRequestController.java b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
index f5f2fa8..6c3fd83d 100644
--- a/services/core/java/com/android/server/devicestate/OverrideRequestController.java
+++ b/services/core/java/com/android/server/devicestate/OverrideRequestController.java
@@ -18,6 +18,7 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.IBinder;
import android.util.Slog;
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java
new file mode 100644
index 0000000..f218516
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeMap.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.AnyThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.view.inputmethod.InputMethodSubtype;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An on-memory immutable data representation of subtype.xml, which contains so-called additional
+ * {@link InputMethodSubtype}.
+ *
+ * <p>While the data structure could be also used for general purpose map from IME ID to
+ * a list of {@link InputMethodSubtype}, unlike {@link InputMethodMap} this particular data
+ * structure is currently used only around additional {@link InputMethodSubtype}, which is why this
+ * class is (still) called {@code AdditionalSubtypeMap} rather than {@code InputMethodSubtypeMap}.
+ * </p>
+ */
+final class AdditionalSubtypeMap {
+ /**
+ * An empty {@link AdditionalSubtypeMap}.
+ */
+ static final AdditionalSubtypeMap EMPTY_MAP = new AdditionalSubtypeMap(new ArrayMap<>());
+
+ @NonNull
+ private final ArrayMap<String, List<InputMethodSubtype>> mMap;
+
+ @AnyThread
+ @NonNull
+ private static AdditionalSubtypeMap createOrEmpty(
+ @NonNull ArrayMap<String, List<InputMethodSubtype>> map) {
+ return map.isEmpty() ? EMPTY_MAP : new AdditionalSubtypeMap(map);
+ }
+
+ /**
+ * Create a new instance from the given {@link ArrayMap}.
+ *
+ * <p>This method effectively creates a new copy of map.</p>
+ *
+ * @param map An {@link ArrayMap} from which {@link AdditionalSubtypeMap} is to be created.
+ * @return A {@link AdditionalSubtypeMap} that contains a new copy of {@code map}.
+ */
+ @AnyThread
+ @NonNull
+ static AdditionalSubtypeMap of(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) {
+ return createOrEmpty(map);
+ }
+
+ /**
+ * Create a new instance of {@link AdditionalSubtypeMap} from an existing
+ * {@link AdditionalSubtypeMap} by removing {@code key}, or return {@code map} itself if it does
+ * not contain an entry of {@code key}.
+ *
+ * @param key The key to be removed from {@code map}.
+ * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain
+ * {@code key}, or {@code map} itself if it does not contain an entry of {@code key}.
+ */
+ @AnyThread
+ @NonNull
+ AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull String key) {
+ if (isEmpty() || !containsKey(key)) {
+ return this;
+ }
+ final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap);
+ newMap.remove(key);
+ return createOrEmpty(newMap);
+ }
+
+ /**
+ * Create a new instance of {@link AdditionalSubtypeMap} from an existing
+ * {@link AdditionalSubtypeMap} by removing {@code keys} or return {@code map} itself if it does
+ * not contain any entry for {@code keys}.
+ *
+ * @param keys Keys to be removed from {@code map}.
+ * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to not contain
+ * {@code keys}, or {@code map} itself if it does not contain any entry of {@code keys}.
+ */
+ @AnyThread
+ @NonNull
+ AdditionalSubtypeMap cloneWithRemoveOrSelf(@NonNull Collection<String> keys) {
+ if (isEmpty()) {
+ return this;
+ }
+ final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap);
+ return newMap.removeAll(keys) ? createOrEmpty(newMap) : this;
+ }
+
+ /**
+ * Create a new instance of {@link AdditionalSubtypeMap} from an existing
+ * {@link AdditionalSubtypeMap} by putting {@code key} and {@code value}.
+ *
+ * @param key Key to be put into {@code map}.
+ * @param value Value to be put into {@code map}.
+ * @return A new instance of {@link AdditionalSubtypeMap}, which is guaranteed to contain the
+ * pair of {@code key} and {@code value}.
+ */
+ @AnyThread
+ @NonNull
+ AdditionalSubtypeMap cloneWithPut(
+ @Nullable String key, @NonNull List<InputMethodSubtype> value) {
+ final ArrayMap<String, List<InputMethodSubtype>> newMap = new ArrayMap<>(mMap);
+ newMap.put(key, value);
+ return new AdditionalSubtypeMap(newMap);
+ }
+
+ private AdditionalSubtypeMap(@NonNull ArrayMap<String, List<InputMethodSubtype>> map) {
+ mMap = map;
+ }
+
+ @AnyThread
+ @Nullable
+ List<InputMethodSubtype> get(@Nullable String key) {
+ return mMap.get(key);
+ }
+
+ @AnyThread
+ boolean containsKey(@Nullable String key) {
+ return mMap.containsKey(key);
+ }
+
+ @AnyThread
+ boolean isEmpty() {
+ return mMap.isEmpty();
+ }
+
+ @AnyThread
+ @NonNull
+ Collection<String> keySet() {
+ return mMap.keySet();
+ }
+
+ @AnyThread
+ int size() {
+ return mMap.size();
+ }
+}
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
index fba71fd..146ce17 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
@@ -108,12 +108,12 @@
* multiple threads are not calling this method at the same time for the same {@code userId}.
* </p>
*
- * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. Passing an empty
- * map deletes the file.
+ * @param allSubtypes {@link AdditionalSubtypeMap} from IME ID to additional subtype list.
+ * Passing an empty map deletes the file.
* @param methodMap {@link ArrayMap} from IME ID to {@link InputMethodInfo}.
* @param userId The user ID to be associated with.
*/
- static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
+ static void save(AdditionalSubtypeMap allSubtypes,
InputMethodMap methodMap, @UserIdInt int userId) {
final File inputMethodDir = getInputMethodDir(userId);
@@ -142,7 +142,7 @@
}
@VisibleForTesting
- static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
+ static void saveToFile(AdditionalSubtypeMap allSubtypes,
InputMethodMap methodMap, AtomicFile subtypesFile) {
// Safety net for the case that this function is called before methodMap is set.
final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
@@ -212,24 +212,21 @@
* multiple threads are not calling this method at the same time for the same {@code userId}.
* </p>
*
- * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. This parameter
- * will be used to return the result.
- * @param userId The user ID to be associated with.
+ * @param userId The user ID to be associated with.
+ * @return {@link AdditionalSubtypeMap} that contains the additional {@link InputMethodSubtype}.
*/
- static void load(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
- @UserIdInt int userId) {
- allSubtypes.clear();
-
+ static AdditionalSubtypeMap load(@UserIdInt int userId) {
final AtomicFile subtypesFile = getAdditionalSubtypeFile(getInputMethodDir(userId));
// Not having the file means there is no additional subtype.
if (subtypesFile.exists()) {
- loadFromFile(allSubtypes, subtypesFile);
+ return loadFromFile(subtypesFile);
}
+ return AdditionalSubtypeMap.EMPTY_MAP;
}
@VisibleForTesting
- static void loadFromFile(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
- AtomicFile subtypesFile) {
+ static AdditionalSubtypeMap loadFromFile(AtomicFile subtypesFile) {
+ final ArrayMap<String, List<InputMethodSubtype>> allSubtypes = new ArrayMap<>();
try (FileInputStream fis = subtypesFile.openRead()) {
final TypedXmlPullParser parser = Xml.resolvePullParser(fis);
int type = parser.next();
@@ -310,5 +307,6 @@
} catch (XmlPullParserException | IOException | NumberFormatException e) {
Slog.w(TAG, "Error reading subtypes", e);
}
+ return AdditionalSubtypeMap.of(allSubtypes);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index ffdc934..96c0c8a 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -286,8 +286,10 @@
final InputManagerInternal mInputManagerInternal;
final ImePlatformCompatUtils mImePlatformCompatUtils;
final InputMethodDeviceConfigs mInputMethodDeviceConfigs;
- private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap =
- new ArrayMap<>();
+
+ @GuardedBy("ImfLock.class")
+ @NonNull
+ private AdditionalSubtypeMap mAdditionalSubtypeMap = AdditionalSubtypeMap.EMPTY_MAP;
private final UserManagerInternal mUserManagerInternal;
private final InputMethodMenuController mMenuController;
@NonNull private final InputMethodBindingController mBindingController;
@@ -1332,16 +1334,21 @@
@Override
public void onPackageDataCleared(String packageName, int uid) {
synchronized (ImfLock.class) {
- boolean changed = false;
+ // Note that one package may implement multiple IMEs.
+ final ArrayList<String> changedImes = new ArrayList<>();
for (InputMethodInfo imi : mSettings.getMethodList()) {
if (imi.getPackageName().equals(packageName)) {
- mAdditionalSubtypeMap.remove(imi.getId());
- changed = true;
+ changedImes.add(imi.getId());
}
}
- if (changed) {
+ final AdditionalSubtypeMap newMap =
+ mAdditionalSubtypeMap.cloneWithRemoveOrSelf(changedImes);
+ if (newMap != mAdditionalSubtypeMap) {
+ mAdditionalSubtypeMap = newMap;
AdditionalSubtypeUtils.save(
mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId());
+ }
+ if (!changedImes.isEmpty()) {
mChangedPackages.add(packageName);
}
}
@@ -1413,7 +1420,8 @@
Slog.i(TAG,
"Input method reinstalling, clearing additional subtypes: "
+ imi.getComponent());
- mAdditionalSubtypeMap.remove(imi.getId());
+ mAdditionalSubtypeMap =
+ mAdditionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId());
AdditionalSubtypeUtils.save(mAdditionalSubtypeMap,
mSettings.getMethodMap(), mSettings.getUserId());
}
@@ -1648,7 +1656,7 @@
// mSettings should be created before buildInputMethodListLocked
mSettings = InputMethodSettings.createEmptyMap(userId);
- AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
+ mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(userId);
mSwitchingController =
InputMethodSubtypeSwitchingController.createInstanceLocked(context,
mSettings.getMethodMap(), userId);
@@ -1783,7 +1791,7 @@
mSettings = InputMethodSettings.createEmptyMap(newUserId);
// Additional subtypes should be reset when the user is changed
- AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId);
+ mAdditionalSubtypeMap = AdditionalSubtypeUtils.load(newUserId);
final String defaultImiId = mSettings.getSelectedInputMethod();
if (DEBUG) {
@@ -2016,9 +2024,7 @@
&& directBootAwareness == DirectBootAwareness.AUTO) {
settings = mSettings;
} else {
- final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
- new ArrayMap<>();
- AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId);
settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
directBootAwareness);
}
@@ -3554,8 +3560,7 @@
ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME);
mCurStatsToken = null;
- if (!Flags.useHandwritingListenerForTooltype()
- && lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
+ if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) {
curMethod.updateEditorToolType(lastClickToolType);
}
mVisibilityApplier.performShowIme(windowToken, statsToken,
@@ -4218,10 +4223,15 @@
}
if (mSettings.getUserId() == userId) {
- if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded,
- mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) {
+ final var newAdditionalSubtypeMap = mSettings.getNewAdditionalSubtypeMap(
+ imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal,
+ callingUid);
+ if (mAdditionalSubtypeMap == newAdditionalSubtypeMap) {
return;
}
+ AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, mSettings.getMethodMap(),
+ mSettings.getUserId());
+ mAdditionalSubtypeMap = newAdditionalSubtypeMap;
final long ident = Binder.clearCallingIdentity();
try {
buildInputMethodListLocked(false /* resetDefaultEnabledIme */);
@@ -4231,13 +4241,15 @@
return;
}
- final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
- new ArrayMap<>();
- AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId);
final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId,
additionalSubtypeMap, DirectBootAwareness.AUTO);
- settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
- mPackageManagerInternal, callingUid);
+ final var newAdditionalSubtypeMap = settings.getNewAdditionalSubtypeMap(
+ imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid);
+ if (additionalSubtypeMap != newAdditionalSubtypeMap) {
+ AdditionalSubtypeUtils.save(newAdditionalSubtypeMap, settings.getMethodMap(),
+ settings.getUserId());
+ }
}
}
@@ -5072,7 +5084,7 @@
@NonNull
static InputMethodSettings queryInputMethodServicesInternal(Context context,
- @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
+ @UserIdInt int userId, @NonNull AdditionalSubtypeMap additionalSubtypeMap,
@DirectBootAwareness int directBootAwareness) {
final Context userAwareContext = context.getUserId() == userId
? context
@@ -5112,7 +5124,7 @@
@NonNull
static InputMethodMap filterInputMethodServices(
- ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
+ @NonNull AdditionalSubtypeMap additionalSubtypeMap,
List<String> enabledInputMethodList, Context userAwareContext,
List<ResolveInfo> services) {
final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>();
@@ -5512,9 +5524,7 @@
if (userId == mSettings.getUserId()) {
settings = mSettings;
} else {
- final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
- new ArrayMap<>();
- AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId);
settings = queryInputMethodServicesInternal(mContext, userId,
additionalSubtypeMap, DirectBootAwareness.AUTO);
}
@@ -5522,9 +5532,7 @@
}
private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) {
- final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
- new ArrayMap<>();
- AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ final AdditionalSubtypeMap additionalSubtypeMap = AdditionalSubtypeUtils.load(userId);
return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
DirectBootAwareness.AUTO);
}
@@ -6572,9 +6580,8 @@
nextIme = mSettings.getSelectedInputMethod();
nextEnabledImes = mSettings.getEnabledInputMethodList();
} else {
- final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
- new ArrayMap<>();
- AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ final AdditionalSubtypeMap additionalSubtypeMap =
+ AdditionalSubtypeUtils.load(userId);
final InputMethodSettings settings = queryInputMethodServicesInternal(
mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO);
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
index e444db1..a558838 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
@@ -24,7 +24,6 @@
import android.os.LocaleList;
import android.provider.Settings;
import android.text.TextUtils;
-import android.util.ArrayMap;
import android.util.IntArray;
import android.util.Pair;
import android.util.Printer;
@@ -614,26 +613,27 @@
explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
}
- boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
+ @NonNull
+ AdditionalSubtypeMap getNewAdditionalSubtypeMap(@NonNull String imeId,
@NonNull ArrayList<InputMethodSubtype> subtypes,
- @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
+ @NonNull AdditionalSubtypeMap additionalSubtypeMap,
@NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
final InputMethodInfo imi = mMethodMap.get(imeId);
if (imi == null) {
- return false;
+ return additionalSubtypeMap;
}
if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
imi.getPackageName())) {
- return false;
+ return additionalSubtypeMap;
}
+ final AdditionalSubtypeMap newMap;
if (subtypes.isEmpty()) {
- additionalSubtypeMap.remove(imi.getId());
+ newMap = additionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId());
} else {
- additionalSubtypeMap.put(imi.getId(), subtypes);
+ newMap = additionalSubtypeMap.cloneWithPut(imi.getId(), subtypes);
}
- AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getUserId());
- return true;
+ return newMap;
}
boolean setEnabledInputMethodSubtypes(@NonNull String imeId,
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
index 5c1897d..fc99471 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientBroker.java
@@ -25,12 +25,15 @@
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.PendingIntent;
+import android.chre.flags.Flags;
import android.compat.Compatibility;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.Context;
import android.content.Intent;
+import android.hardware.contexthub.ErrorCode;
import android.hardware.contexthub.HostEndpointInfo;
+import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.location.ContextHubInfo;
import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
@@ -65,6 +68,7 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
import java.util.function.Supplier;
/**
@@ -441,9 +445,35 @@
@ContextHubTransaction.Result
@Override
public int sendMessageToNanoApp(NanoAppMessage message) {
+ return doSendMessageToNanoApp(message, /* transactionCallback= */ null);
+ }
+
+ /**
+ * Sends a reliable message rom this client to a nanoapp.
+ *
+ * @param message the message to send
+ * @param transactionCallback The callback to use to confirm the delivery of the message for
+ * reliable messages.
+ * @return the error code of sending the message
+ * @throws SecurityException if this client doesn't have permissions to send a message to the
+ * nanoapp
+ */
+ @ContextHubTransaction.Result
+ @Override
+ public int sendReliableMessageToNanoApp(NanoAppMessage message,
+ IContextHubTransactionCallback transactionCallback) {
+ return doSendMessageToNanoApp(message, transactionCallback);
+ }
+
+ /**
+ * See sendReliableMessageToNanoApp().
+ */
+ @ContextHubTransaction.Result
+ private int doSendMessageToNanoApp(NanoAppMessage message,
+ @Nullable IContextHubTransactionCallback transactionCallback) {
ContextHubServiceUtil.checkPermissions(mContext);
- int result;
+ @ContextHubTransaction.Result int result;
if (isRegistered()) {
int authState = mMessageChannelNanoappIdMap.getOrDefault(
message.getNanoAppId(), AUTHORIZATION_UNKNOWN);
@@ -462,13 +492,30 @@
checkNanoappPermsAsync();
}
- try {
- result = mContextHubProxy.sendMessageToContextHub(
- mHostEndPointId, mAttachedContextHubInfo.getId(), message);
- } catch (RemoteException e) {
- Log.e(TAG, "RemoteException in sendMessageToNanoApp (target hub ID = "
- + mAttachedContextHubInfo.getId() + ")", e);
- result = ContextHubTransaction.RESULT_FAILED_UNKNOWN;
+ if (!Flags.reliableMessageImplementation() || transactionCallback == null) {
+ try {
+ result = mContextHubProxy.sendMessageToContextHub(mHostEndPointId,
+ mAttachedContextHubInfo.getId(), message);
+ } catch (RemoteException e) {
+ Log.e(TAG,
+ "RemoteException in sendMessageToNanoApp (target hub ID = "
+ + mAttachedContextHubInfo.getId() + ")",
+ e);
+ result = ContextHubTransaction.RESULT_FAILED_UNKNOWN;
+ }
+ } else {
+ result = ContextHubTransaction.RESULT_SUCCESS;
+ ContextHubServiceTransaction transaction =
+ mTransactionManager.createMessageTransaction(mHostEndPointId,
+ mAttachedContextHubInfo.getId(), message, transactionCallback,
+ getPackageName());
+ try {
+ mTransactionManager.addTransaction(transaction);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Unable to add a transaction in sendMessageToNanoApp "
+ + "(target hub ID = " + mAttachedContextHubInfo.getId() + ")", e);
+ result = ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE;
+ }
}
ContextHubEventLogger.getInstance().logMessageToNanoapp(
@@ -569,15 +616,17 @@
}
/**
- * Sends a message to the client associated with this object.
+ * Sends a message to the client associated with this object. This function will call
+ * onFinishedCallback when the operation is complete if the message is reliable.
*
* @param message the message that came from a nanoapp
* @param nanoappPermissions permissions required to communicate with the nanoapp sending this
* message
* @param messagePermissions permissions required to consume the message being delivered. These
* permissions are what will be attributed to the client through noteOp.
+ * @return An error from ErrorCode
*/
- void sendMessageToClient(
+ byte sendMessageToClient(
NanoAppMessage message,
List<String> nanoappPermissions,
List<String> messagePermissions) {
@@ -592,7 +641,7 @@
if (authState == AUTHORIZATION_DENIED_GRACE_PERIOD && !messagePermissions.isEmpty()) {
Log.e(TAG, "Dropping message from " + Long.toHexString(nanoAppId) + ". " + mPackage
+ " in grace period and napp msg has permissions");
- return;
+ return ErrorCode.PERMISSION_DENIED;
}
// If in the grace period, don't check permissions state since it'll cause cleanup
@@ -601,15 +650,23 @@
|| !notePermissions(messagePermissions, RECEIVE_MSG_NOTE + nanoAppId)) {
Log.e(TAG, "Dropping message from " + Long.toHexString(nanoAppId) + ". " + mPackage
+ " doesn't have permission");
- return;
+ return ErrorCode.PERMISSION_DENIED;
}
- invokeCallback(callback -> callback.onMessageFromNanoApp(message));
+ byte errorCode = invokeCallback(callback -> callback.onMessageFromNanoApp(message));
+ if (errorCode != ErrorCode.OK) {
+ return errorCode;
+ }
Supplier<Intent> supplier =
() -> createIntent(ContextHubManager.EVENT_NANOAPP_MESSAGE, nanoAppId)
.putExtra(ContextHubManager.EXTRA_MESSAGE, message);
- sendPendingIntent(supplier, nanoAppId);
+ Consumer<Byte> onFinishedCallback = (Byte error) ->
+ sendMessageDeliveryStatusToContextHub(message.getMessageSequenceNumber(), error);
+ return sendPendingIntent(supplier, nanoAppId,
+ Flags.reliableMessageImplementation() && message.isReliable()
+ ? onFinishedCallback
+ : null);
}
/**
@@ -873,8 +930,9 @@
* Helper function to invoke a specified client callback, if the connection is open.
*
* @param consumer the consumer specifying the callback to invoke
+ * @return the ErrorCode for this operation
*/
- private synchronized void invokeCallback(CallbackConsumer consumer) {
+ private synchronized byte invokeCallback(CallbackConsumer consumer) {
if (mContextHubClientCallback != null) {
try {
acquireWakeLock();
@@ -886,8 +944,10 @@
+ mHostEndPointId
+ ")",
e);
+ return ErrorCode.PERMANENT_ERROR;
}
}
+ return ErrorCode.OK;
}
/**
@@ -918,37 +978,81 @@
}
/**
- * Sends an intent to any existing PendingIntent
+ * Sends an intent to any existing PendingIntent.
*
- * @param supplier method to create the extra Intent
+ * @param supplier method to create the extra Intent.
+ * @return the ErrorCode indicating the status of sending the intent.
+ * ErrorCode.TRANSIENT_ERROR indicates there is no intent.
*/
- private synchronized void sendPendingIntent(Supplier<Intent> supplier) {
+ private synchronized byte sendPendingIntent(Supplier<Intent> supplier) {
if (mPendingIntentRequest.hasPendingIntent()) {
- doSendPendingIntent(mPendingIntentRequest.getPendingIntent(), supplier.get(), this);
+ return doSendPendingIntent(mPendingIntentRequest.getPendingIntent(), supplier.get(),
+ this);
}
+ return ErrorCode.OK;
}
/**
- * Sends an intent to any existing PendingIntent
+ * Sends an intent to any existing PendingIntent.
*
- * @param supplier method to create the extra Intent
- * @param nanoAppId the ID of the nanoapp which this event is for
+ * @param supplier method to create the extra Intent.
+ * @param nanoAppId the ID of the nanoapp which this event is for.
+ * @return the ErrorCode indicating the status of sending the intent.
+ * ErrorCode.TRANSIENT_ERROR indicates there is no intent.
*/
- private synchronized void sendPendingIntent(Supplier<Intent> supplier, long nanoAppId) {
+ private synchronized byte sendPendingIntent(Supplier<Intent> supplier, long nanoAppId) {
+ return sendPendingIntent(supplier, nanoAppId, null);
+ }
+
+ /**
+ * Sends an intent to any existing PendingIntent. This function will set the onFinishedCallback
+ * to be called when the pending intent is sent or upon a failure.
+ *
+ * @param supplier method to create the extra Intent.
+ * @param nanoAppId the ID of the nanoapp which this event is for.
+ * @param onFinishedCallback the callback called when the operation is finished.
+ * @return the ErrorCode indicating the status of sending the intent.
+ * ErrorCode.TRANSIENT_ERROR indicates there is no intent.
+ */
+ private synchronized byte sendPendingIntent(Supplier<Intent> supplier, long nanoAppId,
+ Consumer<Byte> onFinishedCallback) {
if (mPendingIntentRequest.hasPendingIntent()
&& mPendingIntentRequest.getNanoAppId() == nanoAppId) {
- doSendPendingIntent(mPendingIntentRequest.getPendingIntent(), supplier.get(), this);
+ ContextHubClientBroker broker = this;
+ PendingIntent.OnFinished onFinished = new PendingIntent.OnFinished() {
+ @Override
+ public void onSendFinished(
+ PendingIntent pendingIntent,
+ Intent intent,
+ int resultCode,
+ String resultData,
+ Bundle resultExtras) {
+ if (onFinishedCallback != null) {
+ onFinishedCallback.accept(resultCode == 0
+ ? ErrorCode.OK
+ : ErrorCode.TRANSIENT_ERROR);
+ }
+
+ broker.onSendFinished(pendingIntent, intent, resultCode, resultData,
+ resultExtras);
+ }
+ };
+
+ return doSendPendingIntent(mPendingIntentRequest.getPendingIntent(), supplier.get(),
+ onFinished);
}
+ return ErrorCode.OK;
}
/**
- * Sends a PendingIntent with extra Intent data
+ * Sends a PendingIntent with extra Intent data.
*
- * @param pendingIntent the PendingIntent
- * @param intent the extra Intent data
+ * @param pendingIntent the PendingIntent.
+ * @param intent the extra Intent data.
+ * @return the ErrorCode indicating the status of sending the intent.
*/
@VisibleForTesting
- void doSendPendingIntent(
+ byte doSendPendingIntent(
PendingIntent pendingIntent,
Intent intent,
PendingIntent.OnFinished onFinishedCallback) {
@@ -963,6 +1067,7 @@
/* handler= */ null,
requiredPermission,
/* options= */ null);
+ return ErrorCode.OK;
} catch (PendingIntent.CanceledException e) {
mIsPendingIntentCancelled.set(true);
// The PendingIntent is no longer valid
@@ -973,6 +1078,7 @@
+ mHostEndPointId
+ ")");
close();
+ return ErrorCode.PERMANENT_ERROR;
}
}
@@ -1089,6 +1195,16 @@
releaseWakeLock();
}
+ /**
+ * Callback that arrives when direct-call message callback delivery completed.
+ * Used for reliable messages.
+ */
+ @Override
+ public void reliableMessageCallbackFinished(int messageSequenceNumber, byte errorCode) {
+ sendMessageDeliveryStatusToContextHub(messageSequenceNumber, errorCode);
+ callbackFinished();
+ }
+
@Override
public void onSendFinished(
PendingIntent pendingIntent,
@@ -1148,4 +1264,18 @@
}
});
}
+
+ private void sendMessageDeliveryStatusToContextHub(int messageSequenceNumber, byte errorCode) {
+ if (!Flags.reliableMessageImplementation()) {
+ return;
+ }
+
+ MessageDeliveryStatus status = new MessageDeliveryStatus();
+ status.messageSequenceNumber = messageSequenceNumber;
+ status.errorCode = errorCode;
+ if (mContextHubProxy.sendMessageDeliveryStatusToContextHub(mAttachedContextHubInfo.getId(),
+ status) != ContextHubTransaction.RESULT_SUCCESS) {
+ Log.e(TAG, "Failed to send the reliable message status");
+ }
+ }
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java
index 4de7c0c..4636a49 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java
@@ -18,7 +18,9 @@
import android.annotation.IntDef;
import android.app.PendingIntent;
+import android.chre.flags.Flags;
import android.content.Context;
+import android.hardware.contexthub.ErrorCode;
import android.hardware.location.ContextHubInfo;
import android.hardware.location.IContextHubClient;
import android.hardware.location.IContextHubClientCallback;
@@ -218,21 +220,27 @@
/**
* Handles a message sent from a nanoapp.
*
- * @param contextHubId the ID of the hub where the nanoapp sent the message from
+ * @param contextHubId the ID of the hub where the nanoapp sent the message from.
* @param hostEndpointId The host endpoint ID of the client that this message is for.
- * @param message the message send by a nanoapp
- * @param nanoappPermissions the set of permissions the nanoapp holds
+ * @param message the message send by a nanoapp.
+ * @param nanoappPermissions the set of permissions the nanoapp holds.
* @param messagePermissions the set of permissions that should be used for attributing
- * permissions when this message is consumed by a client
+ * permissions when this message is consumed by a client.
+ * @return An error from ErrorCode.
*/
- /* package */ void onMessageFromNanoApp(
- int contextHubId, short hostEndpointId, NanoAppMessage message,
- List<String> nanoappPermissions, List<String> messagePermissions) {
+ /* package */ byte onMessageFromNanoApp(int contextHubId, short hostEndpointId,
+ NanoAppMessage message, List<String> nanoappPermissions,
+ List<String> messagePermissions) {
if (DEBUG_LOG_ENABLED) {
Log.v(TAG, "Received " + message);
}
if (message.isBroadcastMessage()) {
+ if (Flags.reliableMessageImplementation() && message.isReliable()) {
+ Log.e(TAG, "Received reliable broadcast message from " + message.getNanoAppId());
+ return ErrorCode.PERMANENT_ERROR;
+ }
+
// Broadcast messages shouldn't be sent with any permissions tagged per CHRE API
// requirements.
if (!messagePermissions.isEmpty()) {
@@ -240,21 +248,25 @@
+ message.getNanoAppId());
}
- ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message, true);
+ ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message,
+ /* success= */ true);
broadcastMessage(contextHubId, message, nanoappPermissions, messagePermissions);
- } else {
- ContextHubClientBroker proxy = mHostEndPointIdToClientMap.get(hostEndpointId);
- if (proxy != null) {
- ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message,
- true);
- proxy.sendMessageToClient(message, nanoappPermissions, messagePermissions);
- } else {
- ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message,
- false);
- Log.e(TAG, "Cannot send message to unregistered client (host endpoint ID = "
- + hostEndpointId + ")");
- }
+ return ErrorCode.OK;
}
+
+ ContextHubClientBroker proxy = mHostEndPointIdToClientMap.get(hostEndpointId);
+ if (proxy == null) {
+ ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message,
+ /* success= */ false);
+ Log.e(TAG,
+ "Cannot send message to unregistered client (host endpoint ID = "
+ + hostEndpointId + ")");
+ return ErrorCode.DESTINATION_NOT_FOUND;
+ }
+
+ ContextHubEventLogger.getInstance().logMessageFromNanoapp(contextHubId, message,
+ /* success= */ true);
+ return proxy.sendMessageToClient(message, nanoappPermissions, messagePermissions);
}
/**
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
index 08cf3f7..e196dee 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java
@@ -21,6 +21,7 @@
import android.app.ActivityManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
+import android.chre.flags.Flags;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -29,6 +30,8 @@
import android.database.ContentObserver;
import android.hardware.SensorPrivacyManager;
import android.hardware.SensorPrivacyManagerInternal;
+import android.hardware.contexthub.ErrorCode;
+import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.location.ContextHubInfo;
import android.hardware.location.ContextHubMessage;
import android.hardware.location.ContextHubTransaction;
@@ -218,6 +221,11 @@
resetSettings();
Log.i(TAG, "Finished Context Hub Service restart");
}
+
+ @Override
+ public void handleMessageDeliveryStatus(MessageDeliveryStatus messageDeliveryStatus) {
+ handleMessageDeliveryStatusCallback(messageDeliveryStatus);
+ }
}
public ContextHubService(Context context, IContextHubWrapper contextHubWrapper) {
@@ -822,8 +830,8 @@
info.getAppId(), msg.getMsgType(), msg.getData());
IContextHubClient client = mDefaultClientMap.get(contextHubHandle);
- success = (client.sendMessageToNanoApp(message) ==
- ContextHubTransaction.RESULT_SUCCESS);
+ success = client.sendMessageToNanoApp(message)
+ == ContextHubTransaction.RESULT_SUCCESS;
} else {
Log.e(TAG, "Failed to send nanoapp message - nanoapp with handle "
+ nanoAppHandle + " does not exist.");
@@ -841,16 +849,33 @@
* @param message the message contents
* @param nanoappPermissions the set of permissions the nanoapp holds
* @param messagePermissions the set of permissions that should be used for attributing
- * permissions when this message is consumed by a client
+ * permissions when this message is consumed by a client
*/
- private void handleClientMessageCallback(
- int contextHubId,
- short hostEndpointId,
- NanoAppMessage message,
- List<String> nanoappPermissions,
+ private void handleClientMessageCallback(int contextHubId, short hostEndpointId,
+ NanoAppMessage message, List<String> nanoappPermissions,
List<String> messagePermissions) {
- mClientManager.onMessageFromNanoApp(
- contextHubId, hostEndpointId, message, nanoappPermissions, messagePermissions);
+ byte errorCode = mClientManager.onMessageFromNanoApp(contextHubId, hostEndpointId, message,
+ nanoappPermissions, messagePermissions);
+ if (message.isReliable() && errorCode != ErrorCode.OK) {
+ sendMessageDeliveryStatusToContextHub(contextHubId, message.getMessageSequenceNumber(),
+ errorCode);
+ }
+ }
+
+ private void sendMessageDeliveryStatusToContextHub(int contextHubId,
+ int messageSequenceNumber, byte errorCode) {
+ if (!Flags.reliableMessageImplementation()) {
+ return;
+ }
+
+ MessageDeliveryStatus status = new MessageDeliveryStatus();
+ status.messageSequenceNumber = messageSequenceNumber;
+ status.errorCode = errorCode;
+ if (mContextHubWrapper.sendMessageDeliveryStatusToContextHub(contextHubId, status)
+ != ContextHubTransaction.RESULT_SUCCESS) {
+ Log.e(TAG, "Failed to send the reliable message status for message sequence number: "
+ + messageSequenceNumber + " with error code: " + errorCode);
+ }
}
/**
@@ -897,6 +922,16 @@
}
/**
+ * Handles a message deliveyr status from a Context Hub.
+ *
+ * @param messageDeliveryStatus The message delivery status to deliver.
+ */
+ private void handleMessageDeliveryStatusCallback(MessageDeliveryStatus messageDeliveryStatus) {
+ mTransactionManager.onMessageDeliveryResponse(messageDeliveryStatus.messageSequenceNumber,
+ messageDeliveryStatus.errorCode == ErrorCode.OK);
+ }
+
+ /**
* Handles an asynchronous event from a Context Hub.
*
* @param contextHubId the ID of the hub the response came from
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java b/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
index a31aecb..4ee2e99 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubServiceTransaction.java
@@ -32,15 +32,20 @@
@ContextHubTransaction.Type
private final int mTransactionType;
- /* The ID of the nanoapp this transaction is targeted for, null if not applicable. */
+ /** The ID of the nanoapp this transaction is targeted for, null if not applicable. */
private final Long mNanoAppId;
- /*
+ /**
* The host package associated with this transaction.
*/
private final String mPackage;
- /*
+ /**
+ * The message sequence number associated with this transaction, null if not applicable.
+ */
+ private final Integer mMessageSequenceNumber;
+
+ /**
* true if the transaction has already completed, false otherwise
*/
private boolean mIsComplete = false;
@@ -50,6 +55,7 @@
mTransactionType = type;
mNanoAppId = null;
mPackage = packageName;
+ mMessageSequenceNumber = null;
}
/* package */ ContextHubServiceTransaction(int id, int type, long nanoAppId,
@@ -58,6 +64,16 @@
mTransactionType = type;
mNanoAppId = nanoAppId;
mPackage = packageName;
+ mMessageSequenceNumber = null;
+ }
+
+ /* package */ ContextHubServiceTransaction(int id, int type, String packageName,
+ int messageSequenceNumber) {
+ mTransactionId = id;
+ mTransactionType = type;
+ mNanoAppId = null;
+ mPackage = packageName;
+ mMessageSequenceNumber = messageSequenceNumber;
}
/**
@@ -111,6 +127,13 @@
}
/**
+ * @return the message sequence number of this transaction
+ */
+ Integer getMessageSequenceNumber() {
+ return mMessageSequenceNumber;
+ }
+
+ /**
* Gets the timeout period as defined in IContexthub.hal
*
* @return the timeout of this transaction in the specified time unit
@@ -119,6 +142,8 @@
switch (mTransactionType) {
case ContextHubTransaction.TYPE_LOAD_NANOAPP:
return unit.convert(30L, TimeUnit.SECONDS);
+ case ContextHubTransaction.TYPE_RELIABLE_MESSAGE:
+ return unit.convert(1000L, TimeUnit.MILLISECONDS);
case ContextHubTransaction.TYPE_UNLOAD_NANOAPP:
case ContextHubTransaction.TYPE_ENABLE_NANOAPP:
case ContextHubTransaction.TYPE_DISABLE_NANOAPP:
@@ -152,7 +177,11 @@
if (mNanoAppId != null) {
out += "appId = 0x" + Long.toHexString(mNanoAppId) + ", ";
}
- out += "package = " + mPackage + ")";
+ out += "package = " + mPackage;
+ if (mMessageSequenceNumber != null) {
+ out += ", messageSequenceNumber = " + mMessageSequenceNumber;
+ }
+ out += ")";
return out;
}
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java b/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java
index f637149..33d2ff0 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubServiceUtil.java
@@ -276,6 +276,8 @@
aidlMessage.messageBody = message.getMessageBody();
// This explicit definition is required to avoid erroneous behavior at the binder.
aidlMessage.permissions = new String[0];
+ aidlMessage.isReliable = message.isReliable();
+ aidlMessage.messageSequenceNumber = message.getMessageSequenceNumber();
return aidlMessage;
}
@@ -306,7 +308,8 @@
android.hardware.contexthub.ContextHubMessage message) {
return NanoAppMessage.createMessageFromNanoApp(
message.nanoappId, message.messageType, message.messageBody,
- message.hostEndPoint == HOST_ENDPOINT_BROADCAST);
+ message.hostEndPoint == HOST_ENDPOINT_BROADCAST,
+ message.isReliable, message.messageSequenceNumber);
}
/**
diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
index e46b8c0c..b18871c 100644
--- a/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
+++ b/services/core/java/com/android/server/location/contexthub/ContextHubTransactionManager.java
@@ -19,6 +19,7 @@
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.IContextHubTransactionCallback;
import android.hardware.location.NanoAppBinary;
+import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
import android.os.RemoteException;
import android.util.Log;
@@ -75,6 +76,11 @@
*/
private final AtomicInteger mNextAvailableId = new AtomicInteger();
+ /**
+ * The next available message sequence number
+ */
+ private final AtomicInteger mNextAvailableMessageSequenceNumber = new AtomicInteger();
+
/*
* An executor and the future object for scheduling timeout timers
*/
@@ -309,6 +315,47 @@
}
/**
+ * Creates a transaction to send a reliable message.
+ *
+ * @param hostEndpointId The ID of the host endpoint sending the message.
+ * @param contextHubId The ID of the hub to send the message to.
+ * @param message The message to send.
+ * @param transactionCallback The callback of the transactions.
+ * @param packageName The host package associated with this transaction.
+ * @return The generated transaction.
+ */
+ /* package */ ContextHubServiceTransaction createMessageTransaction(
+ short hostEndpointId, int contextHubId, NanoAppMessage message,
+ IContextHubTransactionCallback transactionCallback, String packageName) {
+ return new ContextHubServiceTransaction(mNextAvailableId.getAndIncrement(),
+ ContextHubTransaction.TYPE_RELIABLE_MESSAGE, packageName,
+ mNextAvailableMessageSequenceNumber.getAndIncrement()) {
+ @Override
+ /* package */ int onTransact() {
+ try {
+ message.setIsReliable(/* isReliable= */ true);
+ message.setMessageSequenceNumber(getMessageSequenceNumber());
+
+ return mContextHubProxy.sendMessageToContextHub(hostEndpointId, contextHubId,
+ message);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while trying to send a reliable message", e);
+ return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
+ }
+ }
+
+ @Override
+ /* package */ void onTransactionComplete(@ContextHubTransaction.Result int result) {
+ try {
+ transactionCallback.onTransactionComplete(result);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException while calling client onTransactionComplete", e);
+ }
+ }
+ };
+ }
+
+ /**
* Creates a transaction for querying for a list of nanoapps.
*
* @param contextHubId the ID of the hub to query
@@ -397,6 +444,30 @@
removeTransactionAndStartNext();
}
+ /* package */
+ synchronized void onMessageDeliveryResponse(int messageSequenceNumber, boolean success) {
+ ContextHubServiceTransaction transaction = mTransactionQueue.peek();
+ if (transaction == null) {
+ Log.w(TAG, "Received unexpected transaction response (no transaction pending)");
+ return;
+ }
+
+ Integer transactionMessageSequenceNumber = transaction.getMessageSequenceNumber();
+ if (transaction.getTransactionType() != ContextHubTransaction.TYPE_RELIABLE_MESSAGE
+ || transactionMessageSequenceNumber == null
+ || transactionMessageSequenceNumber != messageSequenceNumber) {
+ Log.w(TAG, "Received unexpected message transaction response (expected message "
+ + "sequence number = "
+ + transaction.getMessageSequenceNumber()
+ + ", received messageSequenceNumber = " + messageSequenceNumber + ")");
+ return;
+ }
+
+ transaction.onTransactionComplete(success ? ContextHubTransaction.RESULT_SUCCESS :
+ ContextHubTransaction.RESULT_FAILED_AT_HUB);
+ removeTransactionAndStartNext();
+ }
+
/**
* Handles a query response from a Context Hub.
*
@@ -481,10 +552,10 @@
}
};
- long timeoutSeconds = transaction.getTimeout(TimeUnit.SECONDS);
+ long timeoutMs = transaction.getTimeout(TimeUnit.MILLISECONDS);
try {
- mTimeoutFuture = mTimeoutExecutor.schedule(onTimeoutFunc, timeoutSeconds,
- TimeUnit.SECONDS);
+ mTimeoutFuture = mTimeoutExecutor.schedule(
+ onTimeoutFunc, timeoutMs, TimeUnit.MILLISECONDS);
} catch (Exception e) {
Log.e(TAG, "Error when schedule a timer", e);
}
diff --git a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
index 9c27c22..552809b 100644
--- a/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
+++ b/services/core/java/com/android/server/location/contexthub/IContextHubWrapper.java
@@ -17,6 +17,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.chre.flags.Flags;
import android.hardware.contexthub.HostEndpointInfo;
import android.hardware.contexthub.MessageDeliveryStatus;
import android.hardware.contexthub.NanSessionRequest;
@@ -88,18 +89,25 @@
/**
* Handles a message from a nanoapp to a ContextHubClient.
*
- * @param hostEndpointId The host endpoint ID of the recipient.
- * @param message The message from the nanoapp.
- * @param nanoappPermissions The list of permissions held by the nanoapp.
- * @param messagePermissions The list of permissions required to receive the message.
+ * @param hostEndpointId The host endpoint ID of the recipient.
+ * @param message The message from the nanoapp.
+ * @param nanoappPermissions The list of permissions held by the nanoapp.
+ * @param messagePermissions The list of permissions required to receive the message.
*/
void handleNanoappMessage(short hostEndpointId, NanoAppMessage message,
List<String> nanoappPermissions, List<String> messagePermissions);
/**
- * Handles a restart of the service
+ * Handles a restart of the service.
*/
void handleServiceRestart();
+
+ /**
+ * Handles a message delivery status.
+ *
+ * @param messageDeliveryStatus The message delivery status to deliver.
+ */
+ void handleMessageDeliveryStatus(MessageDeliveryStatus messageDeliveryStatus);
}
/**
@@ -308,15 +316,25 @@
/**
* Sends a message to the Context Hub.
*
- * @param hostEndpointId The host endpoint ID of the sender.
- * @param contextHubId The ID of the Context Hub to send the message to.
- * @param message The message to send.
+ * @param hostEndpointId The host endpoint ID of the sender.
+ * @param contextHubId The ID of the Context Hub to send the message to.
+ * @param message The message to send.
* @return the result of the message sending.
*/
@ContextHubTransaction.Result
- public abstract int sendMessageToContextHub(
- short hostEndpointId, int contextHubId, NanoAppMessage message)
- throws RemoteException;
+ public abstract int sendMessageToContextHub(short hostEndpointId, int contextHubId,
+ NanoAppMessage message) throws RemoteException;
+
+ /**
+ * Sends a transaction status to the Context Hub.
+ *
+ * @param contextHubId The ID of the context hub to sent the status to.
+ * @param status The status of the transaction.
+ * @return the result of the message sending.
+ */
+ @ContextHubTransaction.Result
+ public abstract int sendMessageDeliveryStatusToContextHub(
+ int contextHubId, MessageDeliveryStatus status);
/**
* Loads a nanoapp on the Context Hub.
@@ -443,8 +461,7 @@
public void handleContextHubMessage(android.hardware.contexthub.ContextHubMessage msg,
String[] msgContentPerms) {
mHandler.post(() -> {
- mCallback.handleNanoappMessage(
- (short) msg.hostEndPoint,
+ mCallback.handleNanoappMessage((short) msg.hostEndPoint,
ContextHubServiceUtil.createNanoAppMessage(msg),
new ArrayList<>(Arrays.asList(msg.permissions)),
new ArrayList<>(Arrays.asList(msgContentPerms)));
@@ -468,9 +485,17 @@
// TODO(271471342): Implement
}
- public void handleMessageDeliveryStatus(char hostEndPointId,
+ public void handleMessageDeliveryStatus(
+ char hostEndpointId,
MessageDeliveryStatus messageDeliveryStatus) {
- // TODO(b/312417087): Implement reliable message support
+ if (Flags.reliableMessageImplementation()) {
+ mHandler.post(() -> {
+ mCallback.handleMessageDeliveryStatus(messageDeliveryStatus);
+ });
+ } else {
+ Log.w(TAG, "handleMessageDeliveryStatus called when the "
+ + "reliableMessageImplementation flag is disabled");
+ }
}
public byte[] getUuid() {
@@ -624,17 +649,35 @@
}
@ContextHubTransaction.Result
- public int sendMessageToContextHub(
- short hostEndpointId, int contextHubId, NanoAppMessage message)
- throws RemoteException {
+ public int sendMessageToContextHub(short hostEndpointId, int contextHubId,
+ NanoAppMessage message) throws RemoteException {
android.hardware.contexthub.IContextHub hub = getHub();
if (hub == null) {
return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
}
try {
- hub.sendMessageToHub(contextHubId,
- ContextHubServiceUtil.createAidlContextHubMessage(hostEndpointId, message));
+ var msg = ContextHubServiceUtil.createAidlContextHubMessage(
+ hostEndpointId, message);
+ hub.sendMessageToHub(contextHubId, msg);
+ return ContextHubTransaction.RESULT_SUCCESS;
+ } catch (RemoteException | ServiceSpecificException e) {
+ return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
+ } catch (IllegalArgumentException e) {
+ return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+ }
+ }
+
+ @ContextHubTransaction.Result
+ public int sendMessageDeliveryStatusToContextHub(int contextHubId,
+ MessageDeliveryStatus status) {
+ android.hardware.contexthub.IContextHub hub = getHub();
+ if (hub == null) {
+ return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+ }
+
+ try {
+ hub.sendMessageDeliveryStatusToHub(contextHubId, status);
return ContextHubTransaction.RESULT_SUCCESS;
} catch (RemoteException | ServiceSpecificException e) {
return ContextHubTransaction.RESULT_FAILED_UNKNOWN;
@@ -847,8 +890,7 @@
@Override
public void handleClientMsg(ContextHubMsg message) {
- mCallback.handleNanoappMessage(
- message.hostEndPoint,
+ mCallback.handleNanoappMessage(message.hostEndPoint,
ContextHubServiceUtil.createNanoAppMessage(message),
Collections.emptyList() /* nanoappPermissions */,
Collections.emptyList() /* messagePermissions */);
@@ -880,8 +922,7 @@
@Override
public void handleClientMsg_1_2(android.hardware.contexthub.V1_2.ContextHubMsg message,
ArrayList<String> messagePermissions) {
- mCallback.handleNanoappMessage(
- message.msg_1_0.hostEndPoint,
+ mCallback.handleNanoappMessage(message.msg_1_0.hostEndPoint,
ContextHubServiceUtil.createNanoAppMessage(message.msg_1_0),
message.permissions, messagePermissions);
}
@@ -899,9 +940,12 @@
}
@ContextHubTransaction.Result
- public int sendMessageToContextHub(
- short hostEndpointId, int contextHubId, NanoAppMessage message)
- throws RemoteException {
+ public int sendMessageToContextHub(short hostEndpointId, int contextHubId,
+ NanoAppMessage message) throws RemoteException {
+ if (message.isReliable()) {
+ Log.e(TAG, "Reliable messages are only supported with the AIDL HAL");
+ return ContextHubTransaction.RESULT_FAILED_BAD_PARAMS;
+ }
ContextHubMsg messageToNanoApp =
ContextHubServiceUtil.createHidlContextHubMessage(hostEndpointId, message);
return ContextHubServiceUtil.toTransactionResult(
@@ -909,6 +953,13 @@
}
@ContextHubTransaction.Result
+ public int sendMessageDeliveryStatusToContextHub(int contextHubId,
+ MessageDeliveryStatus status) {
+ // Only supported on the AIDL implementation.
+ return ContextHubTransaction.RESULT_FAILED_NOT_SUPPORTED;
+ }
+
+ @ContextHubTransaction.Result
public int loadNanoapp(int contextHubId, NanoAppBinary binary,
int transactionId) throws RemoteException {
android.hardware.contexthub.V1_0.NanoAppBinary hidlNanoAppBinary =
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
index cec7a79..5d415c2 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java
@@ -200,7 +200,9 @@
Slog.d(TAG, this + ": Starting");
}
mRunning = true;
- updateBinding();
+ if (!Flags.enablePreventionOfKeepAliveRouteProviders()) {
+ updateBinding();
+ }
}
if (rebindIfDisconnected && mActiveConnection == null && shouldBind()) {
unbind();
diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
index 233a3ab..fcca94b 100644
--- a/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
+++ b/services/core/java/com/android/server/media/MediaRoute2ProviderWatcher.java
@@ -150,7 +150,9 @@
mCallback.onAddProviderService(proxy);
} else if (sourceIndex >= targetIndex) {
MediaRoute2ProviderServiceProxy proxy = mProxies.get(sourceIndex);
- proxy.start(/* rebindIfDisconnected= */ true); // restart the proxy if needed
+ proxy.start(
+ /* rebindIfDisconnected= */
+ !Flags.enablePreventionOfKeepAliveRouteProviders());
Collections.swap(mProxies, sourceIndex, targetIndex++);
}
}
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 85a1315..1f7d549 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -20,6 +20,9 @@
import static android.content.Intent.ACTION_SCREEN_OFF;
import static android.content.Intent.ACTION_SCREEN_ON;
import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR;
+import static android.media.MediaRouter2.SCANNING_STATE_NOT_SCANNING;
+import static android.media.MediaRouter2.SCANNING_STATE_SCANNING_FULL;
+import static android.media.MediaRouter2.SCANNING_STATE_WHILE_INTERACTIVE;
import static android.media.MediaRouter2Utils.getOriginalId;
import static android.media.MediaRouter2Utils.getProviderId;
@@ -42,6 +45,7 @@
import android.media.MediaRoute2Info;
import android.media.MediaRoute2ProviderInfo;
import android.media.MediaRoute2ProviderService;
+import android.media.MediaRouter2.ScanningState;
import android.media.MediaRouter2Manager;
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
@@ -224,17 +228,27 @@
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier();
- final boolean hasConfigureWifiDisplayPermission = mContext.checkCallingOrSelfPermission(
- android.Manifest.permission.CONFIGURE_WIFI_DISPLAY)
- == PackageManager.PERMISSION_GRANTED;
+ final boolean hasConfigureWifiDisplayPermission =
+ mContext.checkCallingOrSelfPermission(Manifest.permission.CONFIGURE_WIFI_DISPLAY)
+ == PackageManager.PERMISSION_GRANTED;
final boolean hasModifyAudioRoutingPermission =
checkCallerHasModifyAudioRoutingPermission(pid, uid);
+ boolean hasMediaRoutingControlPermission =
+ checkMediaRoutingControlPermission(uid, pid, packageName);
+
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- registerRouter2Locked(router, uid, pid, packageName, userId,
- hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission);
+ registerRouter2Locked(
+ router,
+ uid,
+ pid,
+ packageName,
+ userId,
+ hasConfigureWifiDisplayPermission,
+ hasModifyAudioRoutingPermission,
+ hasMediaRoutingControlPermission);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -254,6 +268,21 @@
}
}
+ public void updateScanningState(
+ @NonNull IMediaRouter2 router, @ScanningState int scanningState) {
+ Objects.requireNonNull(router, "router must not be null");
+ validateScanningStateValue(scanningState);
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ updateScanningStateLocked(router, scanningState);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
public void setDiscoveryRequestWithRouter2(@NonNull IMediaRouter2 router,
@NonNull RouteDiscoveryPreference preference) {
Objects.requireNonNull(router, "router must not be null");
@@ -570,24 +599,15 @@
}
}
- public void startScan(@NonNull IMediaRouter2Manager manager) {
+ public void updateScanningState(
+ @NonNull IMediaRouter2Manager manager, @ScanningState int scanningState) {
Objects.requireNonNull(manager, "manager must not be null");
- final long token = Binder.clearCallingIdentity();
- try {
- synchronized (mLock) {
- startScanLocked(manager);
- }
- } finally {
- Binder.restoreCallingIdentity(token);
- }
- }
+ validateScanningStateValue(scanningState);
- public void stopScan(@NonNull IMediaRouter2Manager manager) {
- Objects.requireNonNull(manager, "manager must not be null");
final long token = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- stopScanLocked(manager);
+ updateScanningStateLocked(manager, scanningState);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -825,7 +845,16 @@
throw new SecurityException("Must hold MEDIA_CONTENT_CONTROL");
}
- if (PermissionChecker.checkPermissionForDataDelivery(
+ if (!checkMediaRoutingControlPermission(callerUid, callerPid, callerPackageName)) {
+ throw new SecurityException(
+ "Must hold MEDIA_CONTENT_CONTROL or MEDIA_ROUTING_CONTROL permissions.");
+ }
+ }
+
+ @RequiresPermission(value = Manifest.permission.MEDIA_ROUTING_CONTROL, conditional = true)
+ private boolean checkMediaRoutingControlPermission(
+ int callerUid, int callerPid, @Nullable String callerPackageName) {
+ return PermissionChecker.checkPermissionForDataDelivery(
mContext,
Manifest.permission.MEDIA_ROUTING_CONTROL,
callerPid,
@@ -833,11 +862,8 @@
callerPackageName,
/* attributionTag */ null,
/* message */ "Checking permissions for registering manager in"
- + " MediaRouter2ServiceImpl.")
- != PermissionChecker.PERMISSION_GRANTED) {
- throw new SecurityException(
- "Must hold MEDIA_CONTENT_CONTROL or MEDIA_ROUTING_CONTROL permissions.");
- }
+ + " MediaRouter2ServiceImpl.")
+ == PermissionChecker.PERMISSION_GRANTED;
}
@RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS)
@@ -944,9 +970,15 @@
// Start of locked methods that are used by MediaRouter2.
@GuardedBy("mLock")
- private void registerRouter2Locked(@NonNull IMediaRouter2 router, int uid, int pid,
- @NonNull String packageName, int userId, boolean hasConfigureWifiDisplayPermission,
- boolean hasModifyAudioRoutingPermission) {
+ private void registerRouter2Locked(
+ @NonNull IMediaRouter2 router,
+ int uid,
+ int pid,
+ @NonNull String packageName,
+ int userId,
+ boolean hasConfigureWifiDisplayPermission,
+ boolean hasModifyAudioRoutingPermission,
+ boolean hasMediaRoutingControlPermission) {
final IBinder binder = router.asBinder();
if (mAllRouterRecords.get(binder) != null) {
Slog.w(TAG, "registerRouter2Locked: Same router already exists. packageName="
@@ -955,8 +987,16 @@
}
UserRecord userRecord = getOrCreateUserRecordLocked(userId);
- RouterRecord routerRecord = new RouterRecord(userRecord, router, uid, pid, packageName,
- hasConfigureWifiDisplayPermission, hasModifyAudioRoutingPermission);
+ RouterRecord routerRecord =
+ new RouterRecord(
+ userRecord,
+ router,
+ uid,
+ pid,
+ packageName,
+ hasConfigureWifiDisplayPermission,
+ hasModifyAudioRoutingPermission,
+ hasMediaRoutingControlPermission);
try {
binder.linkToDeath(routerRecord, 0);
} catch (RemoteException ex) {
@@ -970,9 +1010,16 @@
obtainMessage(UserHandler::notifyRouterRegistered,
userRecord.mHandler, routerRecord));
- Slog.i(TAG, TextUtils.formatSimple(
- "registerRouter2 | package: %s, uid: %d, pid: %d, router id: %d",
- packageName, uid, pid, routerRecord.mRouterId));
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "registerRouter2 | package: %s, uid: %d, pid: %d, router id: %d,"
+ + " hasMediaRoutingControl: %b",
+ packageName,
+ uid,
+ pid,
+ routerRecord.mRouterId,
+ hasMediaRoutingControlPermission));
}
@GuardedBy("mLock")
@@ -1012,6 +1059,33 @@
}
@GuardedBy("mLock")
+ private void updateScanningStateLocked(
+ @NonNull IMediaRouter2 router, @ScanningState int scanningState) {
+ final IBinder binder = router.asBinder();
+ RouterRecord routerRecord = mAllRouterRecords.get(binder);
+ if (routerRecord == null) {
+ Slog.w(TAG, "Router record not found. Ignoring updateScanningState call.");
+ return;
+ }
+
+ if (scanningState == SCANNING_STATE_SCANNING_FULL
+ && !routerRecord.mHasMediaRoutingControl) {
+ throw new SecurityException("Screen off scan requires MEDIA_ROUTING_CONTROL");
+ }
+
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "updateScanningStateLocked | router: %d, packageName: %s, scanningState:"
+ + " %d",
+ routerRecord.mRouterId,
+ routerRecord.mPackageName,
+ getScanningStateString(scanningState)));
+
+ routerRecord.updateScanningState(scanningState);
+ }
+
+ @GuardedBy("mLock")
private void setDiscoveryRequestWithRouter2Locked(@NonNull RouterRecord routerRecord,
@NonNull RouteDiscoveryPreference discoveryRequest) {
if (routerRecord.mDiscoveryPreference.equals(discoveryRequest)) {
@@ -1347,14 +1421,24 @@
return;
}
+ boolean hasMediaRoutingControl =
+ checkMediaRoutingControlPermission(callerUid, callerPid, callerPackageName);
+
Slog.i(
TAG,
TextUtils.formatSimple(
"registerManager | callerUid: %d, callerPid: %d, callerPackage: %s,"
- + "targetPackageName: %s, targetUserId: %d",
- callerUid, callerPid, callerPackageName, targetPackageName, targetUser));
+ + " targetPackageName: %s, targetUserId: %d, hasMediaRoutingControl:"
+ + " %b",
+ callerUid,
+ callerPid,
+ callerPackageName,
+ targetPackageName,
+ targetUser,
+ hasMediaRoutingControl));
UserRecord userRecord = getOrCreateUserRecordLocked(targetUser.getIdentifier());
+
managerRecord =
new ManagerRecord(
userRecord,
@@ -1362,7 +1446,8 @@
callerUid,
callerPid,
callerPackageName,
- targetPackageName);
+ targetPackageName,
+ hasMediaRoutingControl);
try {
binder.linkToDeath(managerRecord, 0);
} catch (RemoteException ex) {
@@ -1427,41 +1512,31 @@
}
@GuardedBy("mLock")
- private void startScanLocked(@NonNull IMediaRouter2Manager manager) {
+ private void updateScanningStateLocked(
+ @NonNull IMediaRouter2Manager manager, @ScanningState int scanningState) {
final IBinder binder = manager.asBinder();
ManagerRecord managerRecord = mAllManagerRecords.get(binder);
if (managerRecord == null) {
+ Slog.w(TAG, "Manager record not found. Ignoring updateScanningState call.");
return;
}
+ if (!managerRecord.mHasMediaRoutingControl
+ && scanningState == SCANNING_STATE_SCANNING_FULL) {
+ throw new SecurityException("Screen off scan requires MEDIA_ROUTING_CONTROL");
+ }
+
Slog.i(
TAG,
TextUtils.formatSimple(
- "startScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
+ "updateScanningState | manager: %d, ownerPackageName: %s,"
+ + " targetPackageName: %s, scanningState: %d",
managerRecord.mManagerId,
managerRecord.mOwnerPackageName,
- managerRecord.mTargetPackageName));
+ managerRecord.mTargetPackageName,
+ getScanningStateString(scanningState)));
- managerRecord.startScan();
- }
-
- @GuardedBy("mLock")
- private void stopScanLocked(@NonNull IMediaRouter2Manager manager) {
- final IBinder binder = manager.asBinder();
- ManagerRecord managerRecord = mAllManagerRecords.get(binder);
- if (managerRecord == null) {
- return;
- }
-
- Slog.i(
- TAG,
- TextUtils.formatSimple(
- "stopScan | manager: %d, ownerPackageName: %s, targetPackageName: %s",
- managerRecord.mManagerId,
- managerRecord.mOwnerPackageName,
- managerRecord.mTargetPackageName));
-
- managerRecord.stopScan();
+ managerRecord.updateScanningState(scanningState);
}
@GuardedBy("mLock")
@@ -1738,6 +1813,24 @@
return (int) uniqueRequestId;
}
+ private static String getScanningStateString(@ScanningState int scanningState) {
+ return switch (scanningState) {
+ case SCANNING_STATE_NOT_SCANNING -> "NOT_SCANNING";
+ case SCANNING_STATE_WHILE_INTERACTIVE -> "SCREEN_ON_ONLY";
+ case SCANNING_STATE_SCANNING_FULL -> "FULL";
+ default -> "Invalid scanning state: " + scanningState;
+ };
+ }
+
+ private static void validateScanningStateValue(@ScanningState int scanningState) {
+ if (scanningState != SCANNING_STATE_NOT_SCANNING
+ && scanningState != SCANNING_STATE_WHILE_INTERACTIVE
+ && scanningState != SCANNING_STATE_SCANNING_FULL) {
+ throw new IllegalArgumentException(
+ TextUtils.formatSimple("Scanning state %d is not valid.", scanningState));
+ }
+ }
+
final class UserRecord {
public final int mUserId;
//TODO: make records private for thread-safety
@@ -1817,13 +1910,21 @@
public final boolean mHasModifyAudioRoutingPermission;
public final AtomicBoolean mHasBluetoothRoutingPermission;
public final int mRouterId;
+ public final boolean mHasMediaRoutingControl;
+ public @ScanningState int mScanningState = SCANNING_STATE_NOT_SCANNING;
public RouteDiscoveryPreference mDiscoveryPreference;
@Nullable public RouteListingPreference mRouteListingPreference;
- RouterRecord(UserRecord userRecord, IMediaRouter2 router, int uid, int pid,
- String packageName, boolean hasConfigureWifiDisplayPermission,
- boolean hasModifyAudioRoutingPermission) {
+ RouterRecord(
+ UserRecord userRecord,
+ IMediaRouter2 router,
+ int uid,
+ int pid,
+ String packageName,
+ boolean hasConfigureWifiDisplayPermission,
+ boolean hasModifyAudioRoutingPermission,
+ boolean hasMediaRoutingControl) {
mUserRecord = userRecord;
mPackageName = packageName;
mSelectRouteSequenceNumbers = new ArrayList<>();
@@ -1835,6 +1936,7 @@
mHasModifyAudioRoutingPermission = hasModifyAudioRoutingPermission;
mHasBluetoothRoutingPermission =
new AtomicBoolean(checkCallerHasBluetoothPermissions(mPid, mUid));
+ mHasMediaRoutingControl = hasMediaRoutingControl;
mRouterId = mNextRouterOrManagerId.getAndIncrement();
}
@@ -1846,6 +1948,12 @@
return mHasModifyAudioRoutingPermission || mHasBluetoothRoutingPermission.get();
}
+ public boolean isActivelyScanning() {
+ return mScanningState == SCANNING_STATE_WHILE_INTERACTIVE
+ || mScanningState == SCANNING_STATE_SCANNING_FULL
+ || mDiscoveryPreference.shouldPerformActiveScan();
+ }
+
@GuardedBy("mLock")
public void maybeUpdateSystemRoutingPermissionLocked() {
boolean oldSystemRoutingPermissionValue = hasSystemRoutingPermission();
@@ -1877,6 +1985,18 @@
routerDied(this);
}
+ public void updateScanningState(@ScanningState int scanningState) {
+ if (mScanningState == scanningState) {
+ return;
+ }
+
+ mScanningState = scanningState;
+
+ mUserRecord.mHandler.sendMessage(
+ obtainMessage(
+ UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler));
+ }
+
public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
pw.println(prefix + "RouterRecord");
@@ -2002,8 +2122,11 @@
public final int mManagerId;
// TODO (b/281072508): Document behaviour around nullability for mTargetPackageName.
@Nullable public final String mTargetPackageName;
+
+ public final boolean mHasMediaRoutingControl;
@Nullable public SessionCreationRequest mLastSessionCreationRequest;
- public boolean mIsScanning;
+
+ public @ScanningState int mScanningState = SCANNING_STATE_NOT_SCANNING;
ManagerRecord(
@NonNull UserRecord userRecord,
@@ -2011,7 +2134,8 @@
int ownerUid,
int ownerPid,
@NonNull String ownerPackageName,
- @Nullable String targetPackageName) {
+ @Nullable String targetPackageName,
+ boolean hasMediaRoutingControl) {
mUserRecord = userRecord;
mManager = manager;
mOwnerUid = ownerUid;
@@ -2019,6 +2143,7 @@
mOwnerPackageName = ownerPackageName;
mTargetPackageName = targetPackageName;
mManagerId = mNextRouterOrManagerId.getAndIncrement();
+ mHasMediaRoutingControl = hasMediaRoutingControl;
}
public void dispose() {
@@ -2040,29 +2165,23 @@
pw.println(indent + "mManagerId=" + mManagerId);
pw.println(indent + "mOwnerUid=" + mOwnerUid);
pw.println(indent + "mOwnerPid=" + mOwnerPid);
- pw.println(indent + "mIsScanning=" + mIsScanning);
+ pw.println(indent + "mScanningState=" + getScanningStateString(mScanningState));
if (mLastSessionCreationRequest != null) {
mLastSessionCreationRequest.dump(pw, indent);
}
}
- public void startScan() {
- if (mIsScanning) {
+ private void updateScanningState(@ScanningState int scanningState) {
+ if (mScanningState == scanningState) {
return;
}
- mIsScanning = true;
- mUserRecord.mHandler.sendMessage(PooledLambda.obtainMessage(
- UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler));
- }
- public void stopScan() {
- if (!mIsScanning) {
- return;
- }
- mIsScanning = false;
- mUserRecord.mHandler.sendMessage(PooledLambda.obtainMessage(
- UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler));
+ mScanningState = scanningState;
+
+ mUserRecord.mHandler.sendMessage(
+ obtainMessage(
+ UserHandler::updateDiscoveryPreferenceOnHandler, mUserRecord.mHandler));
}
@Override
@@ -3103,6 +3222,13 @@
buildCompositeDiscoveryPreference(
activeRouterRecords, areManagersScanning, activelyScanningPackages);
+ Slog.i(
+ TAG,
+ TextUtils.formatSimple(
+ "Updating composite discovery preference | preference: %s, active"
+ + " routers: %s",
+ newPreference, activelyScanningPackages));
+
if (updateScanningOnUserRecord(service, activelyScanningPackages, newPreference)) {
updateDiscoveryPreferenceForProviders(activelyScanningPackages);
}
@@ -3152,7 +3278,7 @@
for (RouterRecord activeRouterRecord : activeRouterRecords) {
RouteDiscoveryPreference preference = activeRouterRecord.mDiscoveryPreference;
preferredFeatures.addAll(preference.getPreferredFeatures());
- if (preference.shouldPerformActiveScan()) {
+ if (activeRouterRecord.isActivelyScanning()) {
activeScan = true;
activelyScanningPackages.add(activeRouterRecord.mPackageName);
}
@@ -3175,33 +3301,40 @@
private static List<RouterRecord> getIndividuallyActiveRouters(
MediaRouter2ServiceImpl service, List<RouterRecord> allRouterRecords) {
if (!Flags.disableScreenOffBroadcastReceiver()
- && !service.mPowerManager.isInteractive()) {
+ && !service.mPowerManager.isInteractive()
+ && !Flags.enableScreenOffScanning()) {
return Collections.emptyList();
}
return allRouterRecords.stream()
.filter(
record ->
- service.mActivityManager.getPackageImportance(
- record.mPackageName)
- <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING)
+ isPackageImportanceSufficientForScanning(
+ service, record.mPackageName)
+ || record.mScanningState
+ == SCANNING_STATE_SCANNING_FULL)
.collect(Collectors.toList());
}
private static boolean areManagersScanning(
MediaRouter2ServiceImpl service, List<ManagerRecord> managerRecords) {
if (!Flags.disableScreenOffBroadcastReceiver()
- && !service.mPowerManager.isInteractive()) {
+ && !service.mPowerManager.isInteractive()
+ && !Flags.enableScreenOffScanning()) {
return false;
}
- return managerRecords.stream()
- .anyMatch(
- manager ->
- manager.mIsScanning
- && service.mActivityManager.getPackageImportance(
- manager.mOwnerPackageName)
- <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING);
+ return managerRecords.stream().anyMatch(manager ->
+ (manager.mScanningState == SCANNING_STATE_WHILE_INTERACTIVE
+ && isPackageImportanceSufficientForScanning(service,
+ manager.mOwnerPackageName))
+ || manager.mScanningState == SCANNING_STATE_SCANNING_FULL);
+ }
+
+ private static boolean isPackageImportanceSufficientForScanning(
+ MediaRouter2ServiceImpl service, String packageName) {
+ return service.mActivityManager.getPackageImportance(packageName)
+ <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING;
}
private MediaRoute2Provider findProvider(@Nullable String providerId) {
diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java
index 7dd1314..6af3480 100644
--- a/services/core/java/com/android/server/media/MediaRouterService.java
+++ b/services/core/java/com/android/server/media/MediaRouterService.java
@@ -43,6 +43,7 @@
import android.media.IMediaRouterService;
import android.media.MediaRoute2Info;
import android.media.MediaRouter;
+import android.media.MediaRouter2.ScanningState;
import android.media.MediaRouterClientState;
import android.media.RemoteDisplayState;
import android.media.RemoteDisplayState.RemoteDisplayInfo;
@@ -439,6 +440,13 @@
// Binder call
@Override
+ public void updateScanningStateWithRouter2(
+ IMediaRouter2 router, @ScanningState int scanningState) {
+ mService2.updateScanningState(router, scanningState);
+ }
+
+ // Binder call
+ @Override
public void setDiscoveryRequestWithRouter2(IMediaRouter2 router,
RouteDiscoveryPreference request) {
mService2.setDiscoveryRequestWithRouter2(router, request);
@@ -574,14 +582,9 @@
// Binder call
@Override
- public void startScan(IMediaRouter2Manager manager) {
- mService2.startScan(manager);
- }
-
- // Binder call
- @Override
- public void stopScan(IMediaRouter2Manager manager) {
- mService2.stopScan(manager);
+ public void updateScanningState(
+ IMediaRouter2Manager manager, @ScanningState int scanningState) {
+ mService2.updateScanningState(manager, scanningState);
}
// Binder call
diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
index bbe6d3a..2cd8fe0 100644
--- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
+++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
@@ -18,10 +18,13 @@
import android.content.Context;
import android.content.pm.PackageManager;
+import android.hardware.DataSpace;
import android.media.MediaMetrics;
+import android.media.codec.Enums;
import android.media.metrics.BundleSession;
import android.media.metrics.EditingEndedEvent;
import android.media.metrics.IMediaMetricsManager;
+import android.media.metrics.MediaItemInfo;
import android.media.metrics.NetworkEvent;
import android.media.metrics.PlaybackErrorEvent;
import android.media.metrics.PlaybackMetrics;
@@ -31,7 +34,9 @@
import android.os.PersistableBundle;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
+import android.text.TextUtils;
import android.util.Base64;
+import android.util.Size;
import android.util.Slog;
import android.util.StatsEvent;
import android.util.StatsLog;
@@ -72,7 +77,14 @@
private static final String mMetricsId = MediaMetrics.Name.METRICS_MANAGER;
private static final String FAILED_TO_GET = "failed_to_get";
+
+ private static final MediaItemInfo EMPTY_MEDIA_ITEM_INFO = new MediaItemInfo.Builder().build();
+ private static final int DURATION_BUCKETS_BELOW_ONE_MINUTE = 8;
+ private static final int DURATION_BUCKETS_COUNT = 13;
+ private static final String AUDIO_MIME_TYPE_PREFIX = "audio/";
+ private static final String VIDEO_MIME_TYPE_PREFIX = "video/";
private final SecureRandom mSecureRandom;
+
@GuardedBy("mLock")
private Integer mMode = null;
@GuardedBy("mLock")
@@ -353,6 +365,51 @@
if (level == LOGGING_LEVEL_BLOCKED) {
return;
}
+ MediaItemInfo inputMediaItemInfo =
+ event.getInputMediaItemInfos().isEmpty()
+ ? EMPTY_MEDIA_ITEM_INFO
+ : event.getInputMediaItemInfos().get(0);
+ String inputAudioSampleMimeType =
+ getFilteredFirstMimeType(
+ inputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX);
+ String inputVideoSampleMimeType =
+ getFilteredFirstMimeType(
+ inputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX);
+ Size inputVideoSize = inputMediaItemInfo.getVideoSize();
+ int inputVideoResolution = getVideoResolutionEnum(inputVideoSize);
+ if (inputVideoResolution == Enums.RESOLUTION_UNKNOWN) {
+ // Try swapping width/height in case it's a portrait video.
+ inputVideoResolution =
+ getVideoResolutionEnum(
+ new Size(inputVideoSize.getHeight(), inputVideoSize.getWidth()));
+ }
+ List<String> inputCodecNames = inputMediaItemInfo.getCodecNames();
+ String inputFirstCodecName = !inputCodecNames.isEmpty() ? inputCodecNames.get(0) : "";
+ String inputSecondCodecName = inputCodecNames.size() > 1 ? inputCodecNames.get(1) : "";
+
+ MediaItemInfo outputMediaItemInfo =
+ event.getOutputMediaItemInfo() == null
+ ? EMPTY_MEDIA_ITEM_INFO
+ : event.getOutputMediaItemInfo();
+ String outputAudioSampleMimeType =
+ getFilteredFirstMimeType(
+ outputMediaItemInfo.getSampleMimeTypes(), AUDIO_MIME_TYPE_PREFIX);
+ String outputVideoSampleMimeType =
+ getFilteredFirstMimeType(
+ outputMediaItemInfo.getSampleMimeTypes(), VIDEO_MIME_TYPE_PREFIX);
+ Size outputVideoSize = outputMediaItemInfo.getVideoSize();
+ int outputVideoResolution = getVideoResolutionEnum(outputVideoSize);
+ if (outputVideoResolution == Enums.RESOLUTION_UNKNOWN) {
+ // Try swapping width/height in case it's a portrait video.
+ outputVideoResolution =
+ getVideoResolutionEnum(
+ new Size(outputVideoSize.getHeight(), outputVideoSize.getWidth()));
+ }
+ List<String> outputCodecNames = outputMediaItemInfo.getCodecNames();
+ String outputFirstCodecName =
+ !outputCodecNames.isEmpty() ? outputCodecNames.get(0) : "";
+ String outputSecondCodecName =
+ outputCodecNames.size() > 1 ? outputCodecNames.get(1) : "";
StatsEvent statsEvent =
StatsEvent.newBuilder()
.setAtomId(798)
@@ -360,6 +417,66 @@
.writeInt(event.getFinalState())
.writeInt(event.getErrorCode())
.writeLong(event.getTimeSinceCreatedMillis())
+ .writeInt(getThroughputFps(event))
+ .writeInt(event.getInputMediaItemInfos().size())
+ .writeInt(inputMediaItemInfo.getSourceType())
+ .writeLong(
+ getBucketedDurationMillis(
+ inputMediaItemInfo.getDurationMillis()))
+ .writeLong(
+ getBucketedDurationMillis(
+ inputMediaItemInfo.getClipDurationMillis()))
+ .writeString(
+ getFilteredMimeType(inputMediaItemInfo.getContainerMimeType()))
+ .writeString(inputAudioSampleMimeType)
+ .writeString(inputVideoSampleMimeType)
+ .writeInt(getCodecEnum(inputVideoSampleMimeType))
+ .writeInt(
+ getFilteredAudioSampleRateHz(
+ inputMediaItemInfo.getAudioSampleRateHz()))
+ .writeInt(inputMediaItemInfo.getAudioChannelCount())
+ .writeInt(inputVideoSize.getWidth())
+ .writeInt(inputVideoSize.getHeight())
+ .writeInt(inputVideoResolution)
+ .writeInt(getVideoResolutionAspectRatioEnum(inputVideoSize))
+ .writeInt(inputMediaItemInfo.getVideoDataSpace())
+ .writeInt(
+ getVideoHdrFormatEnum(
+ inputMediaItemInfo.getVideoDataSpace(),
+ inputVideoSampleMimeType))
+ .writeInt(Math.round(inputMediaItemInfo.getVideoFrameRate()))
+ .writeInt(getVideoFrameRateEnum(inputMediaItemInfo.getVideoFrameRate()))
+ .writeString(inputFirstCodecName)
+ .writeString(inputSecondCodecName)
+ .writeLong(
+ getBucketedDurationMillis(
+ outputMediaItemInfo.getDurationMillis()))
+ .writeLong(
+ getBucketedDurationMillis(
+ outputMediaItemInfo.getClipDurationMillis()))
+ .writeString(
+ getFilteredMimeType(outputMediaItemInfo.getContainerMimeType()))
+ .writeString(outputAudioSampleMimeType)
+ .writeString(outputVideoSampleMimeType)
+ .writeInt(getCodecEnum(outputVideoSampleMimeType))
+ .writeInt(
+ getFilteredAudioSampleRateHz(
+ outputMediaItemInfo.getAudioSampleRateHz()))
+ .writeInt(outputMediaItemInfo.getAudioChannelCount())
+ .writeInt(outputVideoSize.getWidth())
+ .writeInt(outputVideoSize.getHeight())
+ .writeInt(outputVideoResolution)
+ .writeInt(getVideoResolutionAspectRatioEnum(outputVideoSize))
+ .writeInt(outputMediaItemInfo.getVideoDataSpace())
+ .writeInt(
+ getVideoHdrFormatEnum(
+ outputMediaItemInfo.getVideoDataSpace(),
+ outputVideoSampleMimeType))
+ .writeInt(Math.round(outputMediaItemInfo.getVideoFrameRate()))
+ .writeInt(
+ getVideoFrameRateEnum(outputMediaItemInfo.getVideoFrameRate()))
+ .writeString(outputFirstCodecName)
+ .writeString(outputSecondCodecName)
.usePooledBuffer()
.build();
StatsLog.write(statsEvent);
@@ -511,4 +628,215 @@
}
}
}
+
+ private static int getThroughputFps(EditingEndedEvent event) {
+ MediaItemInfo outputMediaItemInfo = event.getOutputMediaItemInfo();
+ if (outputMediaItemInfo == null) {
+ return -1;
+ }
+ long videoSampleCount = outputMediaItemInfo.getVideoSampleCount();
+ if (videoSampleCount == MediaItemInfo.VALUE_UNSPECIFIED) {
+ return -1;
+ }
+ long elapsedTimeMs = event.getTimeSinceCreatedMillis();
+ if (elapsedTimeMs == EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) {
+ return -1;
+ }
+ return (int)
+ Math.min(Integer.MAX_VALUE, Math.round(1000.0 * videoSampleCount / elapsedTimeMs));
+ }
+
+ private static long getBucketedDurationMillis(long durationMillis) {
+ if (durationMillis == MediaItemInfo.VALUE_UNSPECIFIED || durationMillis <= 0) {
+ return -1;
+ }
+ // Bucket values in an exponential distribution to reduce the precision that's stored:
+ // bucket index -> range -> bucketed duration
+ // 1 -> [0, 469 ms) -> 235 ms
+ // 2 -> [469 ms, 938 ms) -> 469 ms
+ // 3 -> [938 ms, 1875 ms) -> 938 ms
+ // 4 -> [1875 ms, 3750 ms) -> 1875 ms
+ // 5 -> [3750 ms, 7500 ms) -> 3750 ms
+ // [...]
+ // 13 -> [960000 ms, max) -> 960000 ms
+ int bucketIndex =
+ (int)
+ Math.floor(
+ DURATION_BUCKETS_BELOW_ONE_MINUTE
+ + Math.log((durationMillis + 1) / 60_000.0) / Math.log(2));
+ // Clamp to range [0, DURATION_BUCKETS_COUNT].
+ bucketIndex = Math.min(DURATION_BUCKETS_COUNT, Math.max(0, bucketIndex));
+ // Map back onto the representative value for the bucket.
+ return (long)
+ Math.ceil(Math.pow(2, bucketIndex - DURATION_BUCKETS_BELOW_ONE_MINUTE) * 60_000.0);
+ }
+
+ /**
+ * Returns the first entry in {@code mimeTypes} with the given prefix, if it matches the
+ * filtering allowlist. If no entries match the prefix or if the first matching entry is not on
+ * the allowlist, returns an empty string.
+ */
+ private static String getFilteredFirstMimeType(List<String> mimeTypes, String prefix) {
+ int size = mimeTypes.size();
+ for (int i = 0; i < size; i++) {
+ String mimeType = mimeTypes.get(i);
+ if (mimeType.startsWith(prefix)) {
+ return getFilteredMimeType(mimeType);
+ }
+ }
+ return "";
+ }
+
+ private static String getFilteredMimeType(String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return "";
+ }
+ // Discard all inputs that aren't allowlisted MIME types.
+ return switch (mimeType) {
+ case "video/mp4",
+ "video/x-matroska",
+ "video/webm",
+ "video/3gpp",
+ "video/avc",
+ "video/hevc",
+ "video/x-vnd.on2.vp8",
+ "video/x-vnd.on2.vp9",
+ "video/av01",
+ "video/mp2t",
+ "video/mp4v-es",
+ "video/mpeg",
+ "video/x-flv",
+ "video/dolby-vision",
+ "video/raw",
+ "audio/mp4",
+ "audio/mp4a-latm",
+ "audio/x-matroska",
+ "audio/webm",
+ "audio/mpeg",
+ "audio/mpeg-L1",
+ "audio/mpeg-L2",
+ "audio/ac3",
+ "audio/eac3",
+ "audio/eac3-joc",
+ "audio/av4",
+ "audio/true-hd",
+ "audio/vnd.dts",
+ "audio/vnd.dts.hd",
+ "audio/vorbis",
+ "audio/opus",
+ "audio/flac",
+ "audio/ogg",
+ "audio/wav",
+ "audio/midi",
+ "audio/raw",
+ "application/mp4",
+ "application/webm",
+ "application/x-matroska",
+ "application/dash+xml",
+ "application/x-mpegURL",
+ "application/vnd.ms-sstr+xml" ->
+ mimeType;
+ default -> "";
+ };
+ }
+
+ private static int getCodecEnum(String mimeType) {
+ if (TextUtils.isEmpty(mimeType)) {
+ return Enums.CODEC_UNKNOWN;
+ }
+ return switch (mimeType) {
+ case "video/avc" -> Enums.CODEC_AVC;
+ case "video/hevc" -> Enums.CODEC_HEVC;
+ case "video/x-vnd.on2.vp8" -> Enums.CODEC_VP8;
+ case "video/x-vnd.on2.vp9" -> Enums.CODEC_VP9;
+ case "video/av01" -> Enums.CODEC_AV1;
+ default -> Enums.CODEC_UNKNOWN;
+ };
+ }
+
+ private static int getFilteredAudioSampleRateHz(int sampleRateHz) {
+ return switch (sampleRateHz) {
+ case 8000, 11025, 16000, 22050, 44100, 48000, 96000, 192000 -> sampleRateHz;
+ default -> -1;
+ };
+ }
+
+ private static int getVideoResolutionEnum(Size size) {
+ int width = size.getWidth();
+ int height = size.getHeight();
+ if (width == 352 && height == 640) {
+ return Enums.RESOLUTION_352X640;
+ } else if (width == 360 && height == 640) {
+ return Enums.RESOLUTION_360X640;
+ } else if (width == 480 && height == 640) {
+ return Enums.RESOLUTION_480X640;
+ } else if (width == 480 && height == 854) {
+ return Enums.RESOLUTION_480X854;
+ } else if (width == 540 && height == 960) {
+ return Enums.RESOLUTION_540X960;
+ } else if (width == 576 && height == 1024) {
+ return Enums.RESOLUTION_576X1024;
+ } else if (width == 1280 && height == 720) {
+ return Enums.RESOLUTION_720P_HD;
+ } else if (width == 1920 && height == 1080) {
+ return Enums.RESOLUTION_1080P_FHD;
+ } else if (width == 1440 && height == 2560) {
+ return Enums.RESOLUTION_1440X2560;
+ } else if (width == 3840 && height == 2160) {
+ return Enums.RESOLUTION_4K_UHD;
+ } else if (width == 7680 && height == 4320) {
+ return Enums.RESOLUTION_8K_UHD;
+ } else {
+ return Enums.RESOLUTION_UNKNOWN;
+ }
+ }
+
+ private static int getVideoResolutionAspectRatioEnum(Size size) {
+ int width = size.getWidth();
+ int height = size.getHeight();
+ if (width <= 0 || height <= 0) {
+ return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_UNSPECIFIED;
+ } else if (width < height) {
+ return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_PORTRAIT;
+ } else if (height < width) {
+ return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_LANDSCAPE;
+ } else {
+ return android.media.editing.Enums.RESOLUTION_ASPECT_RATIO_SQUARE;
+ }
+ }
+
+ private static int getVideoHdrFormatEnum(int dataSpace, String mimeType) {
+ if (dataSpace == DataSpace.DATASPACE_UNKNOWN) {
+ return Enums.HDR_FORMAT_UNKNOWN;
+ }
+ if (mimeType.equals("video/dolby-vision")) {
+ return Enums.HDR_FORMAT_DOLBY_VISION;
+ }
+ int standard = DataSpace.getStandard(dataSpace);
+ int transfer = DataSpace.getTransfer(dataSpace);
+ if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_HLG) {
+ return Enums.HDR_FORMAT_HLG;
+ }
+ if (standard == DataSpace.STANDARD_BT2020 && transfer == DataSpace.TRANSFER_ST2084) {
+ // We don't currently distinguish HDR10+ from HDR10.
+ return Enums.HDR_FORMAT_HDR10;
+ }
+ return Enums.HDR_FORMAT_NONE;
+ }
+
+ private static int getVideoFrameRateEnum(float frameRate) {
+ int frameRateInt = Math.round(frameRate);
+ return switch (frameRateInt) {
+ case 24 -> Enums.FRAMERATE_24;
+ case 25 -> Enums.FRAMERATE_25;
+ case 30 -> Enums.FRAMERATE_30;
+ case 50 -> Enums.FRAMERATE_50;
+ case 60 -> Enums.FRAMERATE_60;
+ case 120 -> Enums.FRAMERATE_120;
+ case 240 -> Enums.FRAMERATE_240;
+ case 480 -> Enums.FRAMERATE_480;
+ case 960 -> Enums.FRAMERATE_960;
+ default -> Enums.FRAMERATE_UNKNOWN;
+ };
+ }
}
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index d0c0543..f645eaa 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -16,6 +16,7 @@
package com.android.server.notification;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
import static android.content.Context.BIND_ALLOW_WHITELIST_MANAGEMENT;
import static android.content.Context.BIND_AUTO_CREATE;
import static android.content.Context.BIND_FOREGROUND_SERVICE;
@@ -24,6 +25,7 @@
import static android.os.UserHandle.USER_SYSTEM;
import static android.service.notification.NotificationListenerService.META_DATA_DEFAULT_AUTOBIND;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -1802,6 +1804,8 @@
public ComponentName component;
public int userid;
public boolean isSystem;
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ public boolean isSystemUi;
public ServiceConnection connection;
public int targetSdkVersion;
public Pair<ComponentName, Integer> mKey;
@@ -1836,6 +1840,11 @@
return isSystem;
}
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ public boolean isSystemUi() {
+ return isSystemUi;
+ }
+
@Override
public String toString() {
return new StringBuilder("ManagedServiceInfo[")
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index e7ad99a..3507d2d 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -71,6 +71,7 @@
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_ON;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
import static android.app.Flags.lifetimeExtensionRefactor;
import static android.app.NotificationManager.zenModeFromInterruptionFilter;
import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED;
@@ -159,6 +160,7 @@
import android.Manifest.permission;
import android.annotation.DurationMillisLong;
import android.annotation.ElapsedRealtimeLong;
+import android.annotation.FlaggedApi;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -1852,6 +1854,7 @@
}
if (ACTION_NOTIFICATION_TIMEOUT.equals(action)) {
final NotificationRecord record;
+ // TODO: b/323013410 - Record should be cloned instead of used directly.
synchronized (mNotificationLock) {
record = findNotificationByKeyLocked(intent.getStringExtra(EXTRA_KEY));
}
@@ -1864,6 +1867,14 @@
FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
| FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
true, record.getUserId(), REASON_TIMEOUT, null);
+ // If cancellation will be prevented due to lifetime extension, we send an
+ // update to system UI.
+ synchronized (mNotificationLock) {
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record,
+ record.getSbn().getPackageName(),
+ mActivityManager.getPackageImportance(
+ record.getSbn().getPackageName()));
+ }
} else {
cancelNotification(record.getSbn().getUid(),
record.getSbn().getInitialPid(),
@@ -3825,7 +3836,17 @@
int mustNotHaveFlags = isCallingUidSystem() ? 0 :
(FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB | FLAG_AUTOGROUP_SUMMARY);
if (lifetimeExtensionRefactor()) {
+ // Also don't allow client apps to cancel lifetime extended notifs.
mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+ // If cancellation will be prevented due to lifetime extension, we send an update to
+ // system UI.
+ NotificationRecord record = null;
+ final int packageImportance = mActivityManager.getPackageImportance(pkg);
+ synchronized (mNotificationLock) {
+ record = findNotificationLocked(pkg, tag, id, userId);
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg,
+ packageImportance);
+ }
}
cancelNotificationInternal(pkg, opPkg, Binder.getCallingUid(), Binder.getCallingPid(),
@@ -3845,6 +3866,16 @@
pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB
| FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY,
userId, REASON_APP_CANCEL_ALL);
+ // If cancellation will be prevented due to lifetime extension, we send updates
+ // to system UI.
+ // In this case, we need to hold the lock to access these lists.
+ final int packageImportance = mActivityManager.getPackageImportance(pkg);
+ synchronized (mNotificationLock) {
+ notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList,
+ packageImportance);
+ notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications,
+ packageImportance);
+ }
} else {
cancelAllNotificationsInt(Binder.getCallingUid(), Binder.getCallingPid(),
pkg, null, 0, FLAG_FOREGROUND_SERVICE | FLAG_USER_INITIATED_JOB,
@@ -4891,11 +4922,19 @@
final long identity = Binder.clearCallingIdentity();
boolean notificationsRapidlyCleared = false;
final String pkg;
+ final int packageImportance;
+ final ManagedServiceInfo info;
try {
synchronized (mNotificationLock) {
- final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
+ info = mListeners.checkServiceTokenLocked(token);
pkg = info.component.getPackageName();
-
+ }
+ if (lifetimeExtensionRefactor()) {
+ packageImportance = mActivityManager.getPackageImportance(pkg);
+ } else {
+ packageImportance = IMPORTANCE_NONE;
+ }
+ synchronized (mNotificationLock) {
// Cancellation reason. If the token comes from assistant, label the
// cancellation as coming from the assistant; default to LISTENER_CANCEL.
int reason = REASON_LISTENER_CANCEL;
@@ -4917,7 +4956,7 @@
|| isNotificationRecent(r.getUpdateTimeMs());
cancelNotificationFromListenerLocked(info, callingUid, callingPid,
r.getSbn().getPackageName(), r.getSbn().getTag(),
- r.getSbn().getId(), userId, reason);
+ r.getSbn().getId(), userId, reason, packageImportance);
}
} else {
for (NotificationRecord notificationRecord : mNotificationList) {
@@ -4931,6 +4970,12 @@
REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
FLAG_ONGOING_EVENT | FLAG_NO_CLEAR
| FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+ // If cancellation will be prevented due to lifetime extension, we send
+ // an update to system UI.
+ notifySystemUiListenerLifetimeExtendedListLocked(mNotificationList,
+ packageImportance);
+ notifySystemUiListenerLifetimeExtendedListLocked(mEnqueuedNotifications,
+ packageImportance);
} else {
cancelAllLocked(callingUid, callingPid, info.userid,
REASON_LISTENER_CANCEL_ALL, info, info.supportsProfiles(),
@@ -5051,10 +5096,14 @@
@GuardedBy("mNotificationLock")
private void cancelNotificationFromListenerLocked(ManagedServiceInfo info,
int callingUid, int callingPid, String pkg, String tag, int id, int userId,
- int reason) {
+ int reason, int packageImportance) {
int mustNotHaveFlags = FLAG_ONGOING_EVENT;
if (lifetimeExtensionRefactor()) {
mustNotHaveFlags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+ // If cancellation will be prevented due to lifetime extension, we send an update
+ // to system UI.
+ NotificationRecord record = findNotificationLocked(pkg, tag, id, userId);
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record, pkg, packageImportance);
}
cancelNotification(callingUid, callingPid, pkg, tag, id, 0 /* mustHaveFlags */,
mustNotHaveFlags,
@@ -5197,7 +5246,13 @@
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
final long identity = Binder.clearCallingIdentity();
+ final int packageImportance;
try {
+ if (lifetimeExtensionRefactor()) {
+ packageImportance = mActivityManager.getPackageImportance(pkg);
+ } else {
+ packageImportance = IMPORTANCE_NONE;
+ }
synchronized (mNotificationLock) {
final ManagedServiceInfo info = mListeners.checkServiceTokenLocked(token);
int cancelReason = REASON_LISTENER_CANCEL;
@@ -5210,7 +5265,7 @@
+ " use cancelNotification(key) instead.");
} else {
cancelNotificationFromListenerLocked(info, callingUid, callingPid,
- pkg, tag, id, info.userid, cancelReason);
+ pkg, tag, id, info.userid, cancelReason, packageImportance);
}
}
} finally {
@@ -11654,6 +11709,30 @@
});
}
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ @GuardedBy("mNotificationLock")
+ private void notifySystemUiListenerLifetimeExtendedListLocked(
+ List<NotificationRecord> notificationList, int packageImportance) {
+ for (int i = notificationList.size() - 1; i >= 0; --i) {
+ NotificationRecord record = notificationList.get(i);
+ maybeNotifySystemUiListenerLifetimeExtendedLocked(record,
+ record.getSbn().getPackageName(), packageImportance);
+ }
+ }
+
+ @FlaggedApi(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ @GuardedBy("mNotificationLock")
+ private void maybeNotifySystemUiListenerLifetimeExtendedLocked(NotificationRecord record,
+ String pkg, int packageImportance) {
+ if (record != null && (record.getSbn().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+ boolean isAppForeground = pkg != null && packageImportance == IMPORTANCE_FOREGROUND;
+ mHandler.post(new EnqueueNotificationRunnable(record.getUser().getIdentifier(),
+ record, isAppForeground,
+ mPostNotificationTrackerFactory.newTracker(null)));
+ }
+ }
+
public class NotificationListeners extends ManagedServices {
static final String TAG_ENABLED_NOTIFICATION_LISTENERS = "enabled_listeners";
static final String TAG_REQUESTED_LISTENERS = "request_listeners";
@@ -11777,6 +11856,11 @@
@Override
public void onServiceAdded(ManagedServiceInfo info) {
+ if (lifetimeExtensionRefactor()) {
+ // Only System or System UI can call registerSystemService, so if the caller is not
+ // system, we know it's system UI.
+ info.isSystemUi = !isCallerSystemOrPhone();
+ }
final INotificationListener listener = (INotificationListener) info.service;
final NotificationRankingUpdate update;
synchronized (mNotificationLock) {
@@ -12141,6 +12225,23 @@
continue;
}
+ if (lifetimeExtensionRefactor()) {
+ // Checks if this is a request to notify system UI about a notification that
+ // has been lifetime extended.
+ // (We only need to check old for the flag, because in both cancellation and
+ // update cases, old should have the flag.)
+ // If it is such a request, and this is system UI, we send the post request
+ // only to System UI, and break as we don't need to continue checking other
+ // Managed Services.
+ if (info.isSystemUi() && old != null && old.getNotification() != null
+ && (old.getNotification().flags
+ & Notification.FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY) > 0) {
+ final NotificationRankingUpdate update = makeRankingUpdateLocked(info);
+ listenerCalls.add(() -> notifyPosted(info, oldSbn, update));
+ break;
+ }
+ }
+
// If we shouldn't notify all listeners, this means the hidden state of
// a notification was changed. Don't notifyPosted listeners targeting >= P.
// Instead, those listeners will receive notifyRankingUpdate.
diff --git a/services/core/java/com/android/server/notification/ZenLog.java b/services/core/java/com/android/server/notification/ZenLog.java
index 88d23ce..82c5733 100644
--- a/services/core/java/com/android/server/notification/ZenLog.java
+++ b/services/core/java/com/android/server/notification/ZenLog.java
@@ -28,6 +28,7 @@
import android.service.notification.NotificationListenerService;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeDiff;
+import android.util.LocalLog;
import android.util.Log;
import android.util.Slog;
@@ -37,26 +38,16 @@
import java.util.List;
public class ZenLog {
- private static final String TAG = "ZenLog";
- // the ZenLog is *very* verbose, so be careful about setting this to true
- private static final boolean DEBUG = false;
private static final int SIZE = Build.IS_DEBUGGABLE ? 200 : 100;
- private static final long[] TIMES = new long[SIZE];
- private static final int[] TYPES = new int[SIZE];
- private static final String[] MSGS = new String[SIZE];
-
- private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
+ private static final LocalLog STATE_CHANGES = new LocalLog(SIZE);
+ private static final LocalLog INTERCEPTION_EVENTS = new LocalLog(SIZE);
private static final int TYPE_INTERCEPTED = 1;
- private static final int TYPE_ALLOW_DISABLE = 2;
private static final int TYPE_SET_RINGER_MODE_EXTERNAL = 3;
private static final int TYPE_SET_RINGER_MODE_INTERNAL = 4;
- private static final int TYPE_DOWNTIME = 5;
private static final int TYPE_SET_ZEN_MODE = 6;
- private static final int TYPE_UPDATE_ZEN_MODE = 7;
- private static final int TYPE_EXIT_CONDITION = 8;
private static final int TYPE_SUBSCRIBE = 9;
private static final int TYPE_UNSUBSCRIBE = 10;
private static final int TYPE_CONFIG = 11;
@@ -71,9 +62,6 @@
private static final int TYPE_CHECK_REPEAT_CALLER = 20;
private static final int TYPE_ALERT_ON_UPDATED_INTERCEPT = 21;
- private static int sNext;
- private static int sSize;
-
public static void traceIntercepted(NotificationRecord record, String reason) {
append(TYPE_INTERCEPTED, record.getKey() + "," + reason);
}
@@ -104,10 +92,6 @@
ringerModeToString(ringerModeExternalOut));
}
- public static void traceDowntimeAutotrigger(String result) {
- append(TYPE_DOWNTIME, result);
- }
-
public static void traceSetZenMode(int zenMode, String reason) {
append(TYPE_SET_ZEN_MODE, zenModeToString(zenMode) + "," + reason);
}
@@ -120,21 +104,12 @@
append(TYPE_SET_CONSOLIDATED_ZEN_POLICY, policy.toString() + "," + reason);
}
- public static void traceUpdateZenMode(int fromMode, int toMode) {
- append(TYPE_UPDATE_ZEN_MODE, zenModeToString(fromMode) + " -> " + zenModeToString(toMode));
- }
-
- public static void traceExitCondition(Condition c, ComponentName component, String reason) {
- append(TYPE_EXIT_CONDITION, c + "," + componentToString(component) + "," + reason);
- }
public static void traceSetNotificationPolicy(String pkg, int targetSdk,
NotificationManager.Policy policy) {
String policyLog = "pkg=" + pkg + " targetSdk=" + targetSdk
+ " NotificationPolicy=" + policy.toString();
append(TYPE_SET_NOTIFICATION_POLICY, policyLog);
- // TODO(b/180205791): remove when we can better surface apps that are changing policy
- Log.d(TAG, "Zen Policy Changed: " + policyLog);
}
public static void traceSubscribe(Uri uri, IConditionProvider provider, RemoteException e) {
@@ -145,13 +120,14 @@
append(TYPE_UNSUBSCRIBE, uri + "," + subscribeResult(provider, e));
}
- public static void traceConfig(String reason, ZenModeConfig oldConfig,
- ZenModeConfig newConfig) {
+ public static void traceConfig(String reason, ComponentName triggeringComponent,
+ ZenModeConfig oldConfig, ZenModeConfig newConfig, int callingUid) {
ZenModeDiff.ConfigDiff diff = new ZenModeDiff.ConfigDiff(oldConfig, newConfig);
if (diff == null || !diff.hasDiff()) {
append(TYPE_CONFIG, reason + " no changes");
} else {
append(TYPE_CONFIG, reason
+ + " - " + triggeringComponent + " : " + callingUid
+ ",\n" + (newConfig != null ? newConfig.toString() : null)
+ ",\n" + diff);
}
@@ -204,13 +180,9 @@
private static String typeToString(int type) {
switch (type) {
case TYPE_INTERCEPTED: return "intercepted";
- case TYPE_ALLOW_DISABLE: return "allow_disable";
case TYPE_SET_RINGER_MODE_EXTERNAL: return "set_ringer_mode_external";
case TYPE_SET_RINGER_MODE_INTERNAL: return "set_ringer_mode_internal";
- case TYPE_DOWNTIME: return "downtime";
case TYPE_SET_ZEN_MODE: return "set_zen_mode";
- case TYPE_UPDATE_ZEN_MODE: return "update_zen_mode";
- case TYPE_EXIT_CONDITION: return "exit_condition";
case TYPE_SUBSCRIBE: return "subscribe";
case TYPE_UNSUBSCRIBE: return "unsubscribe";
case TYPE_CONFIG: return "config";
@@ -278,30 +250,27 @@
}
private static void append(int type, String msg) {
- synchronized(MSGS) {
- TIMES[sNext] = System.currentTimeMillis();
- TYPES[sNext] = type;
- MSGS[sNext] = msg;
- sNext = (sNext + 1) % SIZE;
- if (sSize < SIZE) {
- sSize++;
+ if (type == TYPE_INTERCEPTED || type == TYPE_NOT_INTERCEPTED
+ || type == TYPE_CHECK_REPEAT_CALLER || type == TYPE_RECORD_CALLER
+ || type == TYPE_MATCHES_CALL_FILTER || type == TYPE_ALERT_ON_UPDATED_INTERCEPT) {
+ synchronized (INTERCEPTION_EVENTS) {
+ INTERCEPTION_EVENTS.log(typeToString(type) + ": " +msg);
+ }
+ } else {
+ synchronized (STATE_CHANGES) {
+ STATE_CHANGES.log(typeToString(type) + ": " +msg);
}
}
- if (DEBUG) Slog.d(TAG, typeToString(type) + ": " + msg);
}
public static void dump(PrintWriter pw, String prefix) {
- synchronized(MSGS) {
- final int start = (sNext - sSize + SIZE) % SIZE;
- for (int i = 0; i < sSize; i++) {
- final int j = (start + i) % SIZE;
- pw.print(prefix);
- pw.print(FORMAT.format(new Date(TIMES[j])));
- pw.print(' ');
- pw.print(typeToString(TYPES[j]));
- pw.print(": ");
- pw.println(MSGS[j]);
- }
+ synchronized (INTERCEPTION_EVENTS) {
+ pw.printf(prefix + "Interception Events:\n");
+ INTERCEPTION_EVENTS.dump(prefix, pw);
+ }
+ synchronized (STATE_CHANGES) {
+ pw.printf(prefix + "State Changes:\n");
+ STATE_CHANGES.dump(prefix, pw);
}
}
}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index aebd28a..1c20b2d 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -1713,7 +1713,7 @@
mConfigs.put(config.user, config);
}
if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable());
- ZenLog.traceConfig(reason, mConfig, config);
+ ZenLog.traceConfig(reason, triggeringComponent, mConfig, config, callingUid);
// send some broadcasts
Policy newPolicy = getNotificationPolicy(config);
diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java
index 25a39cc..86d05d9 100644
--- a/services/core/java/com/android/server/om/IdmapManager.java
+++ b/services/core/java/com/android/server/om/IdmapManager.java
@@ -257,7 +257,7 @@
private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage,
@NonNull AndroidPackage overlayPackage, int userId) {
String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName();
- if (targetOverlayableName != null) {
+ if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) {
try {
OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget(
targetPackage.getPackageName(), targetOverlayableName, userId);
diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java
index 79d1753..18ba2cf 100644
--- a/services/core/java/com/android/server/pm/AppDataHelper.java
+++ b/services/core/java/com/android/server/pm/AppDataHelper.java
@@ -50,6 +50,7 @@
import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
import com.android.server.pm.pkg.AndroidPackage;
import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.PackageUserStateInternal;
import com.android.server.pm.pkg.SELinuxUtil;
import dalvik.system.VMRuntime;
@@ -505,16 +506,21 @@
if (packageState == null) {
throw PackageManagerException.ofInternalError("Package " + packageName + " is unknown",
PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_PACKAGE_UNKNOWN);
- } else if (!TextUtils.equals(volumeUuid, packageState.getVolumeUuid())) {
+ }
+ if (!TextUtils.equals(volumeUuid, packageState.getVolumeUuid())) {
throw PackageManagerException.ofInternalError(
"Package " + packageName + " found on unknown volume " + volumeUuid
+ "; expected volume " + packageState.getVolumeUuid(),
PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_VOLUME_UNKNOWN);
- } else if (!packageState.getUserStateOrDefault(userId).isInstalled()) {
+ }
+ final PackageUserStateInternal userState = packageState.getUserStateOrDefault(userId);
+ if (!userState.isInstalled() && !userState.dataExists()) {
throw PackageManagerException.ofInternalError(
- "Package " + packageName + " not installed for user " + userId,
+ "Package " + packageName + " not installed for user " + userId
+ + " or was deleted without DELETE_KEEP_DATA",
PackageManagerException.INTERNAL_ERROR_STORAGE_INVALID_NOT_INSTALLED_FOR_USER);
- } else if (packageState.getPkg() != null
+ }
+ if (packageState.getPkg() != null
&& !shouldHaveAppStorage(packageState.getPkg())) {
throw PackageManagerException.ofInternalError(
"Package " + packageName + " shouldn't have storage",
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
index 3f00a9d..7d90240 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationManagerStub.java
@@ -26,6 +26,7 @@
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationUserState;
import android.content.pm.verify.domain.IDomainVerificationManager;
+import android.os.Bundle;
import android.os.ServiceSpecificException;
import java.util.List;
@@ -41,6 +42,27 @@
mService = service;
}
+ @Override
+ public void setUriRelativeFilterGroups(@NonNull String packageName,
+ @NonNull Bundle domainToGroupsBundle) {
+ try {
+ mService.setUriRelativeFilterGroups(packageName, domainToGroupsBundle);
+ } catch (Exception e) {
+ throw rethrow(e);
+ }
+ }
+
+ @NonNull
+ @Override
+ public Bundle getUriRelativeFilterGroups(
+ @NonNull String packageName, @NonNull List<String> domains) {
+ try {
+ return mService.getUriRelativeFilterGroups(packageName, domains);
+ } catch (Exception e) {
+ throw rethrow(e);
+ }
+ }
+
@NonNull
@Override
public List<String> queryValidVerificationPackageNames() {
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
index ac6d795..de464a4 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationPersistence.java
@@ -19,6 +19,8 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.content.UriRelativeFilter;
+import android.content.UriRelativeFilterGroup;
import android.content.pm.Signature;
import android.content.pm.verify.domain.DomainVerificationState;
import android.os.UserHandle;
@@ -38,7 +40,10 @@
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
import java.util.UUID;
import java.util.function.Function;
@@ -67,6 +72,13 @@
public static final String TAG_DOMAIN = "domain";
public static final String ATTR_NAME = "name";
public static final String ATTR_STATE = "state";
+ public static final String TAG_URI_RELATIVE_FILTER_GROUPS = "uri-relative-filter-groups";
+ public static final String TAG_URI_RELATIVE_FILTER_GROUP = "uri-relative-filter-group";
+ public static final String ATTR_ACTION = "action";
+ public static final String TAG_URI_RELATIVE_FILTER = "uri-relative-filter";
+ public static final String ATTR_URI_PART = "uri-part";
+ public static final String ATTR_PATTERN_TYPE = "pattern-type";
+ public static final String ATTR_FILTER = "filter";
/**
* @param pkgNameToSignature Converts package name to a string representation of its signature.
@@ -176,6 +188,7 @@
final ArrayMap<String, Integer> stateMap = new ArrayMap<>();
final SparseArray<DomainVerificationInternalUserState> userStates = new SparseArray<>();
+ final ArrayMap<String, List<UriRelativeFilterGroup>> groupMap = new ArrayMap<>();
SettingsXml.ChildSection child = section.children();
while (child.moveToNext()) {
@@ -186,11 +199,47 @@
case TAG_USER_STATES:
readUserStates(child, userStates);
break;
+ case TAG_URI_RELATIVE_FILTER_GROUPS:
+ readUriRelativeFilterGroups(child, groupMap);
+ break;
}
}
return new DomainVerificationPkgState(packageName, id, hasAutoVerifyDomains, stateMap,
- userStates, signature);
+ userStates, signature, groupMap);
+ }
+
+ private static void readUriRelativeFilterGroups(@NonNull SettingsXml.ReadSection section,
+ @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) {
+ SettingsXml.ChildSection child = section.children();
+ while (child.moveToNext(TAG_DOMAIN)) {
+ String domain = child.getString(ATTR_NAME);
+ groupMap.put(domain, createUriRelativeFilterGroupsFromXml(child));
+ }
+ }
+
+ private static ArrayList<UriRelativeFilterGroup> createUriRelativeFilterGroupsFromXml(
+ @NonNull SettingsXml.ReadSection section) {
+ SettingsXml.ChildSection child = section.children();
+ ArrayList<UriRelativeFilterGroup> groups = new ArrayList<>();
+ while (child.moveToNext(TAG_URI_RELATIVE_FILTER_GROUP)) {
+ UriRelativeFilterGroup group = new UriRelativeFilterGroup(section.getInt(ATTR_ACTION));
+ readUriRelativeFiltersFromXml(child, group);
+ groups.add(group);
+ }
+ return groups;
+ }
+
+ private static void readUriRelativeFiltersFromXml(
+ @NonNull SettingsXml.ReadSection section, UriRelativeFilterGroup group) {
+ SettingsXml.ChildSection child = section.children();
+ while (child.moveToNext(TAG_URI_RELATIVE_FILTER)) {
+ String filter = child.getString(ATTR_FILTER);
+ if (filter != null) {
+ group.addUriRelativeFilter(new UriRelativeFilter(child.getInt(ATTR_URI_PART),
+ child.getInt(ATTR_PATTERN_TYPE), filter));
+ }
+ }
}
private static void readUserStates(@NonNull SettingsXml.ReadSection section,
@@ -236,6 +285,7 @@
.attribute(ATTR_SIGNATURE, signature)) {
writeStateMap(parentSection, pkgState.getStateMap());
writeUserStates(parentSection, userId, pkgState.getUserStates());
+ writeUriRelativeFilterGroupMap(parentSection, pkgState.getUriRelativeFilterGroupMap());
}
}
@@ -334,6 +384,52 @@
}
}
+ private static void writeUriRelativeFilterGroupMap(
+ @NonNull SettingsXml.WriteSection parentSection,
+ @NonNull ArrayMap<String, List<UriRelativeFilterGroup>> groupMap) throws IOException {
+ if (groupMap.isEmpty()) {
+ return;
+ }
+ try (SettingsXml.WriteSection section =
+ parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUPS)) {
+ for (int i = 0; i < groupMap.size(); i++) {
+ writeUriRelativeFilterGroups(section, groupMap.keyAt(i), groupMap.valueAt(i));
+ }
+ }
+ }
+
+ private static void writeUriRelativeFilterGroups(
+ @NonNull SettingsXml.WriteSection parentSection, @NonNull String domain,
+ @NonNull List<UriRelativeFilterGroup> groups) throws IOException {
+ if (groups.isEmpty()) {
+ return;
+ }
+ try (SettingsXml.WriteSection section =
+ parentSection.startSection(TAG_DOMAIN)
+ .attribute(ATTR_NAME, domain)) {
+ for (int i = 0; i < groups.size(); i++) {
+ writeUriRelativeFilterGroup(section, groups.get(i));
+ }
+ }
+ }
+
+ private static void writeUriRelativeFilterGroup(
+ @NonNull SettingsXml.WriteSection parentSection,
+ @NonNull UriRelativeFilterGroup group) throws IOException {
+ try (SettingsXml.WriteSection section =
+ parentSection.startSection(TAG_URI_RELATIVE_FILTER_GROUP)
+ .attribute(ATTR_ACTION, group.getAction())) {
+ Iterator<UriRelativeFilter> it = group.getUriRelativeFilters().iterator();
+ while (it.hasNext()) {
+ UriRelativeFilter filter = it.next();
+ section.startSection(TAG_URI_RELATIVE_FILTER)
+ .attribute(ATTR_URI_PART, filter.getUriPart())
+ .attribute(ATTR_PATTERN_TYPE, filter.getPatternType())
+ .attribute(ATTR_FILTER, filter.getFilter()).finish();
+ }
+ }
+ }
+
public static class ReadResult {
@NonNull
diff --git a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
index c796b40..305b087 100644
--- a/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
+++ b/services/core/java/com/android/server/pm/verify/domain/DomainVerificationService.java
@@ -19,14 +19,19 @@
import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;
+import android.Manifest;
import android.annotation.CheckResult;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.compat.annotation.ChangeId;
import android.content.Context;
import android.content.Intent;
+import android.content.UriRelativeFilterGroup;
+import android.content.UriRelativeFilterGroupParcel;
+import android.content.pm.Flags;
import android.content.pm.IntentFilterVerificationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -38,6 +43,8 @@
import android.content.pm.verify.domain.DomainVerificationState;
import android.content.pm.verify.domain.DomainVerificationUserState;
import android.content.pm.verify.domain.IDomainVerificationManager;
+import android.net.Uri;
+import android.os.Bundle;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -223,6 +230,72 @@
mProxy = proxy;
}
+ /**
+ * Update the URI relative filter groups for a package's verified domains. All previously
+ * existing groups will be cleared before the new groups will be applied.
+ */
+ @RequiresPermission(Manifest.permission.DOMAIN_VERIFICATION_AGENT)
+ public void setUriRelativeFilterGroups(@NonNull String packageName,
+ @NonNull Bundle bundle)
+ throws NameNotFoundException {
+ getContext().enforceCallingOrSelfPermission(
+ android.Manifest.permission.DOMAIN_VERIFICATION_AGENT,
+ "Caller " + mConnection.getCallingUid() + " does not hold "
+ + android.Manifest.permission.DOMAIN_VERIFICATION_AGENT);
+ if (bundle.isEmpty()) {
+ return;
+ }
+ synchronized (mLock) {
+ DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+ if (pkgState == null) {
+ throw DomainVerificationUtils.throwPackageUnavailable(packageName);
+ }
+ Map<String, List<UriRelativeFilterGroup>> domainToGroupsMap =
+ pkgState.getUriRelativeFilterGroupMap();
+ for (String domain : bundle.keySet()) {
+ ArrayList<UriRelativeFilterGroupParcel> parcels =
+ bundle.getParcelableArrayList(domain, UriRelativeFilterGroupParcel.class);
+ domainToGroupsMap.put(domain, UriRelativeFilterGroup.parcelsToGroups(parcels));
+ }
+ }
+ }
+
+ /**
+ * Retrieve the current URI relative filter groups for a package's verified domain.
+ */
+ @NonNull
+ public Bundle getUriRelativeFilterGroups(@NonNull String packageName,
+ @NonNull List<String> domains) {
+ Bundle bundle = new Bundle();
+ synchronized (mLock) {
+ DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+ if (pkgState != null) {
+ Map<String, List<UriRelativeFilterGroup>> map =
+ pkgState.getUriRelativeFilterGroupMap();
+ for (int i = 0; i < domains.size(); i++) {
+ List<UriRelativeFilterGroup> groups = map.get(domains.get(i));
+ bundle.putParcelableList(domains.get(i),
+ UriRelativeFilterGroup.groupsToParcels(groups));
+ }
+ }
+ }
+ return bundle;
+ }
+
+ @NonNull
+ private List<UriRelativeFilterGroup> getUriRelativeFilterGroups(@NonNull String packageName,
+ @NonNull String domain) {
+ List<UriRelativeFilterGroup> groups = Collections.emptyList();
+ synchronized (mLock) {
+ DomainVerificationPkgState pkgState = mAttachedPkgStates.get(packageName);
+ if (pkgState != null) {
+ groups = pkgState.getUriRelativeFilterGroupMap().getOrDefault(domain,
+ Collections.emptyList());
+ }
+ }
+ return groups;
+ }
+
@NonNull
public List<String> queryValidVerificationPackageNames() {
mEnforcer.assertApprovedVerifier(mConnection.getCallingUid(), mProxy);
@@ -891,6 +964,8 @@
}
ArrayMap<String, Integer> oldStateMap = oldPkgState.getStateMap();
+ ArrayMap<String, List<UriRelativeFilterGroup>> oldGroups =
+ oldPkgState.getUriRelativeFilterGroupMap();
ArraySet<String> newAutoVerifyDomains =
mCollector.collectValidAutoVerifyDomains(newPkg);
int newDomainsSize = newAutoVerifyDomains.size();
@@ -941,7 +1016,7 @@
mAttachedPkgStates.put(pkgName, newDomainSetId, new DomainVerificationPkgState(
pkgName, newDomainSetId, hasAutoVerifyDomains, newStateMap, newUserStates,
- null /* signature */));
+ null /* signature */, oldGroups));
}
if (sendBroadcast) {
@@ -1572,8 +1647,6 @@
public Pair<List<ResolveInfo>, Integer> filterToApprovedApp(@NonNull Intent intent,
@NonNull List<ResolveInfo> infos, @UserIdInt int userId,
@NonNull Function<String, PackageStateInternal> pkgSettingFunction) {
- String domain = intent.getData().getHost();
-
// Collect valid infos
ArrayMap<ResolveInfo, Integer> infoApprovals = new ArrayMap<>();
int infosSize = infos.size();
@@ -1586,7 +1659,7 @@
}
// Find all approval levels
- int highestApproval = fillMapWithApprovalLevels(infoApprovals, domain, userId,
+ int highestApproval = fillMapWithApprovalLevels(infoApprovals, intent.getData(), userId,
pkgSettingFunction);
if (highestApproval <= APPROVAL_LEVEL_NONE) {
return Pair.create(emptyList(), highestApproval);
@@ -1623,12 +1696,23 @@
return Pair.create(finalList, highestApproval);
}
+ private boolean matchUriRelativeFilterGroups(Uri uri, String pkgName) {
+ if (uri.getHost() == null) {
+ return false;
+ }
+ List<UriRelativeFilterGroup> groups = getUriRelativeFilterGroups(pkgName, uri.getHost());
+ if (groups.isEmpty()) {
+ return true;
+ }
+ return UriRelativeFilterGroup.matchGroupsToUri(groups, uri);
+ }
+
/**
* @return highest approval level found
*/
@ApprovalLevel
private int fillMapWithApprovalLevels(@NonNull ArrayMap<ResolveInfo, Integer> inputMap,
- @NonNull String domain, @UserIdInt int userId,
+ @NonNull Uri uri, @UserIdInt int userId,
@NonNull Function<String, PackageStateInternal> pkgSettingFunction) {
int highestApproval = APPROVAL_LEVEL_NONE;
int size = inputMap.size();
@@ -1641,12 +1725,13 @@
ResolveInfo info = inputMap.keyAt(index);
final String packageName = info.getComponentInfo().packageName;
PackageStateInternal pkgSetting = pkgSettingFunction.apply(packageName);
- if (pkgSetting == null) {
+ if (pkgSetting == null || (Flags.relativeReferenceIntentFilters()
+ && !matchUriRelativeFilterGroups(uri, packageName))) {
fillInfoMapForSamePackage(inputMap, packageName, APPROVAL_LEVEL_NONE);
continue;
}
- int approval = approvalLevelForDomain(pkgSetting, domain, false, userId, DEBUG_APPROVAL,
- domain);
+ int approval = approvalLevelForDomain(pkgSetting, uri.getHost(), false, userId,
+ DEBUG_APPROVAL, uri.getHost());
highestApproval = Math.max(highestApproval, approval);
fillInfoMapForSamePackage(inputMap, packageName, approval);
}
diff --git a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
index d71dbbb..46051fe 100644
--- a/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
+++ b/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.content.UriRelativeFilterGroup;
import android.content.pm.Signature;
import android.content.pm.verify.domain.DomainVerificationState;
import android.util.ArrayMap;
@@ -26,6 +27,7 @@
import com.android.internal.util.DataClass;
+import java.util.List;
import java.util.Objects;
import java.util.UUID;
@@ -77,15 +79,30 @@
@Nullable
private final String mBackupSignatureHash;
+ /**
+ * List of {@link UriRelativeFilterGroup} for filtering domains.
+ */
+ @NonNull
+ private final ArrayMap<String, List<UriRelativeFilterGroup>> mUriRelativeFilterGroupMap;
+
public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id,
boolean hasAutoVerifyDomains) {
- this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null);
+ this(packageName, id, hasAutoVerifyDomains, new ArrayMap<>(0), new SparseArray<>(0), null,
+ new ArrayMap<>());
}
public DomainVerificationPkgState(@NonNull DomainVerificationPkgState pkgState,
@NonNull UUID id, boolean hasAutoVerifyDomains) {
this(pkgState.getPackageName(), id, hasAutoVerifyDomains, pkgState.getStateMap(),
- pkgState.getUserStates(), null);
+ pkgState.getUserStates(), null, new ArrayMap<>());
+ }
+
+ public DomainVerificationPkgState(@NonNull String packageName, @NonNull UUID id,
+ boolean hasAutoVerifyDomains, @NonNull ArrayMap<String, Integer> stateMap,
+ @NonNull SparseArray<DomainVerificationInternalUserState> userStates,
+ @Nullable String backupSignatureHash) {
+ this(packageName, id, hasAutoVerifyDomains, stateMap, userStates, backupSignatureHash,
+ new ArrayMap<>());
}
@Nullable
@@ -158,6 +175,8 @@
*
* It's assumed the domain verification agent will eventually re-verify this domain
* and revoke if necessary.
+ * @param uriRelativeFilterGroupMap
+ * List of {@link UriRelativeFilterGroup} for filtering domains.
*/
@DataClass.Generated.Member
public DomainVerificationPkgState(
@@ -166,7 +185,8 @@
boolean hasAutoVerifyDomains,
@NonNull ArrayMap<String,Integer> stateMap,
@NonNull SparseArray<DomainVerificationInternalUserState> userStates,
- @Nullable String backupSignatureHash) {
+ @Nullable String backupSignatureHash,
+ @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> uriRelativeFilterGroupMap) {
this.mPackageName = packageName;
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mPackageName);
@@ -181,6 +201,9 @@
com.android.internal.util.AnnotationValidations.validate(
NonNull.class, null, mUserStates);
this.mBackupSignatureHash = backupSignatureHash;
+ this.mUriRelativeFilterGroupMap = uriRelativeFilterGroupMap;
+ com.android.internal.util.AnnotationValidations.validate(
+ NonNull.class, null, mUriRelativeFilterGroupMap);
// onConstructed(); // You can define this method to get a callback
}
@@ -239,6 +262,14 @@
return mBackupSignatureHash;
}
+ /**
+ * List of {@link UriRelativeFilterGroup} for filtering domains.
+ */
+ @DataClass.Generated.Member
+ public @NonNull ArrayMap<String,List<UriRelativeFilterGroup>> getUriRelativeFilterGroupMap() {
+ return mUriRelativeFilterGroupMap;
+ }
+
@Override
@DataClass.Generated.Member
public String toString() {
@@ -251,7 +282,8 @@
"hasAutoVerifyDomains = " + mHasAutoVerifyDomains + ", " +
"stateMap = " + mStateMap + ", " +
"userStates = " + mUserStates + ", " +
- "backupSignatureHash = " + mBackupSignatureHash +
+ "backupSignatureHash = " + mBackupSignatureHash + ", " +
+ "uriRelativeFilterGroupMap = " + mUriRelativeFilterGroupMap +
" }";
}
@@ -273,7 +305,8 @@
&& mHasAutoVerifyDomains == that.mHasAutoVerifyDomains
&& Objects.equals(mStateMap, that.mStateMap)
&& userStatesEquals(that.mUserStates)
- && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash);
+ && Objects.equals(mBackupSignatureHash, that.mBackupSignatureHash)
+ && Objects.equals(mUriRelativeFilterGroupMap, that.mUriRelativeFilterGroupMap);
}
@Override
@@ -289,14 +322,15 @@
_hash = 31 * _hash + Objects.hashCode(mStateMap);
_hash = 31 * _hash + userStatesHashCode();
_hash = 31 * _hash + Objects.hashCode(mBackupSignatureHash);
+ _hash = 31 * _hash + Objects.hashCode(mUriRelativeFilterGroupMap);
return _hash;
}
@DataClass.Generated(
- time = 1617315369614L,
+ time = 1707351734724L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/verify/domain/models/DomainVerificationPkgState.java",
- inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate @android.annotation.NonNull java.util.UUID mId\nprivate final boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic void removeUser(int)\npublic void removeAllUsers()\nprivate int userStatesHashCode()\nprivate boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)")
+ inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.NonNull java.util.UUID mId\nprivate final boolean mHasAutoVerifyDomains\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.lang.Integer> mStateMap\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState> mUserStates\nprivate final @android.annotation.Nullable java.lang.String mBackupSignatureHash\nprivate final @android.annotation.NonNull android.util.ArrayMap<java.lang.String,java.util.List<android.content.UriRelativeFilterGroup>> mUriRelativeFilterGroupMap\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getUserState(int)\npublic @android.annotation.Nullable com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState getOrCreateUserState(int)\npublic void removeUser(int)\npublic void removeAllUsers()\nprivate int userStatesHashCode()\nprivate boolean userStatesEquals(android.util.SparseArray<com.android.server.pm.verify.domain.models.DomainVerificationInternalUserState>)\nclass DomainVerificationPkgState extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genToString=true, genEqualsHashCode=true)")
@Deprecated
private void __metadata() {}
diff --git a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
index afcf5a0..76952b3 100644
--- a/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
+++ b/services/core/java/com/android/server/policy/DeviceStateProviderImpl.java
@@ -29,6 +29,7 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.os.Environment;
import android.os.PowerManager;
import android.util.ArrayMap;
@@ -40,7 +41,6 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.input.InputManagerInternal;
import com.android.server.policy.devicestate.config.Conditions;
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 25e749f..00036e4 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -137,7 +137,6 @@
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
-import com.android.net.module.util.NetworkCapabilitiesUtils;
import com.android.server.power.optimization.Flags;
import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
@@ -1716,14 +1715,101 @@
return mMaxLearnedBatteryCapacityUah;
}
+ public class FrameworkStatsLogger {
+ public void uidProcessStateChanged(int uid, int state) {
+ // TODO(b/155216561): It is possible for isolated uids to be in a higher
+ // state than its parent uid. We should track the highest state within the union of host
+ // and isolated uids rather than only the parent uid.
+ FrameworkStatsLog.write(FrameworkStatsLog.UID_PROCESS_STATE_CHANGED, uid,
+ ActivityManager.processStateAmToProto(state));
+ }
+
+ public void wakelockStateChanged(int uid, WorkChain wc, String name, int type,
+ int procState, boolean acquired) {
+ int event = acquired
+ ? FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE
+ : FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE;
+ if (wc != null) {
+ FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(),
+ wc.getTags(), getPowerManagerWakeLockLevel(type), name,
+ event, procState);
+ } else {
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED,
+ mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name,
+ event, procState);
+ }
+ }
+
+ public void kernelWakeupReported(long deltaUptimeUs) {
+ FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason,
+ /* duration_usec */ deltaUptimeUs, mLastWakeupElapsedTimeMs);
+ }
+
+ public void gpsScanStateChanged(int uid, WorkChain workChain, boolean stateOn) {
+ int event = stateOn
+ ? FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON
+ : FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF;
+ if (workChain != null) {
+ FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), event);
+ } else {
+ FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
+ uid, null, event);
+ }
+ }
+
+ public void batterySaverModeChanged(boolean enabled) {
+ FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED,
+ enabled
+ ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON
+ : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF);
+ }
+
+ public void deviceIdlingModeStateChanged(int mode) {
+ FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, mode);
+ }
+
+ public void deviceIdleModeStateChanged(int mode) {
+ FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode);
+ }
+
+ public void chargingStateChanged(int status) {
+ FrameworkStatsLog.write(FrameworkStatsLog.CHARGING_STATE_CHANGED, status);
+ }
+
+ public void pluggedStateChanged(int plugType) {
+ FrameworkStatsLog.write(FrameworkStatsLog.PLUGGED_STATE_CHANGED, plugType);
+ }
+
+ public void batteryLevelChanged(int level) {
+ FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_LEVEL_CHANGED, level);
+ }
+
+ public void phoneServiceStateChanged(int state, int simState, int strengthBin) {
+ FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
+ simState, strengthBin);
+ }
+
+ public void phoneSignalStrengthChanged(int strengthBin) {
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin);
+ }
+ }
+
+ private final FrameworkStatsLogger mFrameworkStatsLogger;
+
@VisibleForTesting
public BatteryStatsImpl(Clock clock, File historyDirectory, @NonNull Handler handler,
- @NonNull PowerStatsUidResolver powerStatsUidResolver) {
+ @NonNull PowerStatsUidResolver powerStatsUidResolver,
+ @NonNull FrameworkStatsLogger frameworkStatsLogger,
+ @NonNull BatteryStatsHistory.TraceDelegate traceDelegate,
+ @NonNull BatteryStatsHistory.EventLogger eventLogger) {
mClock = clock;
initKernelStatsReaders();
mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
mHandler = handler;
mPowerStatsUidResolver = powerStatsUidResolver;
+ mFrameworkStatsLogger = frameworkStatsLogger;
mConstants = new Constants(mHandler);
mStartClockTimeMs = clock.currentTimeMillis();
mDailyFile = null;
@@ -1732,12 +1818,14 @@
mCheckinFile = null;
mStatsFile = null;
mHistory = new BatteryStatsHistory(mConstants.MAX_HISTORY_FILES,
- mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock);
+ mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock,
+ traceDelegate, eventLogger);
} else {
mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin"));
mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
mHistory = new BatteryStatsHistory(historyDirectory, mConstants.MAX_HISTORY_FILES,
- mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock);
+ mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock, mMonotonicClock,
+ traceDelegate, eventLogger);
}
mPlatformIdleStateCallback = null;
mEnergyConsumerRetriever = null;
@@ -4269,7 +4357,7 @@
}
@GuardedBy("this")
- private void updateBatteryPropertiesLocked() {
+ protected void updateBatteryPropertiesLocked() {
try {
IBatteryPropertiesRegistrar registrar = IBatteryPropertiesRegistrar.Stub.asInterface(
ServiceManager.getService("batteryproperties"));
@@ -4403,11 +4491,7 @@
return;
}
}
- // TODO(b/155216561): It is possible for isolated uids to be in a higher
- // state than its parent uid. We should track the highest state within the union of host
- // and isolated uids rather than only the parent uid.
- FrameworkStatsLog.write(FrameworkStatsLog.UID_PROCESS_STATE_CHANGED, uid,
- ActivityManager.processStateAmToProto(state));
+ mFrameworkStatsLogger.uidProcessStateChanged(uid, state);
getUidStatsLocked(parentUid, elapsedRealtimeMs, uptimeMs)
.updateUidProcessStateLocked(state, elapsedRealtimeMs, uptimeMs);
}
@@ -4721,17 +4805,8 @@
Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs);
uidStats.noteStartWakeLocked(pid, name, type, elapsedRealtimeMs);
- int procState = uidStats.mProcessState;
-
- if (wc != null) {
- FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(),
- wc.getTags(), getPowerManagerWakeLockLevel(type), name,
- FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE, procState);
- } else {
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED,
- mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name,
- FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__ACQUIRE, procState);
- }
+ mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, type,
+ uidStats.mProcessState, true /* acquired */);
}
}
@@ -4774,16 +4849,8 @@
Uid uidStats = getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs);
uidStats.noteStopWakeLocked(pid, name, type, elapsedRealtimeMs);
- int procState = uidStats.mProcessState;
- if (wc != null) {
- FrameworkStatsLog.write(FrameworkStatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(),
- wc.getTags(), getPowerManagerWakeLockLevel(type), name,
- FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE, procState);
- } else {
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.WAKELOCK_STATE_CHANGED,
- mapIsolatedUid(uid), null, getPowerManagerWakeLockLevel(type), name,
- FrameworkStatsLog.WAKELOCK_STATE_CHANGED__STATE__RELEASE, procState);
- }
+ mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name, type,
+ uidStats.mProcessState, false /* acquired */);
if (mappedUid != uid) {
// Decrement the ref count for the isolated uid and delete the mapping if uneeded.
@@ -5020,8 +5087,7 @@
long deltaUptimeMs = uptimeMs - mLastWakeupUptimeMs;
SamplingTimer timer = getWakeupReasonTimerLocked(mLastWakeupReason);
timer.add(deltaUptimeMs * 1000, 1, elapsedRealtimeMs); // time in in microseconds
- FrameworkStatsLog.write(FrameworkStatsLog.KERNEL_WAKEUP_REPORTED, mLastWakeupReason,
- /* duration_usec */ deltaUptimeMs * 1000, mLastWakeupElapsedTimeMs);
+ mFrameworkStatsLogger.kernelWakeupReported(deltaUptimeMs * 1000);
mLastWakeupReason = null;
}
}
@@ -5159,14 +5225,7 @@
}
mGpsNesting++;
- if (workChain == null) {
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
- mapIsolatedUid(uid), null, FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON);
- } else {
- FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
- workChain.getUids(), workChain.getTags(),
- FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__ON);
- }
+ mFrameworkStatsLogger.gpsScanStateChanged(mapIsolatedUid(uid), workChain, /* on */true);
getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs).noteStartGps(elapsedRealtimeMs);
}
@@ -5188,14 +5247,7 @@
mGpsSignalQualityBin = -1;
}
- if (workChain == null) {
- FrameworkStatsLog.write_non_chained(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED,
- mapIsolatedUid(uid), null,
- FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF);
- } else {
- FrameworkStatsLog.write(FrameworkStatsLog.GPS_SCAN_STATE_CHANGED, workChain.getUids(),
- workChain.getTags(), FrameworkStatsLog.GPS_SCAN_STATE_CHANGED__STATE__OFF);
- }
+ mFrameworkStatsLogger.gpsScanStateChanged(mapIsolatedUid(uid), workChain, /* on */ false);
getUidStatsLocked(mappedUid, elapsedRealtimeMs, uptimeMs).noteStopGps(elapsedRealtimeMs);
}
@@ -5673,10 +5725,7 @@
} else {
// Log an initial value for BATTERY_SAVER_MODE_STATE_CHANGED in order to
// allow the atom to read all future state changes.
- FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED,
- enabled
- ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON
- : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF);
+ mFrameworkStatsLogger.batterySaverModeChanged(enabled);
}
}
@@ -5696,10 +5745,7 @@
HistoryItem.STATE2_POWER_SAVE_FLAG);
mPowerSaveModeEnabledTimer.stopRunningLocked(elapsedRealtimeMs);
}
- FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED,
- enabled
- ? FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__ON
- : FrameworkStatsLog.BATTERY_SAVER_MODE_STATE_CHANGED__STATE__OFF);
+ mFrameworkStatsLogger.batterySaverModeChanged(enabled);
}
}
@@ -5727,7 +5773,7 @@
if (nowIdling) statsmode = DEVICE_IDLE_MODE_DEEP;
else if (nowLightIdling) statsmode = DEVICE_IDLE_MODE_LIGHT;
else statsmode = DEVICE_IDLE_MODE_OFF;
- FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLING_MODE_STATE_CHANGED, statsmode);
+ mFrameworkStatsLogger.deviceIdlingModeStateChanged(statsmode);
}
if (mDeviceIdling != nowIdling) {
mDeviceIdling = nowIdling;
@@ -5769,7 +5815,7 @@
mDeviceIdleModeFullTimer.startRunningLocked(elapsedRealtimeMs);
}
mDeviceIdleMode = mode;
- FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mode);
+ mFrameworkStatsLogger.deviceIdleModeStateChanged(mode);
}
}
@@ -5933,8 +5979,7 @@
addStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG;
newHistory = true;
mPhoneSignalScanningTimer.startRunningLocked(elapsedRealtimeMs);
- FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
- simState, strengthBin);
+ mFrameworkStatsLogger.phoneServiceStateChanged(state, simState, strengthBin);
}
}
@@ -5944,8 +5989,7 @@
removeStateFlag = HistoryItem.STATE_PHONE_SCANNING_FLAG;
newHistory = true;
mPhoneSignalScanningTimer.stopRunningLocked(elapsedRealtimeMs);
- FrameworkStatsLog.write(FrameworkStatsLog.PHONE_SERVICE_STATE_CHANGED, state,
- simState, strengthBin);
+ mFrameworkStatsLogger.phoneServiceStateChanged(state, simState, strengthBin);
}
}
@@ -5966,8 +6010,7 @@
}
newSignalStrength = strengthBin;
newHistory = true;
- FrameworkStatsLog.write(
- FrameworkStatsLog.PHONE_SIGNAL_STRENGTH_CHANGED, strengthBin);
+ mFrameworkStatsLogger.phoneSignalStrengthChanged(strengthBin);
} else {
stopAllPhoneSignalStrengthTimersLocked(-1, elapsedRealtimeMs);
}
@@ -6076,7 +6119,7 @@
// Unknown is included in DATA_CONNECTION_OTHER.
int bin = DATA_CONNECTION_OUT_OF_SERVICE;
if (hasData) {
- if (dataType > 0 && dataType <= TelephonyManager.getAllNetworkTypes().length) {
+ if (dataType > 0 && dataType <= NUM_ALL_NETWORK_TYPES) {
bin = dataType;
} else {
switch (serviceType) {
@@ -6995,7 +7038,7 @@
/** @hide */
public void noteNetworkInterfaceForTransports(String iface, int[] transportTypes) {
if (TextUtils.isEmpty(iface)) return;
- final int displayTransport = NetworkCapabilitiesUtils.getDisplayTransport(transportTypes);
+ final int displayTransport = getDisplayTransport(transportTypes);
synchronized (mModemNetworkLock) {
if (displayTransport == TRANSPORT_CELLULAR) {
@@ -10507,8 +10550,7 @@
long elapsedRealtimeMs, long uptimeMs) {
int uidRunningState;
// Make special note of Foreground Services
- final boolean userAwareService =
- (ActivityManager.isForegroundService(procState));
+ final boolean userAwareService = ActivityManager.isForegroundService(procState);
uidRunningState = BatteryStats.mapToInternalProcessState(procState);
if (mProcessState == uidRunningState && userAwareService == mInForegroundService) {
@@ -10912,6 +10954,7 @@
mPowerProfile = powerProfile;
mCpuScalingPolicies = cpuScalingPolicies;
mPowerStatsUidResolver = powerStatsUidResolver;
+ mFrameworkStatsLogger = new FrameworkStatsLogger();
initPowerProfile();
@@ -10966,7 +11009,7 @@
// Notify statsd that the system is initially not in doze.
mDeviceIdleMode = DEVICE_IDLE_MODE_OFF;
- FrameworkStatsLog.write(FrameworkStatsLog.DEVICE_IDLE_MODE_STATE_CHANGED, mDeviceIdleMode);
+ mFrameworkStatsLogger.deviceIdleModeStateChanged(mDeviceIdleMode);
}
private void recordPowerStats(PowerStats stats) {
@@ -11702,7 +11745,9 @@
mWakeupReasonStats.clear();
}
- mTmpRailStats.reset();
+ if (mTmpRailStats != null) {
+ mTmpRailStats.reset();
+ }
EnergyConsumerStats.resetIfNotNull(mGlobalEnergyConsumerStats);
@@ -11864,6 +11909,78 @@
return networkStatsManager.getWifiUidStats();
}
+ private static class NetworkStatsDelta {
+ int mUid;
+ int mSet;
+ long mRxBytes;
+ long mRxPackets;
+ long mTxBytes;
+ long mTxPackets;
+
+ public int getUid() {
+ return mUid;
+ }
+
+
+ public int getSet() {
+ return mSet;
+ }
+
+ public long getRxBytes() {
+ return mRxBytes;
+ }
+
+ public long getRxPackets() {
+ return mRxPackets;
+ }
+
+ public long getTxBytes() {
+ return mTxBytes;
+ }
+
+ public long getTxPackets() {
+ return mTxPackets;
+ }
+ }
+
+ private List<NetworkStatsDelta> computeDelta(NetworkStats currentStats,
+ NetworkStats lastStats) {
+ List<NetworkStatsDelta> deltaList = new ArrayList<>();
+ for (NetworkStats.Entry entry : currentStats) {
+ NetworkStatsDelta delta = new NetworkStatsDelta();
+ delta.mUid = entry.getUid();
+ delta.mSet = entry.getSet();
+ NetworkStats.Entry lastEntry = null;
+ if (lastStats != null) {
+ for (NetworkStats.Entry e : lastStats) {
+ if (e.getUid() == entry.getUid() && e.getSet() == entry.getSet()
+ && e.getTag() == entry.getTag()
+ && e.getMetered() == entry.getMetered()
+ && e.getRoaming() == entry.getRoaming()
+ && e.getDefaultNetwork() == entry.getDefaultNetwork()
+ /*&& Objects.equals(e.getIface(), entry.getIface())*/) {
+ lastEntry = e;
+ break;
+ }
+ }
+ }
+ if (lastEntry != null) {
+ delta.mRxBytes = entry.getRxBytes() - lastEntry.getRxBytes();
+ delta.mRxPackets = entry.getRxPackets() - lastEntry.getRxPackets();
+ delta.mTxBytes = entry.getTxBytes() - lastEntry.getTxBytes();
+ delta.mTxPackets = entry.getTxPackets() - lastEntry.getTxPackets();
+ } else {
+ delta.mRxBytes = entry.getRxBytes();
+ delta.mRxPackets = entry.getRxPackets();
+ delta.mTxBytes = entry.getTxBytes();
+ delta.mTxPackets = entry.getTxPackets();
+ }
+ deltaList.add(delta);
+ }
+
+ return deltaList;
+ }
+
/**
* Distribute WiFi energy info and network traffic to apps.
* @param info The energy information from the WiFi controller.
@@ -11879,14 +11996,14 @@
}
// Grab a separate lock to acquire the network stats, which may do I/O.
- NetworkStats delta = null;
+ List<NetworkStatsDelta> delta;
synchronized (mWifiNetworkLock) {
final NetworkStats latestStats = readWifiNetworkStatsLocked(networkStatsManager);
if (latestStats != null) {
- delta = mLastWifiNetworkStats != null
- ? latestStats.subtract(mLastWifiNetworkStats)
- : latestStats.subtract(new NetworkStats(0, -1));
+ delta = computeDelta(latestStats, mLastWifiNetworkStats);
mLastWifiNetworkStats = latestStats;
+ } else {
+ delta = null;
}
}
@@ -11914,7 +12031,7 @@
long totalTxPackets = 0;
long totalRxPackets = 0;
if (delta != null) {
- for (NetworkStats.Entry entry : delta) {
+ for (NetworkStatsDelta entry : delta) {
if (DEBUG_ENERGY) {
Slog.d(TAG, "Wifi uid " + entry.getUid()
+ ": delta rx=" + entry.getRxBytes()
@@ -12199,13 +12316,16 @@
}
// Converting uWs to mAms.
// Conversion: (uWs * (1000ms / 1s) * (1mW / 1000uW)) / mV = mAms
- long monitoredRailChargeConsumedMaMs =
- (long) (mTmpRailStats.getWifiTotalEnergyUseduWs() / opVolt);
+ long monitoredRailChargeConsumedMaMs = mTmpRailStats != null
+ ? (long) (mTmpRailStats.getWifiTotalEnergyUseduWs() / opVolt)
+ : 0L;
mWifiActivity.getMonitoredRailChargeConsumedMaMs().addCountLocked(
monitoredRailChargeConsumedMaMs);
mHistory.recordWifiConsumedCharge(elapsedRealtimeMs, uptimeMs,
(monitoredRailChargeConsumedMaMs / MILLISECONDS_IN_HOUR));
- mTmpRailStats.resetWifiTotalEnergyUsed();
+ if (mTmpRailStats != null) {
+ mTmpRailStats.resetWifiTotalEnergyUsed();
+ }
if (uidEstimatedConsumptionMah != null) {
totalEstimatedConsumptionMah = Math.max(controllerMaMs / MILLISECONDS_IN_HOUR,
@@ -13519,14 +13639,16 @@
}
}
- // Record whether we've seen a non-zero time (for debugging b/22716723).
- if (wakelockStats.isEmpty()) {
- Slog.wtf(TAG, "All kernel wakelocks had time of zero");
- }
+ if (DEBUG) {
+ // Record whether we've seen a non-zero time (for debugging b/22716723).
+ if (wakelockStats.isEmpty()) {
+ Slog.wtf(TAG, "All kernel wakelocks had time of zero");
+ }
- if (numWakelocksSetStale == mKernelWakelockStats.size()) {
- Slog.wtf(TAG, "All kernel wakelocks were set stale. new version=" +
- wakelockStats.kernelWakelockVersion);
+ if (numWakelocksSetStale == mKernelWakelockStats.size()) {
+ Slog.wtf(TAG, "All kernel wakelocks were set stale. new version="
+ + wakelockStats.kernelWakelockVersion);
+ }
}
}
@@ -14711,13 +14833,13 @@
// Inform StatsLog of setBatteryState changes.
private void reportChangesToStatsLog(final int status, final int plugType, final int level) {
if (!mHaveBatteryLevel || mBatteryStatus != status) {
- FrameworkStatsLog.write(FrameworkStatsLog.CHARGING_STATE_CHANGED, status);
+ mFrameworkStatsLogger.chargingStateChanged(status);
}
if (!mHaveBatteryLevel || mBatteryPlugType != plugType) {
- FrameworkStatsLog.write(FrameworkStatsLog.PLUGGED_STATE_CHANGED, plugType);
+ mFrameworkStatsLogger.pluggedStateChanged(plugType);
}
if (!mHaveBatteryLevel || mBatteryLevel != level) {
- FrameworkStatsLog.write(FrameworkStatsLog.BATTERY_LEVEL_CHANGED, level);
+ mFrameworkStatsLogger.batteryLevelChanged(level);
}
}
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
index aba8e5f..1050e8a 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
@@ -32,6 +32,7 @@
import java.util.ArrayList;
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class MobileRadioPowerCalculator extends PowerCalculator {
private static final String TAG = "MobRadioPowerCalculator";
private static final boolean DEBUG = PowerCalculator.DEBUG;
@@ -320,7 +321,7 @@
private double calculateActiveModemPowerMah(BatteryStats bs, long elapsedRealtimeUs) {
final long elapsedRealtimeMs = elapsedRealtimeUs / 1000;
- final int txLvlCount = CellSignalStrength.getNumSignalStrengthLevels();
+ final int txLvlCount = NUM_SIGNAL_STRENGTH_LEVELS;
double consumptionMah = 0.0;
if (DEBUG) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 8773366..640c9dc 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1503,7 +1503,8 @@
a.configChanges = 0xffffffff;
if (homePanelDream()) {
- a.launchMode = ActivityInfo.LAUNCH_SINGLE_TASK;
+ a.launchMode = ActivityInfo.LAUNCH_MULTIPLE;
+ a.documentLaunchMode = ActivityInfo.DOCUMENT_LAUNCH_ALWAYS;
} else {
a.resizeMode = RESIZE_MODE_UNRESIZEABLE;
a.launchMode = ActivityInfo.LAUNCH_SINGLE_INSTANCE;
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 6a3cf43..a3e2869 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -16,6 +16,9 @@
package com.android.server.wm;
+import static android.view.View.DRAG_FLAG_GLOBAL;
+import static android.view.View.DRAG_FLAG_GLOBAL_SAME_APPLICATION;
+
import static com.android.input.flags.Flags.enablePointerChoreographer;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
@@ -30,15 +33,20 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
import android.util.Slog;
import android.view.Display;
+import android.view.DragEvent;
import android.view.IWindow;
import android.view.InputDevice;
import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
+import android.window.IUnhandledDragCallback;
+import android.window.IUnhandledDragListener;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.WindowManagerInternal.IDragDropCallback;
import java.util.Objects;
@@ -59,6 +67,7 @@
static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 1;
static final int MSG_ANIMATION_END = 2;
static final int MSG_REMOVE_DRAG_SURFACE_TIMEOUT = 3;
+ static final int MSG_UNHANDLED_DROP_LISTENER_TIMEOUT = 4;
/**
* Drag state per operation.
@@ -72,6 +81,21 @@
private WindowManagerService mService;
private final Handler mHandler;
+ // The unhandled drag listener for handling cross-window drags that end with no target window
+ private IUnhandledDragListener mUnhandledDragListener;
+ private final IBinder.DeathRecipient mUnhandledDragListenerDeathRecipient =
+ new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (mService.mGlobalLock) {
+ if (hasPendingUnhandledDropCallback()) {
+ onUnhandledDropCallback(false /* consumedByListeners */);
+ }
+ setUnhandledDragListener(null);
+ }
+ }
+ };
+
/**
* Callback which is used to sync drag state with the vendor-specific code.
*/
@@ -83,10 +107,16 @@
mHandler = new DragHandler(service, looper);
}
+ @VisibleForTesting
+ Handler getHandler() {
+ return mHandler;
+ }
+
boolean dragDropActiveLocked() {
return mDragState != null && !mDragState.isClosing();
}
+ @VisibleForTesting
boolean dragSurfaceRelinquishedToDropTarget() {
return mDragState != null && mDragState.mRelinquishDragSurfaceToDropTarget;
}
@@ -96,6 +126,32 @@
mCallback.set(callback);
}
+ /**
+ * Sets the listener for unhandled cross-window drags.
+ */
+ public void setUnhandledDragListener(IUnhandledDragListener listener) {
+ if (mUnhandledDragListener != null && mUnhandledDragListener.asBinder() != null) {
+ mUnhandledDragListener.asBinder().unlinkToDeath(
+ mUnhandledDragListenerDeathRecipient, 0);
+ }
+ mUnhandledDragListener = listener;
+ if (listener != null && listener.asBinder() != null) {
+ try {
+ mUnhandledDragListener.asBinder().linkToDeath(
+ mUnhandledDragListenerDeathRecipient, 0);
+ } catch (RemoteException e) {
+ mUnhandledDragListener = null;
+ }
+ }
+ }
+
+ /**
+ * Returns whether there is an unhandled drag listener set.
+ */
+ boolean hasUnhandledDragListener() {
+ return mUnhandledDragListener != null;
+ }
+
void sendDragStartedIfNeededLocked(WindowState window) {
mDragState.sendDragStartedIfNeededLocked(window);
}
@@ -247,6 +303,10 @@
}
}
+ /**
+ * This is called from the drop target window that received ACTION_DROP
+ * (see DragState#reportDropWindowLock()).
+ */
void reportDropResult(IWindow window, boolean consumed) {
IBinder token = window.asBinder();
if (DEBUG_DRAG) {
@@ -273,22 +333,89 @@
// so be sure to halt the timeout even if the later WindowState
// lookup fails.
mHandler.removeMessages(MSG_DRAG_END_TIMEOUT, window.asBinder());
+
WindowState callingWin = mService.windowForClientLocked(null, window, false);
if (callingWin == null) {
Slog.w(TAG_WM, "Bad result-reporting window " + window);
return; // !!! TODO: throw here?
}
- mDragState.mDragResult = consumed;
- mDragState.mRelinquishDragSurfaceToDropTarget = consumed
- && mDragState.targetInterceptsGlobalDrag(callingWin);
- mDragState.endDragLocked();
+ // If the drop was not consumed by the target window, then check if it should be
+ // consumed by the system unhandled drag listener
+ if (!consumed && notifyUnhandledDrop(mDragState.mUnhandledDropEvent,
+ "window-drop")) {
+ // If the unhandled drag listener is notified, then defer ending the drag until
+ // the listener calls back
+ return;
+ }
+
+ final boolean relinquishDragSurfaceToDropTarget =
+ consumed && mDragState.targetInterceptsGlobalDrag(callingWin);
+ mDragState.endDragLocked(consumed, relinquishDragSurfaceToDropTarget);
}
} finally {
mCallback.get().postReportDropResult();
}
}
+ /**
+ * Notifies the unhandled drag listener if needed.
+ * @return whether the listener was notified and subsequent drag completion should be deferred
+ * until the listener calls back
+ */
+ boolean notifyUnhandledDrop(DragEvent dropEvent, String reason) {
+ final boolean isLocalDrag =
+ (mDragState.mFlags & (DRAG_FLAG_GLOBAL_SAME_APPLICATION | DRAG_FLAG_GLOBAL)) == 0;
+ if (!com.android.window.flags.Flags.delegateUnhandledDrags()
+ || mUnhandledDragListener == null
+ || isLocalDrag) {
+ // Skip if the flag is disabled, there is no unhandled-drag listener, or if this is a
+ // purely local drag
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Skipping unhandled listener "
+ + "(listener=" + mUnhandledDragListener + ", flags=" + mDragState.mFlags + ")");
+ return false;
+ }
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to unhandled listener (" + reason + ")");
+ try {
+ // Schedule timeout for the unhandled drag listener to call back
+ sendTimeoutMessage(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null, DRAG_TIMEOUT_MS);
+ mUnhandledDragListener.onUnhandledDrop(dropEvent, new IUnhandledDragCallback.Stub() {
+ @Override
+ public void notifyUnhandledDropComplete(boolean consumedByListener) {
+ if (DEBUG_DRAG) Slog.d(TAG_WM, "Unhandled listener finished handling DROP");
+ synchronized (mService.mGlobalLock) {
+ onUnhandledDropCallback(consumedByListener);
+ }
+ }
+ });
+ return true;
+ } catch (RemoteException e) {
+ Slog.e(TAG_WM, "Failed to call unhandled drag listener", e);
+ return false;
+ }
+ }
+
+ /**
+ * Called when the unhandled drag listener has completed handling the drop
+ * (if it was notififed).
+ */
+ @VisibleForTesting
+ void onUnhandledDropCallback(boolean consumedByListener) {
+ mHandler.removeMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT, null);
+ // If handled, then the listeners assume responsibility of cleaning up the drag surface
+ mDragState.mDragResult = consumedByListener;
+ mDragState.mRelinquishDragSurfaceToDropTarget = consumedByListener;
+ mDragState.closeLocked();
+ }
+
+ /**
+ * Returns whether we are currently waiting for the unhandled drag listener to callback after
+ * it was notified of an unhandled drag.
+ */
+ boolean hasPendingUnhandledDropCallback() {
+ return mHandler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT);
+ }
+
void cancelDragAndDrop(IBinder dragToken, boolean skipAnimation) {
if (DEBUG_DRAG) {
Slog.d(TAG_WM, "cancelDragAndDrop");
@@ -436,8 +563,8 @@
synchronized (mService.mGlobalLock) {
// !!! TODO: ANR the drag-receiving app
if (mDragState != null) {
- mDragState.mDragResult = false;
- mDragState.endDragLocked();
+ mDragState.endDragLocked(false /* consumed */,
+ false /* relinquishDragSurfaceToDropTarget */);
}
}
break;
@@ -473,6 +600,13 @@
}
break;
}
+
+ case MSG_UNHANDLED_DROP_LISTENER_TIMEOUT: {
+ synchronized (mService.mGlobalLock) {
+ onUnhandledDropCallback(false /* consumedByListener */);
+ }
+ break;
+ }
}
}
}
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index d302f06..76038b9 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -147,6 +147,11 @@
*/
private boolean mIsClosing;
+ // Stores the last drop event which was reported to a valid drop target window, or null
+ // otherwise. This drop event will contain private info and should only be consumed by the
+ // unhandled drag listener.
+ DragEvent mUnhandledDropEvent;
+
DragState(WindowManagerService service, DragDropController controller, IBinder token,
SurfaceControl surface, int flags, IBinder localWin) {
mService = service;
@@ -287,14 +292,54 @@
mData = null;
mThumbOffsetX = mThumbOffsetY = 0;
mNotifiedWindows = null;
+ if (mUnhandledDropEvent != null) {
+ mUnhandledDropEvent.recycle();
+ mUnhandledDropEvent = null;
+ }
// Notifies the controller that the drag state is closed.
mDragDropController.onDragStateClosedLocked(this);
}
/**
+ * Creates the drop event for this drag gesture. If `touchedWin` is null, then the drop event
+ * will be created for dispatching to the unhandled drag and the drag surface will be provided
+ * as a part of the dispatched event.
+ */
+ private DragEvent createDropEvent(float x, float y, @Nullable WindowState touchedWin,
+ boolean includeDragSurface) {
+ if (touchedWin != null) {
+ final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
+ final DragAndDropPermissionsHandler dragAndDropPermissions;
+ if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0
+ && mData != null) {
+ dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock,
+ mData,
+ mUid,
+ touchedWin.getOwningPackage(),
+ mFlags & DRAG_FLAGS_URI_PERMISSIONS,
+ mSourceUserId,
+ targetUserId);
+ } else {
+ dragAndDropPermissions = null;
+ }
+ if (mSourceUserId != targetUserId) {
+ if (mData != null) {
+ mData.fixUris(mSourceUserId);
+ }
+ }
+ return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+ targetInterceptsGlobalDrag(touchedWin), dragAndDropPermissions);
+ } else {
+ return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mData,
+ includeDragSurface /* includeDragSurface */, null /* dragAndDropPermissions */);
+ }
+ }
+
+ /**
* Notify the drop target and tells it about the data. If the drop event is not sent to the
- * target, invokes {@code endDragLocked} immediately.
+ * target, invokes {@code endDragLocked} after the unhandled drag listener gets a chance to
+ * handle the drop.
*/
boolean reportDropWindowLock(IBinder token, float x, float y) {
if (mAnimator != null) {
@@ -302,41 +347,27 @@
}
final WindowState touchedWin = mService.mInputToWindowMap.get(token);
+ final DragEvent unhandledDropEvent = createDropEvent(x, y, null /* touchedWin */,
+ true /* includePrivateInfo */);
if (!isWindowNotified(touchedWin)) {
- // "drop" outside a valid window -- no recipient to apply a
- // timeout to, and we can send the drag-ended message immediately.
- mDragResult = false;
- endDragLocked();
+ // Delegate to the unhandled drag listener as a first pass
+ if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) {
+ // The unhandled drag listener will call back to notify whether it has consumed
+ // the drag, so return here
+ return true;
+ }
+
+ // "drop" outside a valid window -- no recipient to apply a timeout to, and we can send
+ // the drag-ended message immediately.
+ endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */);
if (DEBUG_DRAG) Slog.d(TAG_WM, "Drop outside a valid window " + touchedWin);
return false;
}
if (DEBUG_DRAG) Slog.d(TAG_WM, "sending DROP to " + touchedWin);
- final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
-
- final DragAndDropPermissionsHandler dragAndDropPermissions;
- if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0
- && mData != null) {
- dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock,
- mData,
- mUid,
- touchedWin.getOwningPackage(),
- mFlags & DRAG_FLAGS_URI_PERMISSIONS,
- mSourceUserId,
- targetUserId);
- } else {
- dragAndDropPermissions = null;
- }
- if (mSourceUserId != targetUserId) {
- if (mData != null) {
- mData.fixUris(mSourceUserId);
- }
- }
final IBinder clientToken = touchedWin.mClient.asBinder();
- final DragEvent event = obtainDragEvent(DragEvent.ACTION_DROP, x, y,
- mData, targetInterceptsGlobalDrag(touchedWin),
- dragAndDropPermissions);
+ final DragEvent event = createDropEvent(x, y, touchedWin, false /* includePrivateInfo */);
try {
touchedWin.mClient.dispatchDragEvent(event);
@@ -345,7 +376,7 @@
DragDropController.DRAG_TIMEOUT_MS);
} catch (RemoteException e) {
Slog.w(TAG_WM, "can't send drop notification to win " + touchedWin);
- endDragLocked();
+ endDragLocked(false /* consumed */, false /* relinquishDragSurfaceToDropTarget */);
return false;
} finally {
if (MY_PID != touchedWin.mSession.mPid) {
@@ -353,6 +384,7 @@
}
}
mToken = clientToken;
+ mUnhandledDropEvent = unhandledDropEvent;
return true;
}
@@ -478,6 +510,9 @@
boolean containsAppExtras) {
final boolean interceptsGlobalDrag = targetInterceptsGlobalDrag(newWin);
if (mDragInProgress && isValidDropTarget(newWin, containsAppExtras, interceptsGlobalDrag)) {
+ if (DEBUG_DRAG) {
+ Slog.d(TAG_WM, "Sending DRAG_STARTED to new window " + newWin);
+ }
// Only allow the extras to be dispatched to a global-intercepting drag target
ClipData data = interceptsGlobalDrag ? mData.copyForTransferWithActivityInfo() : null;
DragEvent event = obtainDragEvent(DragEvent.ACTION_DRAG_STARTED,
@@ -523,14 +558,25 @@
return false;
}
if (!targetWin.isPotentialDragTarget(interceptsGlobalDrag)) {
+ // Window should not be a target
return false;
}
- if ((mFlags & View.DRAG_FLAG_GLOBAL) == 0 || !targetWindowSupportsGlobalDrag(targetWin)) {
+ final boolean isGlobalSameAppDrag = (mFlags & View.DRAG_FLAG_GLOBAL_SAME_APPLICATION) != 0;
+ final boolean isGlobalDrag = (mFlags & View.DRAG_FLAG_GLOBAL) != 0;
+ final boolean isAnyGlobalDrag = isGlobalDrag || isGlobalSameAppDrag;
+ if (!isAnyGlobalDrag || !targetWindowSupportsGlobalDrag(targetWin)) {
// Drag is limited to the current window.
if (!isLocalWindow) {
return false;
}
}
+ if (isGlobalSameAppDrag) {
+ // Drag is limited to app windows from the same uid or windows that can intercept global
+ // drag
+ if (!interceptsGlobalDrag && mUid != targetWin.getUid()) {
+ return false;
+ }
+ }
return interceptsGlobalDrag
|| mCrossProfileCopyAllowed
@@ -547,7 +593,10 @@
/**
* @return whether the given window {@param targetWin} can intercept global drags.
*/
- public boolean targetInterceptsGlobalDrag(WindowState targetWin) {
+ public boolean targetInterceptsGlobalDrag(@Nullable WindowState targetWin) {
+ if (targetWin == null) {
+ return false;
+ }
return (targetWin.mAttrs.privateFlags & PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP) != 0;
}
@@ -561,9 +610,6 @@
if (isWindowNotified(newWin)) {
return;
}
- if (DEBUG_DRAG) {
- Slog.d(TAG_WM, "need to send DRAG_STARTED to new window " + newWin);
- }
sendDragStartedLocked(newWin, mCurrentX, mCurrentY,
containsApplicationExtras(mDataDescription));
}
@@ -578,7 +624,13 @@
return false;
}
- void endDragLocked() {
+ /**
+ * Ends the current drag, animating the drag surface back to the source if the drop was not
+ * consumed by the receiving window.
+ */
+ void endDragLocked(boolean dropConsumed, boolean relinquishDragSurfaceToDropTarget) {
+ mDragResult = dropConsumed;
+ mRelinquishDragSurfaceToDropTarget = relinquishDragSurfaceToDropTarget;
if (mAnimator != null) {
return;
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 5b51776..2bee095 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4386,10 +4386,12 @@
void setHasBeenVisible(boolean hasBeenVisible) {
mHasBeenVisible = hasBeenVisible;
- if (!hasBeenVisible || mDeferTaskAppear) {
+ if (!hasBeenVisible) {
return;
}
- sendTaskAppeared();
+ if (!mDeferTaskAppear) {
+ sendTaskAppeared();
+ }
for (WindowContainer<?> parent = getParent(); parent != null; parent = parent.getParent()) {
final Task parentTask = parent.asTask();
if (parentTask == null) {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 2accf9a..5d9c42d 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -44,6 +44,7 @@
import static android.view.WindowManager.transitTypeToString;
import static android.window.TaskFragmentAnimationParams.DEFAULT_ANIMATION_BACKGROUND_COLOR;
import static android.window.TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION;
+import static android.window.TransitionInfo.FLAG_CONFIG_AT_END;
import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS;
import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
@@ -65,6 +66,7 @@
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
+import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -307,6 +309,12 @@
*/
int mAnimationTrack = 0;
+ /**
+ * List of activities whose configurations are sent to the client at the end of the transition
+ * instead of immediately when the configuration changes.
+ */
+ ArrayList<ActivityRecord> mConfigAtEndActivities = null;
+
Transition(@TransitionType int type, @TransitionFlags int flags,
TransitionController controller, BLASTSyncEngine syncEngine) {
mType = type;
@@ -484,6 +492,22 @@
return mTargetDisplays.contains(dc);
}
+ void setConfigAtEnd(@NonNull WindowContainer<?> wc) {
+ wc.forAllActivities(ar -> {
+ if (!ar.isVisible() || !ar.isVisibleRequested()) return;
+ if (mConfigAtEndActivities == null) {
+ mConfigAtEndActivities = new ArrayList<>();
+ }
+ if (mConfigAtEndActivities.contains(ar)) {
+ return;
+ }
+ mConfigAtEndActivities.add(ar);
+ ar.pauseConfigurationDispatch();
+ });
+ snapshotStartState(wc);
+ mChanges.get(wc).mFlags |= ChangeInfo.FLAG_CHANGE_CONFIG_AT_END;
+ }
+
/** Set a transition to be a seamless-rotation. */
void setSeamlessRotation(@NonNull WindowContainer wc) {
final ChangeInfo info = mChanges.get(wc);
@@ -644,20 +668,8 @@
}
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, "Collecting in transition %d: %s",
mSyncId, wc);
- // "snapshot" all parents (as potential promotion targets). Do this before checking
- // if this is already a participant in case it has since been re-parented.
- for (WindowContainer<?> curr = getAnimatableParent(wc);
- curr != null && !mChanges.containsKey(curr);
- curr = getAnimatableParent(curr)) {
- final ChangeInfo info = new ChangeInfo(curr);
- updateTransientFlags(info);
- mChanges.put(curr, info);
- if (isReadyGroup(curr)) {
- mReadyTrackerOld.addGroup(curr);
- ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for"
- + " Transition %d with root=%s", mSyncId, curr);
- }
- }
+ // Snapshot before checking if this is a participant in case it has been re-parented.
+ snapshotStartState(getAnimatableParent(wc));
if (mParticipants.contains(wc)) return;
// Transient-hide may be hidden later, so no need to request redraw.
if (!isInTransientHide(wc)) {
@@ -688,6 +700,22 @@
}
}
+ /** "snapshot" `wc` and all its parents (as potential promotion targets). */
+ private void snapshotStartState(@NonNull WindowContainer<?> wc) {
+ for (WindowContainer<?> curr = wc;
+ curr != null && !mChanges.containsKey(curr);
+ curr = getAnimatableParent(curr)) {
+ final ChangeInfo info = new ChangeInfo(curr);
+ updateTransientFlags(info);
+ mChanges.put(curr, info);
+ if (isReadyGroup(curr)) {
+ mReadyTrackerOld.addGroup(curr);
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS, " Creating Ready-group for"
+ + " Transition %d with root=%s", mSyncId, curr);
+ }
+ }
+ }
+
private void updateTransientFlags(@NonNull ChangeInfo info) {
final WindowContainer<?> wc = info.mContainer;
// Only look at tasks, taskfragments, or activities
@@ -934,47 +962,60 @@
}
/**
+ * Populates `t` with instructions to reset surface transform of `change` so it matches
+ * the WM hierarchy. This "undoes" lingering state left by the animation.
+ */
+ private void resetSurfaceTransform(SurfaceControl.Transaction t, WindowContainer target,
+ SurfaceControl targetLeash) {
+ final Point tmpPos = new Point();
+ target.getRelativePosition(tmpPos);
+ t.setPosition(targetLeash, tmpPos.x, tmpPos.y);
+ // No need to clip the display in case seeing the clipped content when during the
+ // display rotation. No need to clip activities because they rely on clipping on
+ // task layers.
+ if (target.asTaskFragment() == null) {
+ t.setCrop(targetLeash, null /* crop */);
+ } else {
+ // Crop to the resolved override bounds.
+ final Rect clipRect = target.getResolvedOverrideBounds();
+ t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height());
+ }
+ t.setMatrix(targetLeash, 1, 0, 0, 1);
+ // The bounds sent to the transition is always a real bounds. This means we lose
+ // information about "null" bounds (inheriting from parent). Core will fix-up
+ // non-organized window surface bounds; however, since Core can't touch organized
+ // surfaces, add the "inherit from parent" restoration here.
+ if (target.isOrganized() && target.matchParentBounds()) {
+ t.setWindowCrop(targetLeash, -1, -1);
+ }
+ }
+
+ /**
* Build a transaction that "resets" all the re-parenting and layer changes. This is
* intended to be applied at the end of the transition but before the finish callback. This
* needs to be passed/applied in shell because until finish is called, shell owns the surfaces.
* Additionally, this gives shell the ability to better deal with merged transitions.
*/
private void buildFinishTransaction(SurfaceControl.Transaction t, TransitionInfo info) {
- final Point tmpPos = new Point();
// usually only size 1
final ArraySet<DisplayContent> displays = new ArraySet<>();
for (int i = mTargets.size() - 1; i >= 0; --i) {
- final WindowContainer target = mTargets.get(i).mContainer;
- if (target.getParent() != null) {
- final SurfaceControl targetLeash = getLeashSurface(target, null /* t */);
- final SurfaceControl origParent = getOrigParentSurface(target);
- // Ensure surfaceControls are re-parented back into the hierarchy.
- t.reparent(targetLeash, origParent);
- t.setLayer(targetLeash, target.getLastLayer());
- target.getRelativePosition(tmpPos);
- t.setPosition(targetLeash, tmpPos.x, tmpPos.y);
- // No need to clip the display in case seeing the clipped content when during the
- // display rotation. No need to clip activities because they rely on clipping on
- // task layers.
- if (target.asTaskFragment() == null) {
- t.setCrop(targetLeash, null /* crop */);
- } else {
- // Crop to the resolved override bounds.
- final Rect clipRect = target.getResolvedOverrideBounds();
- t.setWindowCrop(targetLeash, clipRect.width(), clipRect.height());
- }
- t.setCornerRadius(targetLeash, 0);
- t.setShadowRadius(targetLeash, 0);
- t.setMatrix(targetLeash, 1, 0, 0, 1);
- t.setAlpha(targetLeash, 1);
- // The bounds sent to the transition is always a real bounds. This means we lose
- // information about "null" bounds (inheriting from parent). Core will fix-up
- // non-organized window surface bounds; however, since Core can't touch organized
- // surfaces, add the "inherit from parent" restoration here.
- if (target.isOrganized() && target.matchParentBounds()) {
- t.setWindowCrop(targetLeash, -1, -1);
- }
- displays.add(target.getDisplayContent());
+ final WindowContainer<?> target = mTargets.get(i).mContainer;
+ if (target.getParent() == null) continue;
+ final SurfaceControl targetLeash = getLeashSurface(target, null /* t */);
+ final SurfaceControl origParent = getOrigParentSurface(target);
+ // Ensure surfaceControls are re-parented back into the hierarchy.
+ t.reparent(targetLeash, origParent);
+ t.setLayer(targetLeash, target.getLastLayer());
+ t.setCornerRadius(targetLeash, 0);
+ t.setShadowRadius(targetLeash, 0);
+ t.setAlpha(targetLeash, 1);
+ displays.add(target.getDisplayContent());
+ // For config-at-end, the end-transform will be reset after the config is actually
+ // applied in the client (since the transform depends on config). The other properties
+ // remain here because shell might want to persistently override them.
+ if ((mTargets.get(i).mFlags & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) == 0) {
+ resetSurfaceTransform(t, target, targetLeash);
}
}
// Remove screenshot layers if necessary
@@ -1304,6 +1345,8 @@
mController.mAtm.mRootWindowContainer.rankTaskLayers();
}
+ commitConfigAtEndActivities();
+
// dispatch legacy callback in a different loop. This is because multiple legacy handlers
// (fixed-rotation/displaycontent) make global changes, so we want to ensure that we've
// processed all the participants first (in particular, we want to trigger pip-enter first)
@@ -1421,6 +1464,52 @@
mController.updateAnimatingState();
}
+ private void commitConfigAtEndActivities() {
+ if (mConfigAtEndActivities == null || mConfigAtEndActivities.isEmpty()) {
+ return;
+ }
+ final SurfaceControl.Transaction t =
+ mController.mAtm.mWindowManager.mTransactionFactory.get();
+ for (int i = 0; i < mTargets.size(); ++i) {
+ final WindowContainer target = mTargets.get(i).mContainer;
+ if (target.getParent() == null || (mTargets.get(i).mFlags
+ & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) == 0) {
+ continue;
+ }
+ final SurfaceControl targetLeash = getLeashSurface(target, null /* t */);
+ // Reset surface state here (since it was skipped in buildFinishTransaction). Since
+ // we are resuming config to the "current" state, we have to calculate the matching
+ // surface state now (rather than snapshotting it at animation start).
+ resetSurfaceTransform(t, target, targetLeash);
+ }
+
+ // Now we resume the configuration dispatch, wait until the now resumed configs have been
+ // drawn, and then apply everything together.
+ final BLASTSyncEngine.SyncGroup sg = mSyncEngine.prepareSyncSet(
+ new BLASTSyncEngine.TransactionReadyListener() {
+ @Override
+ public void onTransactionReady(int mSyncId,
+ SurfaceControl.Transaction transaction) {
+ t.merge(transaction);
+ t.apply();
+ }
+
+ @Override
+ public void onTransactionCommitTimeout() {
+ t.apply();
+ }
+ }, "ConfigAtTransitEnd");
+ final int syncId = sg.mSyncId;
+ mSyncEngine.startSyncSet(sg, BLAST_TIMEOUT_DURATION, true /* parallel */);
+ mSyncEngine.setSyncMethod(syncId, BLASTSyncEngine.METHOD_BLAST);
+ for (int i = 0; i < mConfigAtEndActivities.size(); ++i) {
+ final ActivityRecord ar = mConfigAtEndActivities.get(i);
+ mSyncEngine.addToSyncSet(syncId, ar);
+ ar.resumeConfigurationDispatch();
+ }
+ mSyncEngine.setReady(syncId);
+ }
+
@Nullable
private ActivityRecord getVisibleTransientLaunch(TaskDisplayArea taskDisplayArea) {
if (mTransientLaunches == null) return null;
@@ -1546,6 +1635,12 @@
if (mState == STATE_ABORT) {
mController.onAbort(this);
+ if (mConfigAtEndActivities != null) {
+ for (int i = 0; i < mConfigAtEndActivities.size(); ++i) {
+ mConfigAtEndActivities.get(i).resumeConfigurationDispatch();
+ }
+ mConfigAtEndActivities = null;
+ }
primaryDisplay.getPendingTransaction().merge(transaction);
mSyncId = -1;
mOverrideOptions = null;
@@ -2291,6 +2386,11 @@
} else {
parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_YES_ANIMATION;
}
+ final ActivityRecord ar = targetChange.mContainer.asActivityRecord();
+ if ((ar != null && ar.isConfigurationDispatchPaused())
+ || ((targetChange.mFlags & ChangeInfo.FLAG_CHANGE_CONFIG_AT_END) != 0)) {
+ parentChange.mFlags |= ChangeInfo.FLAG_CHANGE_CONFIG_AT_END;
+ }
}
}
@@ -2940,6 +3040,9 @@
/** Whether this change's container moved to the top. */
private static final int FLAG_CHANGE_MOVED_TO_TOP = 0x20;
+ /** Whether this change contains config-at-end members. */
+ private static final int FLAG_CHANGE_CONFIG_AT_END = 0x40;
+
@IntDef(prefix = { "FLAG_" }, value = {
FLAG_NONE,
FLAG_SEAMLESS_ROTATION,
@@ -2947,7 +3050,8 @@
FLAG_ABOVE_TRANSIENT_LAUNCH,
FLAG_CHANGE_NO_ANIMATION,
FLAG_CHANGE_YES_ANIMATION,
- FLAG_CHANGE_MOVED_TO_TOP
+ FLAG_CHANGE_MOVED_TO_TOP,
+ FLAG_CHANGE_CONFIG_AT_END
})
@Retention(RetentionPolicy.SOURCE)
@interface Flag {}
@@ -3095,6 +3199,9 @@
flags |= FLAG_IS_VOICE_INTERACTION;
}
flags |= record.mTransitionChangeFlags;
+ if (record.isConfigurationDispatchPaused()) {
+ flags |= FLAG_CONFIG_AT_END;
+ }
}
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment != null && task == null) {
@@ -3140,6 +3247,9 @@
if ((mFlags & FLAG_CHANGE_MOVED_TO_TOP) != 0) {
flags |= FLAG_MOVED_TO_TOP;
}
+ if ((mFlags & FLAG_CHANGE_CONFIG_AT_END) != 0) {
+ flags |= FLAG_CONFIG_AT_END;
+ }
return flags;
}
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 80894b2..61fde5e 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -465,7 +465,9 @@
}
}
final InsetsSource source = new InsetsSource(id, provider.getType());
- source.setFrame(provider.getArbitraryRectangle()).updateSideHint(getBounds());
+ source.setFrame(provider.getArbitraryRectangle())
+ .updateSideHint(getBounds())
+ .setBoundingRects(provider.getBoundingRects());
mLocalInsetsSources.put(id, source);
mDisplayContent.getInsetsStateController().updateAboveInsetsState(true);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 4ea76e1..de8d9f9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -310,6 +310,7 @@
import android.window.ISurfaceSyncGroupCompletedListener;
import android.window.ITaskFpsCallback;
import android.window.ITrustedPresentationListener;
+import android.window.IUnhandledDragListener;
import android.window.InputTransferToken;
import android.window.ScreenCapture;
import android.window.SystemPerformanceHinter;
@@ -10026,4 +10027,16 @@
void onProcessActivityVisibilityChanged(int uid, boolean visible) {
mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible);
}
+
+ /**
+ * Sets the listener to be called back when a cross-window drag and drop operation is unhandled
+ * (ie. not handled by any window which can handle the drag).
+ */
+ @Override
+ public void setUnhandledDragListener(IUnhandledDragListener listener) throws RemoteException {
+ mAtmService.enforceTaskPermission("setUnhandledDragListener");
+ synchronized (mGlobalLock) {
+ mDragDropController.setUnhandledDragListener(listener);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 8cd399f..a8de919 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -583,8 +583,21 @@
}
final List<WindowContainerTransaction.HierarchyOp> hops = t.getHierarchyOps();
final int hopSize = hops.size();
- Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries =
- t.getChanges().entrySet().iterator();
+ Iterator<Map.Entry<IBinder, WindowContainerTransaction.Change>> entries;
+ if (transition != null) {
+ // Mark any config-at-end containers before applying config changes so that
+ // the config changes don't dispatch to client.
+ entries = t.getChanges().entrySet().iterator();
+ while (entries.hasNext()) {
+ final Map.Entry<IBinder, WindowContainerTransaction.Change> entry =
+ entries.next();
+ if (!entry.getValue().getConfigAtTransitionEnd()) continue;
+ final WindowContainer wc = WindowContainer.fromBinder(entry.getKey());
+ if (wc == null || !wc.isAttached()) continue;
+ transition.setConfigAtEnd(wc);
+ }
+ }
+ entries = t.getChanges().entrySet().iterator();
while (entries.hasNext()) {
final Map.Entry<IBinder, WindowContainerTransaction.Change> entry = entries.next();
final WindowContainer wc = WindowContainer.fromBinder(entry.getKey());
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index dfa9dce..3607ddd 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -34,6 +34,7 @@
"tvinput/BufferProducerThread.cpp",
"tvinput/JTvInputHal.cpp",
"tvinput/TvInputHal_hidl.cpp",
+ "com_android_server_accessibility_BrailleDisplayConnection.cpp",
"com_android_server_adb_AdbDebuggingManager.cpp",
"com_android_server_am_BatteryStatsService.cpp",
"com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp",
diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS
index df7fb99..b999305f 100644
--- a/services/core/jni/OWNERS
+++ b/services/core/jni/OWNERS
@@ -15,6 +15,7 @@
per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS
per-file com_android_server_Usb* = file:/services/usb/OWNERS
per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS
+per-file com_android_server_accessibility_* = file:/services/accessibility/OWNERS
per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS
per-file com_android_server_lights_* = file:/services/core/java/com/android/server/lights/OWNERS
per-file com_android_server_location_* = file:/location/java/android/location/OWNERS
diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
new file mode 100644
index 0000000..9a509a7
--- /dev/null
+++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <core_jni_helpers.h>
+#include <jni.h>
+#include <linux/hidraw.h>
+#include <linux/input.h>
+#include <nativehelper/JNIHelp.h>
+#include <sys/ioctl.h>
+
+/*
+ * This file defines simple wrappers around the kernel UAPI HIDRAW driver's ioctl() commands.
+ * See kernel example samples/hidraw/hid-example.c
+ *
+ * All methods expect an open file descriptor int from Java.
+ */
+
+namespace android {
+
+namespace {
+
+// Max size we allow for the result from HIDIOCGRAWUNIQ (Bluetooth address or USB serial number).
+// Copied from linux/hid.h struct hid_device->uniq char array size; the ioctl implementation
+// writes at most this many bytes to the provided buffer.
+constexpr int UNIQ_SIZE_MAX = 64;
+
+} // anonymous namespace
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize(
+ JNIEnv* env, jobject thiz, int fd) {
+ int size = 0;
+ if (ioctl(fd, HIDIOCGRDESCSIZE, &size) < 0) {
+ return -1;
+ }
+ return size;
+}
+
+static jbyteArray com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc(
+ JNIEnv* env, jobject thiz, int fd, int descSize) {
+ struct hidraw_report_descriptor desc;
+ desc.size = descSize;
+ if (ioctl(fd, HIDIOCGRDESC, &desc) < 0) {
+ return nullptr;
+ }
+ jbyteArray result = env->NewByteArray(descSize);
+ if (result != nullptr) {
+ env->SetByteArrayRegion(result, 0, descSize, (jbyte*)desc.value);
+ }
+ // Local ref is not deleted because it is returned to Java
+ return result;
+}
+
+static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq(JNIEnv* env,
+ jobject thiz,
+ int fd) {
+ char buf[UNIQ_SIZE_MAX];
+ if (ioctl(fd, HIDIOCGRAWUNIQ(UNIQ_SIZE_MAX), buf) < 0) {
+ return nullptr;
+ }
+ // Local ref is not deleted because it is returned to Java
+ return env->NewStringUTF(buf);
+}
+
+static jint com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType(JNIEnv* env,
+ jobject thiz,
+ int fd) {
+ struct hidraw_devinfo info;
+ if (ioctl(fd, HIDIOCGRAWINFO, &info) < 0) {
+ return -1;
+ }
+ return info.bustype;
+}
+
+static const JNINativeMethod gMethods[] = {
+ {"nativeGetHidrawDescSize", "(I)I",
+ (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize},
+ {"nativeGetHidrawDesc", "(II)[B",
+ (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDesc},
+ {"nativeGetHidrawUniq", "(I)Ljava/lang/String;",
+ (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq},
+ {"nativeGetHidrawBusType", "(I)I",
+ (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType},
+};
+
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env) {
+ return RegisterMethodsOrDie(env, "com/android/server/accessibility/BrailleDisplayConnection",
+ gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp
index 5d1eb49..0936888 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -70,6 +70,7 @@
int register_com_android_server_display_DisplayControl(JNIEnv* env);
int register_com_android_server_SystemClockTime(JNIEnv* env);
int register_android_server_display_smallAreaDetectionController(JNIEnv* env);
+int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env);
};
using namespace android;
@@ -132,5 +133,6 @@
register_com_android_server_display_DisplayControl(env);
register_com_android_server_SystemClockTime(env);
register_android_server_display_smallAreaDetectionController(env);
+ register_com_android_server_accessibility_BrailleDisplayConnection(env);
return JNI_VERSION_1_4;
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 05d1c49..85eac29 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -98,6 +98,7 @@
import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS;
import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_SUSPENSION;
import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
+import static android.app.admin.DeviceAdminInfo.HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
import static android.app.admin.DeviceAdminInfo.USES_POLICY_FORCE_LOCK;
import static android.app.admin.DeviceAdminInfo.USES_POLICY_WIPE_DATA;
import static android.app.admin.DeviceAdminReceiver.ACTION_COMPLIANCE_ACKNOWLEDGEMENT_REQUIRED;
@@ -189,6 +190,7 @@
import static android.app.admin.DevicePolicyManager.STATUS_MANAGED_USERS_NOT_SUPPORTED;
import static android.app.admin.DevicePolicyManager.STATUS_NONSYSTEM_USER_EXISTS;
import static android.app.admin.DevicePolicyManager.STATUS_NOT_SYSTEM_USER;
+import static android.app.admin.DevicePolicyManager.STATUS_HEADLESS_ONLY_SYSTEM_USER;
import static android.app.admin.DevicePolicyManager.STATUS_OK;
import static android.app.admin.DevicePolicyManager.STATUS_PROVISIONING_NOT_ALLOWED_FOR_NON_DEVELOPER_USERS;
import static android.app.admin.DevicePolicyManager.STATUS_SYSTEM_USER;
@@ -225,6 +227,7 @@
import static android.app.admin.ProvisioningException.ERROR_STARTING_PROFILE_FAILED;
import static android.app.admin.flags.Flags.backupServiceSecurityLogEventEnabled;
import static android.app.admin.flags.Flags.dumpsysPolicyEngineMigrationEnabled;
+import static android.app.admin.flags.Flags.headlessDeviceOwnerSingleUserEnabled;
import static android.app.admin.flags.Flags.policyEngineMigrationV2Enabled;
import static android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE;
import static android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE;
@@ -9460,7 +9463,8 @@
}
if (setProfileOwnerOnCurrentUserIfNecessary
- && mInjector.userManagerIsHeadlessSystemUserMode()) {
+ && mInjector.userManagerIsHeadlessSystemUserMode()
+ && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED) {
int currentForegroundUser;
synchronized (getLockObject()) {
currentForegroundUser = getCurrentForegroundUserId();
@@ -9476,6 +9480,12 @@
return true;
}
+ private int getHeadlessDeviceOwnerMode() {
+ synchronized (getLockObject()) {
+ return getDeviceOwnerAdminLocked().info.getHeadlessDeviceOwnerMode();
+ }
+ }
+
/**
* This API is cached: invalidate with invalidateBinderCaches().
*/
@@ -12226,6 +12236,13 @@
+ admin + " are not in the same package");
}
final CallerIdentity caller = getCallerIdentity(admin);
+
+ if (headlessDeviceOwnerSingleUserEnabled()) {
+ // Block this method if the device is in headless main user mode
+ Preconditions.checkCallAuthorization(
+ getHeadlessDeviceOwnerMode() != HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER,
+ "createAndManageUser was called while in headless single user mode");
+ }
// Only allow the system user to use this method
Preconditions.checkCallAuthorization(caller.getUserHandle().isSystem(),
"createAndManageUser was called from non-system user");
@@ -16636,29 +16653,51 @@
return STATUS_USER_NOT_RUNNING;
}
+ DeviceAdminInfo adminInfo = null;
+
+ boolean isHeadlessModeAffiliated = false;
+
+ boolean isHeadlessModeSingleUser = false;
+
boolean isHeadlessSystemUserMode = mInjector.userManagerIsHeadlessSystemUserMode();
+ int ensureSetUpUser = UserHandle.USER_SYSTEM;
if (isHeadlessSystemUserMode) {
- if (deviceOwnerUserId != UserHandle.USER_SYSTEM) {
+ if (owner != null) {
+ adminInfo = findAdmin(owner,
+ deviceOwnerUserId, /* throwForMissingPermission= */ false);
+
+ isHeadlessModeAffiliated =
+ adminInfo.getHeadlessDeviceOwnerMode()
+ == HEADLESS_DEVICE_OWNER_MODE_AFFILIATED;
+
+ isHeadlessModeSingleUser =
+ adminInfo.getHeadlessDeviceOwnerMode()
+ == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER;
+
+ if (!isHeadlessModeAffiliated && !isHeadlessModeSingleUser) {
+ return STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED;
+ }
+
+ if (headlessDeviceOwnerSingleUserEnabled() && isHeadlessModeSingleUser) {
+ ensureSetUpUser = mUserManagerInternal.getMainUserId();
+ if (ensureSetUpUser == UserHandle.USER_NULL) {
+ return STATUS_HEADLESS_ONLY_SYSTEM_USER;
+ }
+ }
+ }
+
+ if (isHeadlessModeAffiliated && deviceOwnerUserId != UserHandle.USER_SYSTEM) {
Slogf.e(LOG_TAG, "In headless system user mode, "
+ "device owner can only be set on headless system user.");
return STATUS_NOT_SYSTEM_USER;
}
- if (owner != null) {
- DeviceAdminInfo adminInfo = findAdmin(
- owner, deviceOwnerUserId, /* throwForMissingPermission= */ false);
-
- if (adminInfo.getHeadlessDeviceOwnerMode()
- != HEADLESS_DEVICE_OWNER_MODE_AFFILIATED) {
- return STATUS_HEADLESS_SYSTEM_USER_MODE_NOT_SUPPORTED;
- }
- }
}
if (isAdb) {
// If shell command runs after user setup completed check device status. Otherwise, OK.
- if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) {
+ if (hasUserSetupCompleted(ensureSetUpUser)) {
// DO can be setup only if there are no users which are neither created by default
// nor marked as FOR_TESTING
@@ -16681,11 +16720,12 @@
return STATUS_OK;
} else {
// DO has to be user 0
- if (deviceOwnerUserId != UserHandle.USER_SYSTEM) {
+ if ((!isHeadlessSystemUserMode || isHeadlessModeAffiliated)
+ && deviceOwnerUserId != UserHandle.USER_SYSTEM) {
return STATUS_NOT_SYSTEM_USER;
}
// Only provision DO before setup wizard completes
- if (hasUserSetupCompleted(UserHandle.USER_SYSTEM)) {
+ if (hasUserSetupCompleted(ensureSetUpUser)) {
return STATUS_USER_SETUP_COMPLETED;
}
return STATUS_OK;
@@ -21260,7 +21300,11 @@
setTimeAndTimezone(provisioningParams.getTimeZone(), provisioningParams.getLocalTime());
setLocale(provisioningParams.getLocale());
- int deviceOwnerUserId = UserHandle.USER_SYSTEM;
+ int deviceOwnerUserId = headlessDeviceOwnerSingleUserEnabled()
+ && getHeadlessDeviceOwnerMode() == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER
+ ? mUserManagerInternal.getMainUserId()
+ : UserHandle.USER_SYSTEM;
+
if (!removeNonRequiredAppsForManagedDevice(
deviceOwnerUserId,
provisioningParams.isLeaveAllSystemAppsEnabled(),
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index 8b22718..bc264a4 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -16,11 +16,12 @@
package com.android.server.policy;
-import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
-import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP;
-import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY;
-import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
-import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
+import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_OVERRIDE_REQUESTS;
+import static android.hardware.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP;
+import static android.hardware.devicestate.DeviceState.FLAG_EMULATED_ONLY;
+import static android.hardware.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
+import static android.hardware.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
+
import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState;
@@ -36,7 +37,6 @@
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
import com.android.server.policy.feature.flags.FeatureFlags;
-import com.android.server.policy.feature.flags.FeatureFlagsImpl;
import java.io.PrintWriter;
import java.util.function.Predicate;
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index 021a667..bf2619b 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -34,6 +34,7 @@
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Looper;
@@ -48,7 +49,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.policy.feature.flags.FeatureFlags;
import com.android.server.policy.feature.flags.FeatureFlagsImpl;
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
index 04cebab..930f4a6 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -51,6 +51,7 @@
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.hardware.display.DisplayManager;
import android.hardware.input.InputSensorInfo;
import android.os.Handler;
@@ -58,7 +59,6 @@
import android.testing.AndroidTestingRunner;
import android.view.Display;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider.Listener;
import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java
index 885ed35..b9f00d7 100644
--- a/services/people/java/com/android/server/people/PeopleService.java
+++ b/services/people/java/com/android/server/people/PeopleService.java
@@ -38,6 +38,7 @@
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -474,6 +475,10 @@
getDataManager().restore(userId, payload);
}
+ @Override
+ public void requestServiceFeatures(AppPredictionSessionId sessionId,
+ IRemoteCallback callback) {}
+
@VisibleForTesting
SessionInfo getSessionInfo(AppPredictionSessionId sessionId) {
return mSessions.get(sessionId);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java
new file mode 100644
index 0000000..3bb6712
--- /dev/null
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeMapTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.ArrayMap;
+import android.view.inputmethod.InputMethodSubtype;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Test;
+
+import java.util.List;
+public final class AdditionalSubtypeMapTest {
+
+ private static final String TEST_IME1_ID = "com.android.test.inputmethod/.TestIme1";
+ private static final String TEST_IME2_ID = "com.android.test.inputmethod/.TestIme2";
+
+ private static InputMethodSubtype createTestSubtype(String locale) {
+ return new InputMethodSubtype
+ .InputMethodSubtypeBuilder()
+ .setSubtypeNameResId(0)
+ .setSubtypeIconResId(0)
+ .setSubtypeLocale(locale)
+ .setIsAsciiCapable(true)
+ .build();
+ }
+
+ private static final InputMethodSubtype TEST_SUBTYPE_EN_US = createTestSubtype("en_US");
+ private static final InputMethodSubtype TEST_SUBTYPE_JA_JP = createTestSubtype("ja_JP");
+
+ private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST1 = List.of(TEST_SUBTYPE_EN_US);
+ private static final List<InputMethodSubtype> TEST_SUBTYPE_LIST2 = List.of(TEST_SUBTYPE_JA_JP);
+
+ private static ArrayMap<String, List<InputMethodSubtype>> mapOf(
+ @NonNull String key1, @NonNull List<InputMethodSubtype> value1) {
+ final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>();
+ map.put(key1, value1);
+ return map;
+ }
+
+ private static ArrayMap<String, List<InputMethodSubtype>> mapOf(
+ @NonNull String key1, @NonNull List<InputMethodSubtype> value1,
+ @NonNull String key2, @NonNull List<InputMethodSubtype> value2) {
+ final ArrayMap<String, List<InputMethodSubtype>> map = new ArrayMap<>();
+ map.put(key1, value1);
+ map.put(key2, value2);
+ return map;
+ }
+
+ @Test
+ public void testOfReturnsEmptyInstance() {
+ assertThat(AdditionalSubtypeMap.of(new ArrayMap<>()))
+ .isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP);
+ }
+
+ @Test
+ public void testOfReturnsNewInstance() {
+ final AdditionalSubtypeMap instance = AdditionalSubtypeMap.of(
+ mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1));
+ assertThat(instance.keySet()).containsExactly(TEST_IME1_ID);
+ assertThat(instance.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1);
+ }
+
+ @Test
+ public void testCloneWithRemoveOrSelfReturnsEmptyInstance() {
+ final AdditionalSubtypeMap original = AdditionalSubtypeMap.of(
+ mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1));
+ final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID);
+ assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP);
+ }
+
+ @Test
+ public void testCloneWithRemoveOrSelfWithMultipleKeysReturnsEmptyInstance() {
+ final AdditionalSubtypeMap original = AdditionalSubtypeMap.of(
+ mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2));
+ final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(
+ List.of(TEST_IME1_ID, TEST_IME2_ID));
+ assertThat(result).isSameInstanceAs(AdditionalSubtypeMap.EMPTY_MAP);
+ }
+
+ @Test
+ public void testCloneWithRemoveOrSelfReturnsNewInstance() {
+ final AdditionalSubtypeMap original = AdditionalSubtypeMap.of(
+ mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2));
+ final AdditionalSubtypeMap result = original.cloneWithRemoveOrSelf(TEST_IME1_ID);
+ assertThat(result.keySet()).containsExactly(TEST_IME2_ID);
+ assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2);
+ }
+
+ @Test
+ public void testCloneWithPutWithNewKey() {
+ final AdditionalSubtypeMap original = AdditionalSubtypeMap.of(
+ mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1));
+ final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST2);
+ assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID);
+ assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1);
+ assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST2);
+ }
+
+ @Test
+ public void testCloneWithPutWithExistingKey() {
+ final AdditionalSubtypeMap original = AdditionalSubtypeMap.of(
+ mapOf(TEST_IME1_ID, TEST_SUBTYPE_LIST1, TEST_IME2_ID, TEST_SUBTYPE_LIST2));
+ final AdditionalSubtypeMap result = original.cloneWithPut(TEST_IME2_ID, TEST_SUBTYPE_LIST1);
+ assertThat(result.keySet()).containsExactly(TEST_IME1_ID, TEST_IME2_ID);
+ assertThat(result.get(TEST_IME1_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1);
+ assertThat(result.get(TEST_IME2_ID)).containsExactlyElementsIn(TEST_SUBTYPE_LIST1);
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
index 0edb3df..63224bb 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
@@ -55,9 +55,9 @@
// Save & load.
AtomicFile atomicFile = new AtomicFile(
new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml"));
- AdditionalSubtypeUtils.saveToFile(allSubtypes, InputMethodMap.of(methodMap), atomicFile);
- ArrayMap<String, List<InputMethodSubtype>> loadedSubtypes = new ArrayMap<>();
- AdditionalSubtypeUtils.loadFromFile(loadedSubtypes, atomicFile);
+ AdditionalSubtypeUtils.saveToFile(AdditionalSubtypeMap.of(allSubtypes),
+ InputMethodMap.of(methodMap), atomicFile);
+ AdditionalSubtypeMap loadedSubtypes = AdditionalSubtypeUtils.loadFromFile(atomicFile);
// Verifies the loaded data.
assertEquals(1, loadedSubtypes.size());
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
index 71752ba..2ea2e22 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
@@ -25,10 +25,8 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.util.ArrayMap;
import android.view.inputmethod.InputMethod;
import android.view.inputmethod.InputMethodInfo;
-import android.view.inputmethod.InputMethodSubtype;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -124,10 +122,8 @@
private List<InputMethodInfo> filterInputMethodServices(List<ResolveInfo> resolveInfoList,
List<String> enabledComponents) {
- final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap =
- new ArrayMap<>();
final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices(
- emptyAdditionalSubtypeMap, enabledComponents, mContext, resolveInfoList);
+ AdditionalSubtypeMap.EMPTY_MAP, enabledComponents, mContext, resolveInfoList);
return methodMap.values();
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
index a8100af..66e0717 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationManagerApiTest.kt
@@ -18,6 +18,10 @@
import android.content.Context
import android.content.Intent
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilterGroup
+import android.content.UriRelativeFilterGroupParcel
+import android.content.pm.Flags
import android.content.pm.PackageManager
import android.content.pm.verify.domain.DomainOwner
import android.content.pm.verify.domain.DomainVerificationInfo
@@ -25,8 +29,10 @@
import android.content.pm.verify.domain.DomainVerificationUserState
import android.content.pm.verify.domain.IDomainVerificationManager
import android.os.Build
-import android.os.PatternMatcher
+import android.os.Bundle
+import android.os.PatternMatcher.PATTERN_LITERAL
import android.os.Process
+import android.platform.test.annotations.RequiresFlagsEnabled
import android.util.ArraySet
import android.util.SparseArray
import com.android.internal.pm.parsing.pkg.AndroidPackageInternal
@@ -68,6 +74,63 @@
private val DOMAIN_4 = "four.$DOMAIN_BASE"
}
+ @RequiresFlagsEnabled(Flags.FLAG_RELATIVE_REFERENCE_INTENT_FILTERS)
+ @Test
+ fun updateUriRelativeFilterGroups() {
+ val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2))
+ val service = makeService(pkgWithDomains).apply {
+ addPackages(pkgWithDomains)
+ }
+
+ val bundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1, DOMAIN_2))
+ assertThat(bundle.keySet()).containsExactlyElementsIn(listOf(DOMAIN_1, DOMAIN_2))
+ assertThat(bundle.getParcelableArrayList(DOMAIN_1, UriRelativeFilterGroup::class.java))
+ .isEmpty()
+ assertThat(bundle.getParcelableArrayList(DOMAIN_2, UriRelativeFilterGroup::class.java))
+ .isEmpty()
+
+ val pathGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
+ pathGroup.addUriRelativeFilter(
+ UriRelativeFilter(UriRelativeFilter.PATH, PATTERN_LITERAL, "path")
+ )
+ val queryGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_BLOCK)
+ queryGroup.addUriRelativeFilter(
+ UriRelativeFilter(UriRelativeFilter.QUERY, PATTERN_LITERAL, "query")
+ )
+ val fragmentGroup = UriRelativeFilterGroup(UriRelativeFilterGroup.ACTION_ALLOW)
+ fragmentGroup.addUriRelativeFilter(
+ UriRelativeFilter(UriRelativeFilter.FRAGMENT, PATTERN_LITERAL, "fragment")
+ )
+
+ assertGroups(service, arrayListOf(pathGroup))
+ assertGroups(service, arrayListOf(queryGroup, pathGroup))
+ assertGroups(service, arrayListOf(queryGroup, fragmentGroup, pathGroup))
+ }
+
+ private fun assertGroups(
+ service: DomainVerificationService,
+ groups: List<UriRelativeFilterGroup>
+ ) {
+ val bundle = Bundle()
+ bundle.putParcelableList(DOMAIN_1, UriRelativeFilterGroup.groupsToParcels(groups))
+ service.setUriRelativeFilterGroups(PKG_ONE, bundle)
+ val fetchedBundle = service.getUriRelativeFilterGroups(PKG_ONE, listOf(DOMAIN_1))
+ assertThat(fetchedBundle.keySet()).containsExactlyElementsIn(bundle.keySet())
+ assertThat(
+ UriRelativeFilterGroup.parcelsToGroups(
+ fetchedBundle.getParcelableArrayList(
+ DOMAIN_1,
+ UriRelativeFilterGroupParcel::class.java)
+ )
+ ).containsExactlyElementsIn(
+ UriRelativeFilterGroup.parcelsToGroups(
+ bundle.getParcelableArrayList(
+ DOMAIN_1,
+ UriRelativeFilterGroupParcel::class.java)
+ )
+ ).inOrder()
+ }
+
@Test
fun queryValidVerificationPackageNames() {
val pkgWithDomains = mockPkgState(PKG_ONE, UUID_ONE, listOf(DOMAIN_1, DOMAIN_2))
@@ -484,6 +547,7 @@
DomainVerificationService(mockThrowOnUnmocked {
// Assume the test has every permission necessary
whenever(enforcePermission(anyString(), anyInt(), anyInt(), anyString()))
+ whenever(enforceCallingOrSelfPermission(anyString(), anyString()))
whenever(checkPermission(anyString(), anyInt(), anyInt())) {
PackageManager.PERMISSION_GRANTED
}
@@ -539,7 +603,7 @@
addCategory(Intent.CATEGORY_DEFAULT)
addDataScheme("http")
addDataScheme("https")
- addDataPath("/sub", PatternMatcher.PATTERN_LITERAL)
+ addDataPath("/sub", PATTERN_LITERAL)
addDataAuthority(it, null)
}
}
diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
index 65b99c5..4fa4190 100644
--- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
+++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/verify/domain/DomainVerificationPersistenceTest.kt
@@ -16,7 +16,12 @@
package com.android.server.pm.test.verify.domain
+import android.content.UriRelativeFilter
+import android.content.UriRelativeFilter.PATH
+import android.content.UriRelativeFilterGroup
+import android.content.UriRelativeFilterGroup.ACTION_ALLOW
import android.content.pm.verify.domain.DomainVerificationState
+import android.os.PatternMatcher.PATTERN_LITERAL
import android.os.UserHandle
import android.util.ArrayMap
import android.util.SparseArray
@@ -157,7 +162,7 @@
@Test
fun writeStateSignatureIfFunctionReturnsNull() {
- val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" }
+ val (attached, pending, restored) = mockWriteValues { "SIGNATURE_$it" }
val file = tempFolder.newFile().writeXml {
DomainVerificationPersistence.writeToXml(it, attached, pending, restored,
UserHandle.USER_ALL) { null }
@@ -313,6 +318,9 @@
addHosts(setOf("$packageName-user.com"))
isLinkHandlingAllowed = true
}
+ val group = UriRelativeFilterGroup(ACTION_ALLOW)
+ group.addUriRelativeFilter(UriRelativeFilter(PATH, PATTERN_LITERAL, "test"))
+ uriRelativeFilterGroupMap.put("example.com", listOf(group))
}
private fun pkgName(id: Int) = "$PKG_PREFIX.pkg$id"
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java
index d781433..9e98105 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java
@@ -31,6 +31,7 @@
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
+import android.provider.Settings;
import android.service.notification.NotificationListenerService.Ranking;
import android.service.notification.NotificationListenerService.RankingMap;
import android.service.notification.StatusBarNotification;
@@ -391,6 +392,16 @@
}
@Test
+ public void mediaProjectionOnStart_disabledViaDevOption_noBlockedPackages() {
+ mockDisabledViaDevelopOption();
+ setupSensitiveNotification();
+
+ mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+
+ verifyZeroInteractions(mWindowManager);
+ }
+
+ @Test
public void nlsOnListenerConnected_projectionNotStarted_noop() {
// Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
// as non-sensitive
@@ -484,6 +495,18 @@
}
@Test
+ public void nlsOnListenerConnected_disabledViaDevOption_noBlockedPackages() {
+ mockDisabledViaDevelopOption();
+ // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
+ // as non-sensitive
+ setupSensitiveNotification();
+ mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+ mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected();
+
+ verifyZeroInteractions(mWindowManager);
+ }
+
+ @Test
public void nlsOnNotificationRankingUpdate_projectionNotStarted_noop() {
// Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
// as non-sensitive
@@ -599,6 +622,19 @@
}
@Test
+ public void nlsOnNotificationRankingUpdate_disabledViaDevOption_noBlockedPackages() {
+ mockDisabledViaDevelopOption();
+ // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
+ // as non-sensitive
+ setupSensitiveNotification();
+ mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+ mSensitiveContentProtectionManagerService.mNotificationListener
+ .onNotificationRankingUpdate(mRankingMap);
+
+ verifyZeroInteractions(mWindowManager);
+ }
+
+ @Test
public void nlsOnNotificationPosted_projectionNotStarted_noop() {
// Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
// as non-sensitive
@@ -697,4 +733,26 @@
verifyZeroInteractions(mWindowManager);
}
+
+ @Test
+ public void nlsOnNotificationPosted_disabledViaDevOption_noBlockedPackages() {
+ mockDisabledViaDevelopOption();
+ // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2
+ // as non-sensitive
+ setupSensitiveNotification();
+ mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class));
+ mSensitiveContentProtectionManagerService.mNotificationListener
+ .onNotificationPosted(mNotification1, mRankingMap);
+
+ verifyZeroInteractions(mWindowManager);
+ }
+
+ private void mockDisabledViaDevelopOption() {
+ // mContext (TestableContext) uses [TestableSettingsProvider] and it will be cleared after
+ // the test
+ Settings.Global.putInt(
+ mContext.getContentResolver(),
+ Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS,
+ 1);
+ }
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 28471b3..6bcd778 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -49,7 +49,6 @@
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONTENT_TRIGGER;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_FLEXIBLE;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_IDLE;
-import static com.android.server.job.controllers.JobStatus.MIN_WINDOW_FOR_FLEXIBILITY_MS;
import static com.android.server.job.controllers.JobStatus.NO_LATEST_RUNTIME;
import static org.junit.Assert.assertArrayEquals;
@@ -410,10 +409,12 @@
@Test
public void testOnConstantsUpdated_PercentsToDropConstraints() {
+ final long fallbackDuration = 12 * HOUR_IN_MILLIS;
JobInfo.Builder jb = createJob(0)
- .setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+ .setOverrideDeadline(HOUR_IN_MILLIS);
JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb);
- assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+ // Even though the override deadline is 1 hour, the fallback duration is still used.
+ assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS,
"500=1|2|3|4"
@@ -441,13 +442,13 @@
mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS
.get(JobInfo.PRIORITY_MIN),
new int[]{54, 55, 56, 57});
- assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10,
+ assertEquals(FROZEN_TIME + fallbackDuration / 10,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
js.setNumDroppedFlexibleConstraints(1);
- assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 2,
+ assertEquals(FROZEN_TIME + fallbackDuration / 10 * 2,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
js.setNumDroppedFlexibleConstraints(2);
- assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 3,
+ assertEquals(FROZEN_TIME + fallbackDuration / 10 * 3,
mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js));
}
@@ -504,37 +505,38 @@
@Test
public void testGetNextConstraintDropTimeElapsedLocked() {
+ final long fallbackDuration = 50 * HOUR_IN_MILLIS;
setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 200 * HOUR_IN_MILLIS);
setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES,
"500=" + HOUR_IN_MILLIS
+ ",400=" + 25 * HOUR_IN_MILLIS
- + ",300=" + 50 * HOUR_IN_MILLIS
+ + ",300=" + fallbackDuration
+ ",200=" + 100 * HOUR_IN_MILLIS
+ ",100=" + 200 * HOUR_IN_MILLIS);
long nextTimeToDropNumConstraints;
// no delay, deadline
- JobInfo.Builder jb = createJob(0).setOverrideDeadline(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+ JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
JobStatus js = createJobStatus("time", jb);
assertEquals(JobStatus.NO_EARLIEST_RUNTIME, js.getEarliestRunTime());
- assertEquals(MIN_WINDOW_FOR_FLEXIBILITY_MS + FROZEN_TIME, js.getLatestRunTimeElapsed());
+ assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, js.getLatestRunTimeElapsed());
assertEquals(FROZEN_TIME, js.enqueueTime);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+ assertEquals(FROZEN_TIME + fallbackDuration / 10 * 5,
nextTimeToDropNumConstraints);
js.setNumDroppedFlexibleConstraints(1);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+ assertEquals(FROZEN_TIME + fallbackDuration / 10 * 6,
nextTimeToDropNumConstraints);
js.setNumDroppedFlexibleConstraints(2);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+ assertEquals(FROZEN_TIME + fallbackDuration / 10 * 7,
nextTimeToDropNumConstraints);
// delay, no deadline
@@ -574,81 +576,83 @@
// delay, deadline
jb = createJob(0)
- .setOverrideDeadline(2 * MIN_WINDOW_FOR_FLEXIBILITY_MS)
- .setMinimumLatency(MIN_WINDOW_FOR_FLEXIBILITY_MS);
+ .setOverrideDeadline(2 * HOUR_IN_MILLIS)
+ .setMinimumLatency(HOUR_IN_MILLIS);
js = createJobStatus("time", jb);
- final long windowStart = FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS;
+ final long windowStart = FROZEN_TIME + HOUR_IN_MILLIS;
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5,
+ assertEquals(windowStart + fallbackDuration / 10 * 5,
nextTimeToDropNumConstraints);
js.setNumDroppedFlexibleConstraints(1);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 6,
+ assertEquals(windowStart + fallbackDuration / 10 * 6,
nextTimeToDropNumConstraints);
js.setNumDroppedFlexibleConstraints(2);
nextTimeToDropNumConstraints = mFlexibilityController
.getNextConstraintDropTimeElapsedLocked(js);
- assertEquals(windowStart + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 7,
+ assertEquals(windowStart + fallbackDuration / 10 * 7,
nextTimeToDropNumConstraints);
}
@Test
public void testCurPercent() {
+ final long fallbackDuration = 10 * HOUR_IN_MILLIS;
+ setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, "300=" + fallbackDuration);
long deadline = 100 * MINUTE_IN_MILLIS;
long nowElapsed = FROZEN_TIME;
JobInfo.Builder jb = createJob(0).setOverrideDeadline(deadline);
JobStatus js = createJobStatus("time", jb);
assertEquals(FROZEN_TIME, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
- assertEquals(deadline + FROZEN_TIME,
+ assertEquals(FROZEN_TIME + fallbackDuration,
mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, FROZEN_TIME));
- nowElapsed = FROZEN_TIME + 60 * MINUTE_IN_MILLIS;
+ nowElapsed = FROZEN_TIME + 6 * HOUR_IN_MILLIS;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
- nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS;
+ nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
- nowElapsed = FROZEN_TIME + 95 * MINUTE_IN_MILLIS;
+ nowElapsed = FROZEN_TIME + 9 * HOUR_IN_MILLIS;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
- assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
+ assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
nowElapsed = FROZEN_TIME;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
- long delay = MINUTE_IN_MILLIS;
- deadline = 101 * MINUTE_IN_MILLIS;
+ long delay = HOUR_IN_MILLIS;
+ deadline = HOUR_IN_MILLIS + 100 * MINUTE_IN_MILLIS;
jb = createJob(0).setOverrideDeadline(deadline).setMinimumLatency(delay);
js = createJobStatus("time", jb);
assertEquals(FROZEN_TIME + delay,
mFlexibilityController.getLifeCycleBeginningElapsedLocked(js));
- assertEquals(deadline + FROZEN_TIME,
+ assertEquals(FROZEN_TIME + delay + fallbackDuration,
mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed,
FROZEN_TIME + delay));
- nowElapsed = FROZEN_TIME + delay + 60 * MINUTE_IN_MILLIS;
+ nowElapsed = FROZEN_TIME + delay + 6 * HOUR_IN_MILLIS;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
assertEquals(60, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
- nowElapsed = FROZEN_TIME + 130 * MINUTE_IN_MILLIS;
+ nowElapsed = FROZEN_TIME + 13 * HOUR_IN_MILLIS;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
assertEquals(100, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
- nowElapsed = FROZEN_TIME + delay + 95 * MINUTE_IN_MILLIS;
+ nowElapsed = FROZEN_TIME + delay + 9 * HOUR_IN_MILLIS;
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC);
- assertEquals(95, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
+ assertEquals(90, mFlexibilityController.getCurPercentOfLifecycleLocked(js, nowElapsed));
}
@Test
@@ -786,26 +790,27 @@
// deadline
JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
JobStatus js = createJobStatus("time", jb);
- assertEquals(HOUR_IN_MILLIS + FROZEN_TIME,
- mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+ assertEquals(3 * HOUR_IN_MILLIS + js.enqueueTime,
+ mFlexibilityController
+ .getLifeCycleEndElapsedLocked(js, nowElapsed, js.enqueueTime));
// no deadline
- assertEquals(FROZEN_TIME + 2 * HOUR_IN_MILLIS,
+ assertEquals(js.enqueueTime + 2 * HOUR_IN_MILLIS,
mFlexibilityController.getLifeCycleEndElapsedLocked(
createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_HIGH)),
- nowElapsed, 100L));
- assertEquals(FROZEN_TIME + 3 * HOUR_IN_MILLIS,
+ nowElapsed, js.enqueueTime));
+ assertEquals(js.enqueueTime + 3 * HOUR_IN_MILLIS,
mFlexibilityController.getLifeCycleEndElapsedLocked(
createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
- nowElapsed, 100L));
- assertEquals(FROZEN_TIME + 4 * HOUR_IN_MILLIS,
+ nowElapsed, js.enqueueTime));
+ assertEquals(js.enqueueTime + 4 * HOUR_IN_MILLIS,
mFlexibilityController.getLifeCycleEndElapsedLocked(
createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_LOW)),
- nowElapsed, 100L));
- assertEquals(FROZEN_TIME + 5 * HOUR_IN_MILLIS,
+ nowElapsed, js.enqueueTime));
+ assertEquals(js.enqueueTime + 5 * HOUR_IN_MILLIS,
mFlexibilityController.getLifeCycleEndElapsedLocked(
createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
- nowElapsed, 100L));
+ nowElapsed, js.enqueueTime));
}
@Test
@@ -871,14 +876,16 @@
mFlexibilityController.prepareForExecutionLocked(jsLow);
mFlexibilityController.prepareForExecutionLocked(jsMin);
- // deadline
- JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS);
- JobStatus js = createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", jb);
- assertEquals(HOUR_IN_MILLIS + FROZEN_TIME,
- mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0));
+ final long longDeadlineMs = 14 * 24 * HOUR_IN_MILLIS;
+ JobInfo.Builder jbWithLongDeadline = createJob(0).setOverrideDeadline(longDeadlineMs);
+ JobStatus jsWithLongDeadline = createJobStatus(
+ "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithLongDeadline);
+ JobInfo.Builder jbWithShortDeadline =
+ createJob(0).setOverrideDeadline(15 * MINUTE_IN_MILLIS);
+ JobStatus jsWithShortDeadline = createJobStatus(
+ "testGetLifeCycleEndElapsedLocked_ScoreAddition", jbWithShortDeadline);
final long earliestMs = 123L;
- // no deadline
assertEquals(earliestMs + HOUR_IN_MILLIS + 5 * 15 * MINUTE_IN_MILLIS,
mFlexibilityController.getLifeCycleEndElapsedLocked(
createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
@@ -894,6 +901,9 @@
createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)),
nowElapsed, earliestMs));
+ assertEquals(earliestMs + HOUR_IN_MILLIS + 3 * 15 * MINUTE_IN_MILLIS,
+ mFlexibilityController.getLifeCycleEndElapsedLocked(
+ jsWithShortDeadline, nowElapsed, earliestMs));
assertEquals(earliestMs + HOUR_IN_MILLIS + 2 * 15 * MINUTE_IN_MILLIS,
mFlexibilityController.getLifeCycleEndElapsedLocked(
createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
@@ -904,6 +914,9 @@
createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition",
createJob(0).setPriority(JobInfo.PRIORITY_MIN)),
nowElapsed, earliestMs));
+ assertEquals(jsWithLongDeadline.enqueueTime + longDeadlineMs,
+ mFlexibilityController.getLifeCycleEndElapsedLocked(
+ jsWithLongDeadline, nowElapsed, earliestMs));
}
@Test
@@ -1033,8 +1046,8 @@
JobInfo.Builder jb = createJob(0);
jb.setMinimumLatency(1);
jb.setOverrideDeadline(2);
- JobStatus js = createJobStatus("Disable Flexible When Job Has Short Window", jb);
- assertFalse(js.hasFlexibilityConstraint());
+ JobStatus js = createJobStatus("testExceptions_ShortWindow", jb);
+ assertTrue(js.hasFlexibilityConstraint());
}
@Test
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index 64fef68..1de049e 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -8,6 +8,37 @@
srcs: [
"src/com/android/server/power/stats/AggregatedPowerStatsProcessorTest.java",
"src/com/android/server/power/stats/AggregatedPowerStatsTest.java",
+ "src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/AudioPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/BatteryChargeCalculatorTest.java",
+ "src/com/android/server/power/stats/BatteryStatsCounterTest.java",
+ "src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java",
+ "src/com/android/server/power/stats/BatteryStatsDualTimerTest.java",
+ "src/com/android/server/power/stats/BatteryStatsDurationTimerTest.java",
+ "src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java",
+ "src/com/android/server/power/stats/BatteryStatsHistoryTest.java",
+ "src/com/android/server/power/stats/BatteryStatsImplTest.java",
+ "src/com/android/server/power/stats/BatteryStatsNoteTest.java",
+ "src/com/android/server/power/stats/BatteryStatsSamplingTimerTest.java",
+ "src/com/android/server/power/stats/BatteryStatsSensorTest.java",
+ "src/com/android/server/power/stats/BatteryStatsServTest.java",
+ "src/com/android/server/power/stats/BatteryStatsStopwatchTimerTest.java",
+ "src/com/android/server/power/stats/BatteryStatsTimeBaseTest.java",
+ "src/com/android/server/power/stats/BatteryStatsTimerTest.java",
+ "src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java",
+ "src/com/android/server/power/stats/BatteryUsageStatsTest.java",
+ "src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/CameraPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java",
+ "src/com/android/server/power/stats/CpuPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/EnergyConsumerSnapshotTest.java",
+ "src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/GnssPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/IdlePowerCalculatorTest.java",
+ "src/com/android/server/power/stats/LongSamplingCounterArrayTest.java",
+ "src/com/android/server/power/stats/LongSamplingCounterTest.java",
+ "src/com/android/server/power/stats/MemoryPowerCalculatorTest.java",
"src/com/android/server/power/stats/MultiStateStatsTest.java",
"src/com/android/server/power/stats/PowerStatsAggregatorTest.java",
"src/com/android/server/power/stats/PowerStatsCollectorTest.java",
@@ -15,6 +46,12 @@
"src/com/android/server/power/stats/PowerStatsSchedulerTest.java",
"src/com/android/server/power/stats/PowerStatsStoreTest.java",
"src/com/android/server/power/stats/PowerStatsUidResolverTest.java",
+ "src/com/android/server/power/stats/ScreenPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/SensorPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/UserPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/VideoPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/WakelockPowerCalculatorTest.java",
+ "src/com/android/server/power/stats/WifiPowerCalculatorTest.java",
],
}
@@ -26,10 +63,6 @@
"src/**/*.java",
],
- exclude_srcs: [
- ":power_stats_ravenwood_tests",
- ],
-
static_libs: [
"services.core",
"coretests-aidl",
@@ -41,6 +74,7 @@
"androidx.test.ext.truth",
"androidx.test.uiautomator_uiautomator",
"mockito-target-minus-junit4",
+ "ravenwood-junit",
"servicestests-utils",
"platform-test-annotations",
"flag-junit",
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java
index 319a280..f74cfae 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/AmbientDisplayPowerCalculatorTest.java
@@ -21,6 +21,7 @@
import static com.google.common.truth.Truth.assertThat;
import android.os.BatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.view.Display;
import androidx.test.filters.SmallTest;
@@ -34,10 +35,15 @@
@SmallTest
@SuppressWarnings("GuardedBy")
public class AmbientDisplayPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final long MINUTE_IN_MS = 60 * 1000;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_AMBIENT, 0, 10.0)
.setNumDisplays(1);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java
index fb367b2..ce451c2 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/AudioPowerCalculatorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -34,11 +35,16 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class AudioPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_AUDIO, 360.0);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
index 3f058a2..3ab1c2e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryChargeCalculatorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryManager;
import android.os.BatteryUsageStats;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -34,9 +35,14 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class BatteryChargeCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_BATTERY_CAPACITY, 4000.0);
@@ -46,15 +52,17 @@
final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
- batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
- 1_000_000, 1_000_000, 1_000_000);
- batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
- 1_500_000, 1_500_000, 1_500_000);
- batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0,
- 2_000_000, 2_000_000, 2_000_000);
+ synchronized (batteryStats) {
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 90, 72, 3700, 3_600_000, 4_000_000, 0,
+ 1_000_000, 1_000_000, 1_000_000);
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 85, 72, 3700, 3_000_000, 4_000_000, 0,
+ 1_500_000, 1_500_000, 1_500_000);
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 80, 72, 3700, 2_400_000, 4_000_000, 0,
+ 2_000_000, 2_000_000, 2_000_000);
+ }
mStatsRule.setTime(5_000_000, 5_000_000);
BatteryChargeCalculator calculator = new BatteryChargeCalculator();
@@ -73,10 +81,11 @@
assertThat(batteryUsageStats.getChargeTimeRemainingMs()).isEqualTo(-1);
// Plug in
- batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_CHARGING, 100,
- BatteryManager.BATTERY_PLUGGED_USB, 80, 72, 3700, 2_400_000, 4_000_000, 100,
- 4_000_000, 4_000_000, 4_000_000);
-
+ synchronized (batteryStats) {
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_CHARGING, 100,
+ BatteryManager.BATTERY_PLUGGED_USB, 80, 72, 3700, 2_400_000, 4_000_000, 100,
+ 4_000_000, 4_000_000, 4_000_000);
+ }
batteryUsageStats = mStatsRule.apply(calculator);
assertThat(batteryUsageStats.getChargeTimeRemainingMs()).isEqualTo(100_000);
@@ -86,15 +95,17 @@
public void testDischargeTotals_chargeUahUnavailable() {
final BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
- batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 90, 72, 3700, 0, 0, 0,
- 1_000_000, 1_000_000, 1_000_000);
- batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 85, 72, 3700, 0, 0, 0,
- 1_500_000, 1_500_000, 1_500_000);
- batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
- /* plugType */ 0, 80, 72, 3700, 0, 0, 0,
- 2_000_000, 2_000_000, 2_000_000);
+ synchronized (batteryStats) {
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 90, 72, 3700, 0, 0, 0,
+ 1_000_000, 1_000_000, 1_000_000);
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 85, 72, 3700, 0, 0, 0,
+ 1_500_000, 1_500_000, 1_500_000);
+ batteryStats.setBatteryStateLocked(BatteryManager.BATTERY_STATUS_DISCHARGING, 100,
+ /* plugType */ 0, 80, 72, 3700, 0, 0, 0,
+ 2_000_000, 2_000_000, 2_000_000);
+ }
BatteryChargeCalculator calculator = new BatteryChargeCalculator();
BatteryUsageStats batteryUsageStats = mStatsRule.apply(calculator);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
index 9c2834d..997b771 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryExternalStatsWorkerTest.java
@@ -215,7 +215,7 @@
public class TestBatteryStatsImpl extends BatteryStatsImpl {
public TestBatteryStatsImpl(Context context) {
- super(Clock.SYSTEM_CLOCK, null, null, null);
+ super(Clock.SYSTEM_CLOCK, null, null, null, null, null, null);
mPowerProfile = new PowerProfile(context, true /* forTest */);
SparseArray<int[]> cpusByPolicy = new SparseArray<>();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java
index f9f32b2..6e62147 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsCpuTimesTest.java
@@ -40,6 +40,7 @@
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.util.SparseArray;
import android.util.SparseLongArray;
import android.view.Display;
@@ -56,6 +57,7 @@
import com.android.internal.util.ArrayUtils;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -84,7 +86,13 @@
*/
@SmallTest
@RunWith(AndroidJUnit4.class)
+@SuppressWarnings("SynchronizeOnNonFinalField")
public class BatteryStatsCpuTimesTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
@Mock
KernelCpuUidUserSysTimeReader mCpuUidUserSysTimeReader;
@Mock
@@ -128,7 +136,9 @@
initKernelCpuSpeedReaders(numClusters);
// RUN
- mBatteryStatsImpl.updateCpuTimeLocked(false, false, null);
+ synchronized (mBatteryStatsImpl) {
+ mBatteryStatsImpl.updateCpuTimeLocked(false, false, null);
+ }
// VERIFY
verify(mCpuUidUserSysTimeReader).readDelta(anyBoolean(), isNull());
@@ -147,7 +157,9 @@
mBatteryStatsImpl.setOnBatteryInternal(true);
// RUN
- mBatteryStatsImpl.updateCpuTimeLocked(true, false, null);
+ synchronized (mBatteryStatsImpl) {
+ mBatteryStatsImpl.updateCpuTimeLocked(true, false, null);
+ }
// VERIFY
verify(mUserInfoProvider).refreshUserIds();
@@ -239,7 +251,7 @@
mBatteryStatsImpl.updateClusterSpeedTimes(updatedUids, true, null);
// VERIFY
- int totalClustersTimeMs = 0;
+ long totalClustersTimeMs = 0;
for (int i = 0; i < clusterSpeedTimesMs.length; ++i) {
for (int j = 0; j < clusterSpeedTimesMs[i].length; ++j) {
totalClustersTimeMs += clusterSpeedTimesMs[i][j];
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
index bf5bf36..395b3aa 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryManager;
import android.os.BatteryStats;
import android.os.Process;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -29,6 +30,7 @@
import com.android.internal.os.MonotonicClock;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,6 +44,11 @@
@SmallTest
@SuppressWarnings("GuardedBy")
public class BatteryStatsHistoryIteratorTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
private final MockClock mMockClock = new MockClock();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 9251376..c58c92b 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -26,7 +26,6 @@
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
-import android.content.Context;
import android.os.BatteryConsumer;
import android.os.BatteryManager;
import android.os.BatteryStats;
@@ -39,7 +38,6 @@
import android.util.AtomicFile;
import android.util.Log;
-import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.os.BatteryStatsHistory;
@@ -59,6 +57,7 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -80,13 +79,14 @@
private BatteryStatsHistory.TraceDelegate mTracer;
@Mock
private BatteryStatsHistory.HistoryStepDetailsCalculator mStepDetailsCalculator;
+ @Mock
+ private BatteryStatsHistory.EventLogger mEventLogger;
private List<String> mReadFiles = new ArrayList<>();
@Before
- public void setUp() {
+ public void setUp() throws IOException {
MockitoAnnotations.initMocks(this);
- Context context = InstrumentationRegistry.getContext();
- mSystemDir = context.getDataDir();
+ mSystemDir = Files.createTempDirectory("BatteryStatsHistoryTest").toFile();
mHistoryDir = new File(mSystemDir, "battery-history");
String[] files = mHistoryDir.list();
if (files != null) {
@@ -99,7 +99,7 @@
mClock.realtime = 123;
mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
- mStepDetailsCalculator, mClock, mMonotonicClock, mTracer);
+ mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger);
when(mStepDetailsCalculator.getHistoryStepDetails())
.thenReturn(new BatteryStats.HistoryStepDetails());
@@ -238,7 +238,7 @@
// create a new BatteryStatsHistory object, it will pick up existing history files.
BatteryStatsHistory history2 = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024,
- null, mClock, mMonotonicClock, mTracer);
+ null, mClock, mMonotonicClock, mTracer, mEventLogger);
// verify constructor can pick up all files from file system.
verifyFileNames(history2, fileList);
verifyActiveFile(history2, "33000.bh");
@@ -534,7 +534,7 @@
// Keep the preserved part of history short - we only need to capture the very tail of
// history.
mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 1, 6000,
- mStepDetailsCalculator, mClock, mMonotonicClock, mTracer);
+ mStepDetailsCalculator, mClock, mMonotonicClock, mTracer, mEventLogger);
mHistory.forceRecordAllHistory();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
index 8d51592..548fae7 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsImplTest.java
@@ -31,15 +31,18 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.UidTraffic;
import android.content.Context;
+import android.hardware.SensorManager;
import android.os.BatteryConsumer;
import android.os.BatteryManager;
import android.os.BatteryStats;
@@ -51,11 +54,11 @@
import android.os.Parcel;
import android.os.WakeLockStats;
import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.util.SparseArray;
import android.view.Display;
import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.os.CpuScalingPolicies;
@@ -68,19 +71,26 @@
import com.google.common.truth.LongSubject;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
-@LargeTest
@RunWith(AndroidJUnit4.class)
@SuppressWarnings("GuardedBy")
public class BatteryStatsImplTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
@Mock
private KernelCpuUidFreqTimeReader mKernelUidCpuFreqTimeReader;
@Mock
@@ -110,7 +120,7 @@
private PowerStatsExporter mPowerStatsExporter;
@Before
- public void setUp() {
+ public void setUp() throws IOException {
MockitoAnnotations.initMocks(this);
when(mKernelUidCpuFreqTimeReader.isFastCpuTimesReader()).thenReturn(true);
@@ -128,8 +138,17 @@
.setKernelSingleUidTimeReader(mKernelSingleUidTimeReader)
.setKernelWakelockReader(mKernelWakelockReader);
- final Context context = InstrumentationRegistry.getContext();
- File systemDir = context.getCacheDir();
+ File systemDir = Files.createTempDirectory("BatteryStatsHistoryTest").toFile();
+
+ Context context;
+ if (RavenwoodRule.isUnderRavenwood()) {
+ context = mock(Context.class);
+ SensorManager sensorManager = mock(SensorManager.class);
+ when(sensorManager.getSensorList(anyInt())).thenReturn(List.of());
+ when(context.getSystemService(SensorManager.class)).thenReturn(sensorManager);
+ } else {
+ context = InstrumentationRegistry.getContext();
+ }
mPowerStatsStore = new PowerStatsStore(systemDir, mHandler,
new AggregatedPowerStatsConfig());
mBatteryUsageStatsProvider = new BatteryUsageStatsProvider(context, mPowerStatsExporter,
@@ -747,14 +766,22 @@
}
private UidTraffic createUidTraffic(int appUid, long rxBytes, long txBytes) {
- final Parcel parcel = Parcel.obtain();
- parcel.writeInt(appUid); // mAppUid
- parcel.writeLong(rxBytes); // mRxBytes
- parcel.writeLong(txBytes); // mTxBytes
- parcel.setDataPosition(0);
- UidTraffic uidTraffic = UidTraffic.CREATOR.createFromParcel(parcel);
- parcel.recycle();
- return uidTraffic;
+ if (RavenwoodRule.isUnderRavenwood()) {
+ UidTraffic uidTraffic = mock(UidTraffic.class);
+ when(uidTraffic.getUid()).thenReturn(appUid);
+ when(uidTraffic.getRxBytes()).thenReturn(rxBytes);
+ when(uidTraffic.getTxBytes()).thenReturn(txBytes);
+ return uidTraffic;
+ } else {
+ final Parcel parcel = Parcel.obtain();
+ parcel.writeInt(appUid); // mAppUid
+ parcel.writeLong(rxBytes); // mRxBytes
+ parcel.writeLong(txBytes); // mTxBytes
+ parcel.setDataPosition(0);
+ UidTraffic uidTraffic = UidTraffic.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return uidTraffic;
+ }
}
private BluetoothActivityEnergyInfo createBluetoothActivityEnergyInfo(
@@ -764,21 +791,31 @@
long controllerIdleTimeMs,
long controllerEnergyUsed,
UidTraffic... uidTraffic) {
- Parcel parcel = Parcel.obtain();
- parcel.writeLong(timestamp); // mTimestamp
- parcel.writeInt(
- BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE); // mBluetoothStackState
- parcel.writeLong(controllerTxTimeMs); // mControllerTxTimeMs;
- parcel.writeLong(controllerRxTimeMs); // mControllerRxTimeMs;
- parcel.writeLong(controllerIdleTimeMs); // mControllerIdleTimeMs;
- parcel.writeLong(controllerEnergyUsed); // mControllerEnergyUsed;
- parcel.writeTypedList(ImmutableList.copyOf(uidTraffic)); // mUidTraffic
- parcel.setDataPosition(0);
+ if (RavenwoodRule.isUnderRavenwood()) {
+ BluetoothActivityEnergyInfo info = mock(BluetoothActivityEnergyInfo.class);
+ when(info.getTimestampMillis()).thenReturn(timestamp);
+ when(info.getControllerTxTimeMillis()).thenReturn(controllerTxTimeMs);
+ when(info.getControllerRxTimeMillis()).thenReturn(controllerRxTimeMs);
+ when(info.getControllerIdleTimeMillis()).thenReturn(controllerIdleTimeMs);
+ when(info.getControllerEnergyUsed()).thenReturn(controllerEnergyUsed);
+ when(info.getUidTraffic()).thenReturn(ImmutableList.copyOf(uidTraffic));
+ return info;
+ } else {
+ Parcel parcel = Parcel.obtain();
+ parcel.writeLong(timestamp); // mTimestamp
+ parcel.writeInt(BluetoothActivityEnergyInfo.BT_STACK_STATE_STATE_ACTIVE);
+ parcel.writeLong(controllerTxTimeMs); // mControllerTxTimeMs;
+ parcel.writeLong(controllerRxTimeMs); // mControllerRxTimeMs;
+ parcel.writeLong(controllerIdleTimeMs); // mControllerIdleTimeMs;
+ parcel.writeLong(controllerEnergyUsed); // mControllerEnergyUsed;
+ parcel.writeTypedList(ImmutableList.copyOf(uidTraffic)); // mUidTraffic
+ parcel.setDataPosition(0);
- BluetoothActivityEnergyInfo info =
- BluetoothActivityEnergyInfo.CREATOR.createFromParcel(parcel);
- parcel.recycle();
- return info;
+ BluetoothActivityEnergyInfo info =
+ BluetoothActivityEnergyInfo.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return info;
+ }
}
@Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
index eea2875..07cefa9 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
@@ -28,6 +28,10 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import android.app.ActivityManager;
@@ -40,6 +44,7 @@
import android.os.Process;
import android.os.UserHandle;
import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.telephony.AccessNetworkConstants;
import android.telephony.ActivityStatsTechSpecificInfo;
import android.telephony.Annotation;
@@ -50,7 +55,6 @@
import android.telephony.ServiceState;
import android.telephony.TelephonyManager;
import android.util.Log;
-import android.util.MutableInt;
import android.util.SparseIntArray;
import android.util.SparseLongArray;
import android.view.Display;
@@ -63,8 +67,8 @@
import com.android.internal.power.EnergyConsumerStats;
import com.android.server.power.stats.BatteryStatsImpl.DualTimer;
-import junit.framework.TestCase;
-
+import org.junit.Rule;
+import org.junit.Test;
import org.mockito.Mock;
import java.util.ArrayList;
@@ -78,7 +82,14 @@
* Test various BatteryStatsImpl noteStart methods.
*/
@SuppressWarnings("GuardedBy")
-public class BatteryStatsNoteTest extends TestCase {
+@SmallTest
+public class BatteryStatsNoteTest {
+
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final String TAG = BatteryStatsNoteTest.class.getSimpleName();
private static final int UID = 10500;
@@ -96,7 +107,7 @@
/**
* Test BatteryStatsImpl.Uid.noteBluetoothScanResultLocked.
*/
- @SmallTest
+ @Test
public void testNoteBluetoothScanResultLocked() throws Exception {
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(new MockClock());
bi.updateTimeBasesLocked(true, Display.STATE_OFF, 0, 0);
@@ -125,7 +136,7 @@
/**
* Test BatteryStatsImpl.Uid.noteStartWakeLocked.
*/
- @SmallTest
+ @Test
public void testNoteStartWakeLocked() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -155,7 +166,7 @@
/**
* Test BatteryStatsImpl.Uid.noteStartWakeLocked for an isolated uid.
*/
- @SmallTest
+ @Test
public void testNoteStartWakeLocked_isolatedUid() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -197,7 +208,7 @@
* Test BatteryStatsImpl.Uid.noteStartWakeLocked for an isolated uid, with a race where the
* isolated uid is removed from batterystats before the wakelock has been stopped.
*/
- @SmallTest
+ @Test
public void testNoteStartWakeLocked_isolatedUidRace() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -241,7 +252,7 @@
/**
* Test BatteryStatsImpl.Uid.noteLongPartialWakelockStart for an isolated uid.
*/
- @SmallTest
+ @Test
public void testNoteLongPartialWakelockStart_isolatedUid() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -296,7 +307,7 @@
/**
* Test BatteryStatsImpl.Uid.noteLongPartialWakelockStart for an isolated uid.
*/
- @SmallTest
+ @Test
public void testNoteLongPartialWakelockStart_isolatedUidRace() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
PowerStatsUidResolver uidResolver = new PowerStatsUidResolver();
@@ -354,7 +365,7 @@
/**
* Test BatteryStatsImpl.noteUidProcessStateLocked.
*/
- @SmallTest
+ @Test
public void testNoteUidProcessStateLocked() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -441,7 +452,7 @@
/**
* Test BatteryStatsImpl.updateTimeBasesLocked.
*/
- @SmallTest
+ @Test
public void testUpdateTimeBasesLocked() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -465,7 +476,7 @@
/**
* Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly.
*/
- @SmallTest
+ @Test
public void testNoteScreenStateLocked() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -512,7 +523,7 @@
* Test BatteryStatsImpl.noteScreenStateLocked sets timebases and screen states correctly for
* multi display devices
*/
- @SmallTest
+ @Test
public void testNoteScreenStateLocked_multiDisplay() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -606,7 +617,7 @@
* Off ------- ----------------------
* Doze ----------------
*/
- @SmallTest
+ @Test
public void testNoteScreenStateTimersLocked() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -656,7 +667,7 @@
* Test BatteryStatsImpl.noteScreenStateLocked updates timers correctly for multi display
* devices.
*/
- @SmallTest
+ @Test
public void testNoteScreenStateTimersLocked_multiDisplay() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -807,7 +818,7 @@
/**
* Test BatteryStatsImpl.noteScreenBrightnessLocked updates timers correctly.
*/
- @SmallTest
+ @Test
public void testScreenBrightnessLocked_multiDisplay() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -929,7 +940,7 @@
checkScreenBrightnesses(overallExpected, perDisplayExpected, bi, bk.currentTimeMs);
}
- @SmallTest
+ @Test
public void testAlarmStartAndFinishLocked() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -967,7 +978,7 @@
assertThat(iterator.next()).isNull();
}
- @SmallTest
+ @Test
public void testAlarmStartAndFinishLocked_workSource() throws Exception {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1013,7 +1024,7 @@
assertEquals(500, item.eventTag.uid);
}
- @SmallTest
+ @Test
public void testNoteWakupAlarmLocked() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1031,7 +1042,7 @@
assertEquals(1, pkg.getWakeupAlarmStats().size());
}
- @SmallTest
+ @Test
public void testNoteWakupAlarmLocked_workSource_uid() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1064,7 +1075,7 @@
assertEquals(1, pkg.getWakeupAlarmStats().size());
}
- @SmallTest
+ @Test
public void testNoteWakupAlarmLocked_workSource_workChain() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1090,7 +1101,7 @@
assertEquals(0, pkg.getWakeupAlarmStats().size());
}
- @SmallTest
+ @Test
public void testNoteGpsChanged() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1114,7 +1125,7 @@
assertFalse(t.isRunningLocked());
}
- @SmallTest
+ @Test
public void testNoteGpsChanged_workSource() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1138,7 +1149,7 @@
assertFalse(t.isRunningLocked());
}
- @SmallTest
+ @Test
public void testUpdateDisplayMeasuredEnergyStatsLocked() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1223,7 +1234,7 @@
checkMeasuredCharge("H", uid1, blame1, uid2, blame2, globalDoze, bi);
}
- @SmallTest
+ @Test
public void testUpdateCustomMeasuredEnergyStatsLocked_neverCalled() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1237,7 +1248,7 @@
checkCustomBatteryConsumption("0", 0, 0, uid1, 0, 0, uid2, 0, 0, bi);
}
- @SmallTest
+ @Test
public void testUpdateCustomMeasuredEnergyStatsLocked() {
final MockClock clocks = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -1314,7 +1325,7 @@
"D", totalBlameA, totalBlameB, uid1, blame1A, blame1B, uid2, blame2A, blame2B, bi);
}
- @SmallTest
+ @Test
public void testGetPerStateActiveRadioDurationMs_noModemActivity() {
final MockClock clock = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -1470,7 +1481,7 @@
expectedTxDurationsMs, bi, state.currentTimeMs);
}
- @SmallTest
+ @Test
public void testGetPerStateActiveRadioDurationMs_initialModemActivity() {
final MockClock clock = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -1612,7 +1623,7 @@
expectedTxDurationsMs, bi, state.currentTimeMs);
}
- @SmallTest
+ @Test
public void testGetPerStateActiveRadioDurationMs_withModemActivity() {
final MockClock clock = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -1852,7 +1863,7 @@
expectedTxDurationsMs, bi, state.currentTimeMs);
}
- @SmallTest
+ @Test
public void testGetPerStateActiveRadioDurationMs_withSpecificInfoModemActivity() {
final MockClock clock = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
@@ -2145,19 +2156,19 @@
expectedTxDurationsMs, bi, state.currentTimeMs);
}
- @SmallTest
+ @Test
@SuppressWarnings("GuardedBy")
public void testProcStateSyncScheduling_mobileRadioActiveState() {
final MockClock clock = new MockClock(); // holds realtime and uptime in ms
final MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clock);
- final MutableInt lastProcStateChangeFlags = new MutableInt(0);
+ final int[] lastProcStateChangeFlags = new int[1];
MockBatteryStatsImpl.DummyExternalStatsSync externalStatsSync =
new MockBatteryStatsImpl.DummyExternalStatsSync() {
@Override
public void scheduleSyncDueToProcessStateChange(int flags,
long delayMillis) {
- lastProcStateChangeFlags.value = flags;
+ lastProcStateChangeFlags[0] = flags;
}
};
@@ -2170,19 +2181,19 @@
bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_HIGH, curr,
UID);
- lastProcStateChangeFlags.value = 0;
+ lastProcStateChangeFlags[0] = 0;
clock.realtime = clock.uptime = 2002;
bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND);
final int allProcFlags = BatteryStatsImpl.ExternalStatsSync.UPDATE_ON_PROC_STATE_CHANGE;
- assertEquals(allProcFlags, lastProcStateChangeFlags.value);
+ assertEquals(allProcFlags, lastProcStateChangeFlags[0]);
// Note mobile radio is off.
curr = 1000L * (clock.realtime = clock.uptime = 3003);
bi.noteMobileRadioPowerStateLocked(DataConnectionRealTimeInfo.DC_POWER_STATE_LOW, curr,
UID);
- lastProcStateChangeFlags.value = 0;
+ lastProcStateChangeFlags[0] = 0;
clock.realtime = clock.uptime = 4004;
bi.noteUidProcessStateLocked(UID, ActivityManager.PROCESS_STATE_CACHED_EMPTY);
@@ -2190,10 +2201,10 @@
& ~BatteryStatsImpl.ExternalStatsSync.UPDATE_RADIO;
assertEquals(
"An inactive radio should not be queried on proc state change",
- noRadioProcFlags, lastProcStateChangeFlags.value);
+ noRadioProcFlags, lastProcStateChangeFlags[0]);
}
- @SmallTest
+ @Test
public void testNoteMobileRadioPowerStateLocked() {
long curr;
boolean update;
@@ -2243,7 +2254,7 @@
update);
}
- @SmallTest
+ @Test
public void testNoteMobileRadioPowerStateLocked_rateLimited() {
long curr;
boolean update;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java
index 9c70f37..96780c3 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsSensorTest.java
@@ -16,24 +16,36 @@
package com.android.server.power.stats;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
import android.app.ActivityManager;
import android.os.BatteryStats;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.view.Display;
import androidx.test.filters.SmallTest;
-import junit.framework.TestCase;
+import org.junit.Rule;
+import org.junit.Test;
/**
* Test BatteryStatsImpl Sensor Timers.
*/
-public class BatteryStatsSensorTest extends TestCase {
+@SmallTest
+public class BatteryStatsSensorTest {
+
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
private static final int UID = 10500;
private static final int UID_2 = 10501; // second uid for testing pool usage
private static final int SENSOR_ID = -10000;
- @SmallTest
+ @Test
public void testSensorStartStop() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -70,7 +82,7 @@
clocks.realtime * 1000, BatteryStats.STATS_SINCE_CHARGED));
}
- @SmallTest
+ @Test
public void testCountingWhileOffBattery() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -106,7 +118,7 @@
assertEquals(0, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
}
- @SmallTest
+ @Test
public void testCountingWhileOnBattery() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -141,7 +153,7 @@
assertEquals(1, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
}
- @SmallTest
+ @Test
public void testBatteryStatusOnToOff() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -187,7 +199,7 @@
sensorTimer.getTotalTimeLocked(curr, BatteryStats.STATS_SINCE_CHARGED));
}
- @SmallTest
+ @Test
public void testBatteryStatusOffToOn() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -238,7 +250,7 @@
assertEquals(0, sensorTimer.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
}
- @SmallTest
+ @Test
public void testPooledBackgroundUsage() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -375,7 +387,7 @@
assertEquals(2, bgTimer2.getCountLocked(BatteryStats.STATS_SINCE_CHARGED));
}
- @SmallTest
+ @Test
public void testSensorReset() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
@@ -419,7 +431,7 @@
assertNull(sensor);
}
- @SmallTest
+ @Test
public void testSensorResetTimes() throws Exception {
final MockClock clocks = new MockClock();
MockBatteryStatsImpl bi = new MockBatteryStatsImpl(clocks);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java
index 200eb1d..6f683ae 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsServTest.java
@@ -18,16 +18,25 @@
import android.os.BatteryStats;
import android.os.Parcel;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import junit.framework.Assert;
-import junit.framework.TestCase;
+
+import org.junit.Rule;
+import org.junit.Test;
/**
* Provides test cases for android.os.BatteryStats.
*/
-public class BatteryStatsServTest extends TestCase {
+@SmallTest
+public class BatteryStatsServTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final String TAG = "BatteryStatsServTest";
public static class TestServ extends BatteryStatsImpl.Uid.Pkg.Serv {
@@ -90,7 +99,7 @@
/**
* Test that the constructor and detach methods touch the time bast observer list.
*/
- @SmallTest
+ @Test
public void testConstructAndDetach() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
@@ -104,7 +113,7 @@
/**
* Test parceling and unparceling.
*/
- @SmallTest
+ @Test
public void testParceling() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ orig = new TestServ(bsi);
@@ -133,7 +142,7 @@
/**
* Test getLaunchTimeToNow()
*/
- @SmallTest
+ @Test
public void testLaunchTimeToNow() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -151,7 +160,7 @@
/**
* Test getStartTimeToNow()
*/
- @SmallTest
+ @Test
public void testStartTimeToNow() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -168,7 +177,7 @@
/**
* Test startLaunchedLocked while not previously launched
*/
- @SmallTest
+ @Test
public void testStartLaunchedLockedWhileLaunched() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
@Override
@@ -197,7 +206,7 @@
/**
* Test startLaunchedLocked while previously launched
*/
- @SmallTest
+ @Test
public void testStartLaunchedLockedWhileNotLaunched() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
@Override
@@ -224,7 +233,7 @@
/**
* Test stopLaunchedLocked when not previously launched.
*/
- @SmallTest
+ @Test
public void testStopLaunchedLockedWhileNotLaunched() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
@Override
@@ -254,7 +263,7 @@
* Test stopLaunchedLocked when previously launched, with measurable time between
* start and stop.
*/
- @SmallTest
+ @Test
public void testStopLaunchedLockedWhileLaunchedNormal() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
@Override
@@ -283,7 +292,7 @@
* Test stopLaunchedLocked when previously launched, with no measurable time between
* start and stop.
*/
- @SmallTest
+ @Test
public void testStopLaunchedLockedWhileLaunchedTooQuick() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -306,7 +315,7 @@
/**
* Test startRunningLocked while previously running
*/
- @SmallTest
+ @Test
public void testStartRunningLockedWhileRunning() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
@Override
@@ -335,7 +344,7 @@
/**
* Test startRunningLocked while not previously launched
*/
- @SmallTest
+ @Test
public void testStartRunningLockedWhileNotRunning() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
@Override
@@ -364,7 +373,7 @@
* Test stopRunningLocked when previously launched, with measurable time between
* start and stop.
*/
- @SmallTest
+ @Test
public void testStopRunningLockedWhileRunningNormal() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl() {
@Override
@@ -393,7 +402,7 @@
* Test stopRunningLocked when previously launched, with measurable time between
* start and stop.
*/
- @SmallTest
+ @Test
public void testStopRunningLockedWhileRunningTooQuick() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -416,7 +425,7 @@
/**
* Test that getBatteryStats returns the BatteryStatsImpl passed in to the contstructor.
*/
- @SmallTest
+ @Test
public void testGetBatteryStats() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -427,7 +436,7 @@
/**
* Test getLaunches
*/
- @SmallTest
+ @Test
public void testGetLaunches() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -449,7 +458,7 @@
/**
* Test getStartTime while running
*/
- @SmallTest
+ @Test
public void testGetStartTimeRunning() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -475,7 +484,7 @@
/**
* Test getStartTime while not running
*/
- @SmallTest
+ @Test
public void testGetStartTimeNotRunning() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -502,7 +511,7 @@
/**
* Test getStarts
*/
- @SmallTest
+ @Test
public void testGetStarts() throws Exception {
MockBatteryStatsImpl bsi = new MockBatteryStatsImpl();
TestServ serv = new TestServ(bsi);
@@ -521,4 +530,3 @@
Assert.assertEquals(8085, serv.getLaunches());
}
}
-
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
index face849..05d8a00 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsUserLifecycleTests.java
@@ -46,6 +46,7 @@
@LargeTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.IgnoreUnderRavenwood
public class BatteryStatsUserLifecycleTests {
private static final long POLL_INTERVAL_MS = 500;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 2e0ba00..6cd79bc 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -23,6 +23,7 @@
import android.app.ActivityManager;
import android.content.Context;
+import android.hardware.SensorManager;
import android.os.BatteryConsumer;
import android.os.BatteryManager;
import android.os.BatteryStats;
@@ -32,6 +33,7 @@
import android.os.Parcel;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
@@ -40,36 +42,69 @@
import com.android.internal.os.BatteryStatsHistoryIterator;
import com.android.internal.os.PowerProfile;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
import java.util.List;
-import java.util.Random;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class BatteryUsageStatsProviderTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
private static final long MINUTE_IN_MS = 60 * 1000;
private static final double PRECISION = 0.00001;
- private final File mHistoryDir = createTemporaryDirectory();
+ private File mHistoryDir;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule =
new BatteryUsageStatsRule(12345, mHistoryDir)
.setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0)
.setAveragePower(PowerProfile.POWER_AUDIO, 720.0);
+
private MockClock mMockClock = mStatsRule.getMockClock();
+ private Context mContext;
+
+ @Before
+ public void setup() throws IOException {
+ mHistoryDir = Files.createTempDirectory("BatteryUsageStatsProviderTest").toFile();
+ clearDirectory(mHistoryDir);
+
+ if (RavenwoodRule.isUnderRavenwood()) {
+ mContext = mock(Context.class);
+ SensorManager sensorManager = mock(SensorManager.class);
+ when(mContext.getSystemService(SensorManager.class)).thenReturn(sensorManager);
+ } else {
+ mContext = InstrumentationRegistry.getContext();
+ }
+ }
+
+ private void clearDirectory(File dir) {
+ if (dir.exists()) {
+ for (File child : dir.listFiles()) {
+ if (child.isDirectory()) {
+ clearDirectory(child);
+ }
+ child.delete();
+ }
+ }
+ }
@Test
public void test_getBatteryUsageStats() {
BatteryStatsImpl batteryStats = prepareBatteryStats();
- Context context = InstrumentationRegistry.getContext();
- BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
final BatteryUsageStats batteryUsageStats =
@@ -105,8 +140,7 @@
public void test_selectPowerComponents() {
BatteryStatsImpl batteryStats = prepareBatteryStats();
- Context context = InstrumentationRegistry.getContext();
- BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
final BatteryUsageStats batteryUsageStats =
@@ -211,8 +245,7 @@
batteryStats.noteAlarmFinishLocked("foo", null, APP_UID, 3_001_000, 2_001_000);
}
- Context context = InstrumentationRegistry.getContext();
- BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
final BatteryUsageStats batteryUsageStats =
@@ -300,8 +333,7 @@
}
}
- Context context = InstrumentationRegistry.getContext();
- BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), null, mMockClock);
final BatteryUsageStats batteryUsageStats =
@@ -311,7 +343,9 @@
Parcel parcel = Parcel.obtain();
parcel.writeParcelable(batteryUsageStats, 0);
- assertThat(parcel.dataSize()).isAtMost(128_000);
+ if (!RavenwoodRule.isUnderRavenwood()) {
+ assertThat(parcel.dataSize()).isAtMost(128_000);
+ }
parcel.setDataPosition(0);
@@ -375,7 +409,6 @@
@Test
public void testAggregateBatteryStats() {
- Context context = InstrumentationRegistry.getContext();
BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
setTime(5 * MINUTE_IN_MS);
@@ -384,11 +417,11 @@
}
PowerStatsStore powerStatsStore = new PowerStatsStore(
- new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"),
+ new File(mHistoryDir, "powerstatsstore"),
mStatsRule.getHandler(), null);
powerStatsStore.reset();
- BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), powerStatsStore,
mMockClock);
@@ -485,7 +518,6 @@
@Test
public void testAggregateBatteryStats_incompatibleSnapshot() {
- Context context = InstrumentationRegistry.getContext();
MockBatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
batteryStats.initMeasuredEnergyStats(new String[]{"FOO", "BAR"});
@@ -511,7 +543,7 @@
when(powerStatsStore.loadPowerStatsSpan(1, BatteryUsageStatsSection.TYPE))
.thenReturn(span1);
- BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(context, null,
+ BatteryUsageStatsProvider provider = new BatteryUsageStatsProvider(mContext, null,
mStatsRule.getPowerProfile(), mStatsRule.getCpuScalingPolicies(), powerStatsStore,
mMockClock);
@@ -523,20 +555,4 @@
.isEqualTo(batteryStats.getCustomEnergyConsumerNames());
assertThat(stats.getStatsDuration()).isEqualTo(1234);
}
-
- private static final Random sRandom = new Random();
-
- /**
- * Creates a unique new temporary directory under "java.io.tmpdir".
- */
- private static File createTemporaryDirectory() {
- while (true) {
- String candidateName =
- BatteryUsageStatsProviderTest.class.getSimpleName() + sRandom.nextInt();
- File result = new File(System.getProperty("java.io.tmpdir"), candidateName);
- if (result.mkdir()) {
- return result;
- }
- }
- }
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index ba2b538..8bdb029 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -49,6 +49,7 @@
import java.io.File;
import java.util.Arrays;
+@SuppressWarnings("SynchronizeOnNonFinalField")
public class BatteryUsageStatsRule implements TestRule {
public static final BatteryUsageStatsQuery POWER_PROFILE_MODEL_ONLY =
new BatteryUsageStatsQuery.Builder()
@@ -71,6 +72,8 @@
private int mDisplayCount = -1;
private int mPerUidModemModel = -1;
private NetworkStats mNetworkStats;
+ private boolean[] mSupportedStandardBuckets;
+ private String[] mCustomPowerComponentNames;
public BatteryUsageStatsRule() {
this(0, null);
@@ -102,6 +105,11 @@
mBatteryStats = new MockBatteryStatsImpl(mMockClock, mHistoryDir, mHandler);
mBatteryStats.setPowerProfile(mPowerProfile);
mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy));
+ synchronized (mBatteryStats) {
+ mBatteryStats.initEnergyConsumerStatsLocked(mSupportedStandardBuckets,
+ mCustomPowerComponentNames);
+ }
+ mBatteryStats.informThatAllExternalStatsAreFlushed();
mBatteryStats.onSystemReady();
@@ -230,13 +238,15 @@
/** Call only after setting the power profile information. */
public BatteryUsageStatsRule initMeasuredEnergyStatsLocked(
String[] customPowerComponentNames) {
- final boolean[] supportedStandardBuckets =
- new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
- Arrays.fill(supportedStandardBuckets, true);
- synchronized (mBatteryStats) {
- mBatteryStats.initEnergyConsumerStatsLocked(supportedStandardBuckets,
- customPowerComponentNames);
- mBatteryStats.informThatAllExternalStatsAreFlushed();
+ mCustomPowerComponentNames = customPowerComponentNames;
+ mSupportedStandardBuckets = new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
+ Arrays.fill(mSupportedStandardBuckets, true);
+ if (mBatteryStats != null) {
+ synchronized (mBatteryStats) {
+ mBatteryStats.initEnergyConsumerStatsLocked(mSupportedStandardBuckets,
+ mCustomPowerComponentNames);
+ mBatteryStats.informThatAllExternalStatsAreFlushed();
+ }
}
return this;
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
index 079ea2c7..851cf4a 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsTest.java
@@ -37,6 +37,7 @@
import android.os.Parcel;
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.util.Xml;
import androidx.test.filters.SmallTest;
@@ -45,6 +46,7 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -62,6 +64,10 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
public class BatteryUsageStatsTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
private static final int USER_ID = 42;
private static final int APP_UID1 = 271;
@@ -115,12 +121,13 @@
final Parcel parcel = Parcel.obtain();
parcel.writeParcelable(outBatteryUsageStats, 0);
- assertThat(parcel.dataSize()).isLessThan(2000);
+ // Under ravenwood this parcel is larger. On a device, 2K would suffice
+ assertThat(parcel.dataSize()).isLessThan(128_000);
parcel.setDataPosition(0);
final BatteryUsageStats inBatteryUsageStats =
- parcel.readParcelable(getClass().getClassLoader());
+ parcel.readParcelable(getClass().getClassLoader(), BatteryUsageStats.class);
parcel.recycle();
assertThat(inBatteryUsageStats.getUidBatteryConsumers()).hasSize(uidCount);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java
index 4d4337c1..fe6424f 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BluetoothPowerCalculatorTest.java
@@ -18,6 +18,9 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import android.annotation.Nullable;
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.UidTraffic;
@@ -28,6 +31,7 @@
import android.os.Process;
import android.os.UidBatteryConsumer;
import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -46,10 +50,15 @@
@SmallTest
@SuppressWarnings("GuardedBy")
public class BluetoothPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_IDLE, 10.0)
.setAveragePower(PowerProfile.POWER_BLUETOOTH_CONTROLLER_RX, 50.0)
@@ -331,33 +340,53 @@
assertThat(usageDurationMillis).isEqualTo(durationMs);
}
- private UidTraffic createUidTraffic(int uid, long traffic1, long traffic2) {
- final Parcel uidTrafficParcel = Parcel.obtain();
- uidTrafficParcel.writeInt(uid);
- uidTrafficParcel.writeLong(traffic1);
- uidTrafficParcel.writeLong(traffic2);
- uidTrafficParcel.setDataPosition(0);
+ private UidTraffic createUidTraffic(int appUid, long rxBytes, long txBytes) {
+ if (RavenwoodRule.isUnderRavenwood()) {
+ UidTraffic uidTraffic = mock(UidTraffic.class);
+ when(uidTraffic.getUid()).thenReturn(appUid);
+ when(uidTraffic.getRxBytes()).thenReturn(rxBytes);
+ when(uidTraffic.getTxBytes()).thenReturn(txBytes);
+ return uidTraffic;
+ } else {
+ final Parcel uidTrafficParcel = Parcel.obtain();
+ uidTrafficParcel.writeInt(appUid);
+ uidTrafficParcel.writeLong(rxBytes);
+ uidTrafficParcel.writeLong(txBytes);
+ uidTrafficParcel.setDataPosition(0);
- UidTraffic traffic = UidTraffic.CREATOR.createFromParcel(uidTrafficParcel);
- uidTrafficParcel.recycle();
- return traffic;
+ UidTraffic traffic = UidTraffic.CREATOR.createFromParcel(uidTrafficParcel);
+ uidTrafficParcel.recycle();
+ return traffic;
+ }
}
private BluetoothActivityEnergyInfo createBtEnergyInfo(long timestamp, int stackState,
long txTime, long rxTime, long idleTime, long energyUsed, List<UidTraffic> traffic) {
- final Parcel btActivityEnergyInfoParcel = Parcel.obtain();
- btActivityEnergyInfoParcel.writeLong(timestamp);
- btActivityEnergyInfoParcel.writeInt(stackState);
- btActivityEnergyInfoParcel.writeLong(txTime);
- btActivityEnergyInfoParcel.writeLong(rxTime);
- btActivityEnergyInfoParcel.writeLong(idleTime);
- btActivityEnergyInfoParcel.writeLong(energyUsed);
- btActivityEnergyInfoParcel.writeTypedList(traffic);
- btActivityEnergyInfoParcel.setDataPosition(0);
+ if (RavenwoodRule.isUnderRavenwood()) {
+ BluetoothActivityEnergyInfo info = mock(BluetoothActivityEnergyInfo.class);
+ when(info.getTimestampMillis()).thenReturn(timestamp);
+ when(info.getBluetoothStackState()).thenReturn(stackState);
+ when(info.getControllerTxTimeMillis()).thenReturn(txTime);
+ when(info.getControllerRxTimeMillis()).thenReturn(rxTime);
+ when(info.getControllerIdleTimeMillis()).thenReturn(idleTime);
+ when(info.getControllerEnergyUsed()).thenReturn(energyUsed);
+ when(info.getUidTraffic()).thenReturn(ImmutableList.copyOf(traffic));
+ return info;
+ } else {
+ final Parcel btActivityEnergyInfoParcel = Parcel.obtain();
+ btActivityEnergyInfoParcel.writeLong(timestamp);
+ btActivityEnergyInfoParcel.writeInt(stackState);
+ btActivityEnergyInfoParcel.writeLong(txTime);
+ btActivityEnergyInfoParcel.writeLong(rxTime);
+ btActivityEnergyInfoParcel.writeLong(idleTime);
+ btActivityEnergyInfoParcel.writeLong(energyUsed);
+ btActivityEnergyInfoParcel.writeTypedList(traffic);
+ btActivityEnergyInfoParcel.setDataPosition(0);
- BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR
- .createFromParcel(btActivityEnergyInfoParcel);
- btActivityEnergyInfoParcel.recycle();
- return info;
+ BluetoothActivityEnergyInfo info = BluetoothActivityEnergyInfo.CREATOR
+ .createFromParcel(btActivityEnergyInfoParcel);
+ btActivityEnergyInfoParcel.recycle();
+ return info;
+ }
}
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java
index ccace40..29e2f5e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BstatsCpuTimesValidationTest.java
@@ -75,6 +75,7 @@
@LargeTest
@RunWith(AndroidJUnit4.class)
+@android.platform.test.annotations.IgnoreUnderRavenwood
public class BstatsCpuTimesValidationTest {
private static final String TAG = BstatsCpuTimesValidationTest.class.getSimpleName();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
index 5fce32f0..7225f2d 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CameraPowerCalculatorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -34,12 +35,17 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CameraPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP1_UID = Process.FIRST_APPLICATION_UID + 42;
private static final int APP2_UID = Process.FIRST_APPLICATION_UID + 43;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_CAMERA, 360.0)
.initMeasuredEnergyStatsLocked();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
index 993d834..5c0e268 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuAggregatedPowerStatsProcessorTest.java
@@ -33,6 +33,7 @@
import android.os.BatteryConsumer;
import android.os.PersistableBundle;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.util.LongArray;
import androidx.test.filters.SmallTest;
@@ -55,7 +56,12 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CpuAggregatedPowerStatsProcessorTest {
- @Rule
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720)
.setCpuScalingPolicy(0, new int[]{0, 1}, new int[]{100, 200})
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java
index 888bc62..71a65c8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerCalculatorTest.java
@@ -32,6 +32,7 @@
import android.os.BatteryUsageStatsQuery;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.util.SparseArray;
import androidx.test.filters.SmallTest;
@@ -55,6 +56,11 @@
@SmallTest
@SuppressWarnings("GuardedBy")
public class CpuPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42;
@@ -62,7 +68,7 @@
private static final int NUM_CPU_FREQS = 2 + 2; // 2 clusters * 2 freqs each
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_CPU_ACTIVE, 720)
.setCpuScalingPolicy(0, new int[]{0, 1}, new int[]{100, 200})
@@ -93,14 +99,13 @@
private SystemServerCpuThreadReader mMockSystemServerCpuThreadReader;
@Mock
private KernelSingleUidTimeReader mMockKernelSingleUidTimeReader;
+ private boolean[] mSupportedPowerBuckets;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- final boolean[] supportedPowerBuckets =
- new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
- supportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true;
+ mSupportedPowerBuckets = new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS];
when(mMockCpuUidFreqTimeReader.isFastCpuTimesReader()).thenReturn(true);
@@ -112,8 +117,7 @@
.setKernelCpuUidUserSysTimeReader(mMockKernelCpuUidUserSysTimeReader)
.setKernelCpuUidActiveTimeReader(mMockKerneCpuUidActiveTimeReader)
.setKernelSingleUidTimeReader(mMockKernelSingleUidTimeReader)
- .setSystemServerCpuThreadReader(mMockSystemServerCpuThreadReader)
- .initEnergyConsumerStatsLocked(supportedPowerBuckets, new String[0]);
+ .setSystemServerCpuThreadReader(mMockSystemServerCpuThreadReader);
}
@Test
@@ -216,6 +220,10 @@
@Test
public void testMeasuredEnergyBasedModel() {
+ mSupportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true;
+ mStatsRule.getBatteryStats().initEnergyConsumerStatsLocked(mSupportedPowerBuckets,
+ new String[0]);
+
when(mMockUserInfoProvider.exists(anyInt())).thenReturn(true);
when(mMockKernelCpuSpeedReaders[0].readDelta()).thenReturn(new long[]{1000, 2000});
@@ -397,6 +405,10 @@
@Test
public void testMeasuredEnergyBasedModel_perProcessState() {
+ mSupportedPowerBuckets[EnergyConsumerStats.POWER_BUCKET_CPU] = true;
+ mStatsRule.getBatteryStats().initEnergyConsumerStatsLocked(mSupportedPowerBuckets,
+ new String[0]);
+
when(mMockUserInfoProvider.exists(anyInt())).thenReturn(true);
when(mMockKernelCpuSpeedReaders[0].readDelta()).thenReturn(new long[]{1000, 2000});
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
index 38a5d19..cbce7e8 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CpuPowerStatsCollectorValidationTest.java
@@ -52,6 +52,7 @@
@RunWith(AndroidJUnit4.class)
@LargeTest
+@android.platform.test.annotations.IgnoreUnderRavenwood
public class CpuPowerStatsCollectorValidationTest {
@Rule
public final CheckFlagsRule mCheckFlagsRule =
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
index 245faaf..4ab706e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/CustomEnergyConsumerPowerCalculatorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.util.SparseLongArray;
import androidx.test.filters.SmallTest;
@@ -34,11 +35,16 @@
@SmallTest
@SuppressWarnings("GuardedBy")
public class CustomEnergyConsumerPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.initMeasuredEnergyStatsLocked(new String[]{"CUSTOM_COMPONENT1", "CUSTOM_COMPONENT2"});
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java
index 0f85fdc..757025e 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/FlashlightPowerCalculatorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -34,11 +35,16 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class FlashlightPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_FLASHLIGHT, 360.0);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java
index 3f2a6d0..3b5658c 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/GnssPowerCalculatorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -35,12 +36,16 @@
@SmallTest
@SuppressWarnings("GuardedBy")
public class GnssPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 222;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_GPS_ON, 360.0)
.setAveragePower(PowerProfile.POWER_GPS_SIGNAL_QUALITY_BASED,
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java
index 3d150af..487d864 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/IdlePowerCalculatorTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import android.os.BatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -32,9 +33,14 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class IdlePowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_CPU_IDLE, 720.0)
.setAveragePower(PowerProfile.POWER_CPU_SUSPEND, 360.0);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java
index 2edfc8e..e023866 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/KernelWakelockReaderTest.java
@@ -24,6 +24,7 @@
import java.nio.charset.Charset;
+@android.platform.test.annotations.IgnoreUnderRavenwood
public class KernelWakelockReaderTest extends TestCase {
/**
* Helper class that builds the mock Kernel module file /d/wakeup_sources.
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java
index 2e962c3..1807ac5 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterArrayTest.java
@@ -24,7 +24,6 @@
import static org.junit.Assert.assertArrayEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.os.Parcel;
@@ -159,7 +158,7 @@
// Test with detachIfReset=false
mCounterArray.reset(false /* detachIfReset */);
assertArrayEquals(ZEROES, mCounterArray.mCounts);
- verifyZeroInteractions(mTimeBase);
+ verifyNoMoreInteractions(mTimeBase);
updateCounts(COUNTS);
// Test with detachIfReset=true
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java
index 0eac625..4b608e3 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/LongSamplingCounterTest.java
@@ -24,7 +24,6 @@
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.os.Parcel;
@@ -140,7 +139,7 @@
// Test with detachIfReset=false
mCounter.reset(false /* detachIfReset */);
assertEquals(0, getCount());
- verifyZeroInteractions(mTimeBase);
+ verifyNoMoreInteractions(mTimeBase);
mCounter.addCountLocked(COUNT, true);
assertEquals(COUNT, getCount());
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java
index 2cce449..3a27188 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MemoryPowerCalculatorTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import android.os.BatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -32,9 +33,14 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MemoryPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_MEMORY, new double[] {360.0, 720.0, 1080.0});
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
index 78c4bac..9f06913 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/MockBatteryStatsImpl.java
@@ -26,6 +26,7 @@
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.BatteryStatsHistory;
import com.android.internal.os.Clock;
import com.android.internal.os.CpuScalingPolicies;
import com.android.internal.os.KernelCpuSpeedReader;
@@ -70,7 +71,9 @@
MockBatteryStatsImpl(Clock clock, File historyDirectory, Handler handler,
PowerStatsUidResolver powerStatsUidResolver) {
- super(clock, historyDirectory, handler, powerStatsUidResolver);
+ super(clock, historyDirectory, handler, powerStatsUidResolver,
+ mock(FrameworkStatsLogger.class), mock(BatteryStatsHistory.TraceDelegate.class),
+ mock(BatteryStatsHistory.EventLogger.class));
initTimersAndCounters();
setMaxHistoryBuffer(128 * 1024);
@@ -107,7 +110,9 @@
}
public Queue<UidToRemove> getPendingRemovedUids() {
- return mPendingRemovedUids;
+ synchronized (this) {
+ return mPendingRemovedUids;
+ }
}
public boolean isOnBattery() {
@@ -275,6 +280,10 @@
mHandler = handler;
}
+ @Override
+ protected void updateBatteryPropertiesLocked() {
+ }
+
public static class DummyExternalStatsSync implements ExternalStatsSync {
public int flags = 0;
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
index 22a7351..af5b462 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsAggregatorTest.java
@@ -60,7 +60,7 @@
public void setup() throws ParseException {
mHistory = new BatteryStatsHistory(32, 1024,
mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock,
- mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class));
+ mMonotonicClock, mock(BatteryStatsHistory.TraceDelegate.class), null);
AggregatedPowerStatsConfig config = new AggregatedPowerStatsConfig();
config.trackPowerComponent(TEST_POWER_COMPONENT)
@@ -178,7 +178,7 @@
}
@NonNull
- private static CharSequence formatDateTime(long timeInMillis) {
+ private static String formatDateTime(long timeInMillis) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
format.getCalendar().setTimeZone(TimeZone.getTimeZone("GMT"));
return format.format(new Date(timeInMillis));
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
index 3560a26..18d7b90 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/PowerStatsExporterTest.java
@@ -99,7 +99,7 @@
mPowerStatsStore = new PowerStatsStore(storeDirectory, new TestHandler(), config);
mHistory = new BatteryStatsHistory(Parcel.obtain(), storeDirectory, 0, 10000,
mock(BatteryStatsHistory.HistoryStepDetailsCalculator.class), mClock,
- mMonotonicClock, null);
+ mMonotonicClock, null, null);
mPowerStatsAggregator = new PowerStatsAggregator(config, mHistory);
mCpuStatsArrayLayout = new CpuPowerStatsCollector.CpuStatsArrayLayout();
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java
index 3723079..88d4ea7 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/ScreenPowerCalculatorTest.java
@@ -25,6 +25,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.view.Display;
import androidx.test.filters.SmallTest;
@@ -38,14 +39,18 @@
@SmallTest
@SuppressWarnings("GuardedBy")
public class ScreenPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID1 = Process.FIRST_APPLICATION_UID + 42;
private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 43;
private static final long MINUTE_IN_MS = 60 * 1000;
private static final long MINUTE_IN_US = 60 * 1000 * 1000;
- private static final long HOUR_IN_MS = 60 * MINUTE_IN_MS;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_ON, 0, 36.0)
.setAveragePowerForOrdinal(POWER_GROUP_DISPLAY_SCREEN_FULL, 0, 48.0)
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java
index 4745270..c01f05f 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SensorPowerCalculatorTest.java
@@ -27,6 +27,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -40,6 +41,11 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class SensorPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int SENSOR_HANDLE_1 = 1;
@@ -47,7 +53,7 @@
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
@Test
@@ -60,10 +66,12 @@
.thenReturn(List.of(sensor1, sensor2));
final BatteryStatsImpl stats = mStatsRule.getBatteryStats();
- stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_1, 1000, 1000);
- stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_1, 2000, 2000);
- stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_2, 3000, 3000);
- stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_2, 5000, 5000);
+ synchronized (stats) {
+ stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_1, 1000, 1000);
+ stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_1, 2000, 2000);
+ stats.noteStartSensorLocked(APP_UID, SENSOR_HANDLE_2, 3000, 3000);
+ stats.noteStopSensorLocked(APP_UID, SENSOR_HANDLE_2, 5000, 5000);
+ }
SensorPowerCalculator calculator = new SensorPowerCalculator(sensorManager);
@@ -84,11 +92,20 @@
.isWithin(PRECISION).of(0.5);
}
- private Sensor createSensor(int handle, int type, double power) {
- return new Sensor(new InputSensorInfo("name", "vendor", 0 /* version */,
- handle, type, 100.0f /*maxRange */, 0.02f /* resolution */,
- (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */,
- 0 /* fifoMaxEventCount */, "" /* stringType */, "" /* requiredPermission */,
- 0 /* maxDelay */, 0 /* flags */, 0 /* id */));
+ private Sensor createSensor(int handle, int type, float power) {
+ if (RavenwoodRule.isUnderRavenwood()) {
+ Sensor sensor = mock(Sensor.class);
+
+ when(sensor.getHandle()).thenReturn(handle);
+ when(sensor.getType()).thenReturn(type);
+ when(sensor.getPower()).thenReturn(power);
+ return sensor;
+ } else {
+ return new Sensor(new InputSensorInfo("name", "vendor", 0 /* version */,
+ handle, type, 100.0f /*maxRange */, 0.02f /* resolution */,
+ (float) power, 1000 /* minDelay */, 0 /* fifoReservedEventCount */,
+ 0 /* fifoMaxEventCount */, "" /* stringType */, "" /* requiredPermission */,
+ 0 /* maxDelay */, 0 /* flags */, 0 /* id */));
+ }
}
}
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java
index f14745e..438f0ec 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/UserPowerCalculatorTest.java
@@ -25,6 +25,7 @@
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
import android.os.UserHandle;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -36,6 +37,11 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UserPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
public static final int USER1 = 0;
public static final int USER2 = 1625;
@@ -43,7 +49,7 @@
private static final int APP_UID2 = Process.FIRST_APPLICATION_UID + 272;
private static final int APP_UID3 = Process.FIRST_APPLICATION_UID + 314;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule();
@Test
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java
index f578aa3..b9b7101 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/VideoPowerCalculatorTest.java
@@ -21,6 +21,7 @@
import android.os.BatteryConsumer;
import android.os.Process;
import android.os.UidBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -34,11 +35,16 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VideoPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_VIDEO, 360.0);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java
index f196185..5b7762d 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerCalculatorTest.java
@@ -23,6 +23,7 @@
import android.os.Process;
import android.os.UidBatteryConsumer;
import android.os.WorkSource;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -36,12 +37,17 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class WakelockPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
private static final int APP_PID = 3145;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_CPU_IDLE, 360.0);
@@ -51,10 +57,12 @@
BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats();
- batteryStats.noteStartWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", "",
- BatteryStats.WAKE_TYPE_PARTIAL, true, 1000, 1000);
- batteryStats.noteStopWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake", "",
- BatteryStats.WAKE_TYPE_PARTIAL, 2000, 2000);
+ synchronized (batteryStats) {
+ batteryStats.noteStartWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake",
+ "", BatteryStats.WAKE_TYPE_PARTIAL, true, 1000, 1000);
+ batteryStats.noteStopWakeFromSourceLocked(new WorkSource(APP_UID), APP_PID, "awake",
+ "", BatteryStats.WAKE_TYPE_PARTIAL, 2000, 2000);
+ }
mStatsRule.setTime(10_000, 6_000);
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java
index 113be8b..8e221be 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WifiPowerCalculatorTest.java
@@ -23,6 +23,9 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
import android.app.usage.NetworkStatsManager;
import android.net.NetworkCapabilities;
import android.net.NetworkStats;
@@ -33,6 +36,7 @@
import android.os.UidBatteryConsumer;
import android.os.WorkSource;
import android.os.connectivity.WifiActivityEnergyInfo;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -44,10 +48,17 @@
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import java.util.List;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
@SuppressWarnings("GuardedBy")
public class WifiPowerCalculatorTest {
+ @Rule(order = 0)
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setProvideMainThread(true)
+ .build();
+
private static final double PRECISION = 0.00001;
private static final int APP_UID = Process.FIRST_APPLICATION_UID + 42;
@@ -55,7 +66,7 @@
@Mock
NetworkStatsManager mNetworkStatsManager;
- @Rule
+ @Rule(order = 1)
public final BatteryUsageStatsRule mStatsRule = new BatteryUsageStatsRule()
.setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE, 360.0)
.setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX, 480.0)
@@ -64,6 +75,7 @@
.setAveragePower(PowerProfile.POWER_WIFI_SCAN, 480.0)
.setAveragePower(PowerProfile.POWER_WIFI_BATCHED_SCAN, 720.0)
.setAveragePower(PowerProfile.POWER_WIFI_ACTIVE, 1080.0)
+ .setAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE, 3700)
.initMeasuredEnergyStatsLocked();
/** Sets up a batterystats object with pre-populated network values. */
@@ -78,21 +90,54 @@
return batteryStats;
}
- private NetworkStats buildNetworkStats(long elapsedRealtime, int rxBytes, int rxPackets,
- int txBytes, int txPackets) {
- return new NetworkStats(elapsedRealtime, 1)
- .addEntry(new NetworkStats.Entry("wifi", APP_UID, 0, 0,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets,
- txBytes, txPackets, 100))
- .addEntry(new NetworkStats.Entry("wifi", Process.WIFI_UID, 0, 0,
- METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1111, 111,
- 2222, 22, 111));
+ private NetworkStats buildNetworkStats(long elapsedRealtime, long rxBytes, long rxPackets,
+ long txBytes, long txPackets) {
+ if (RavenwoodRule.isUnderRavenwood()) {
+ NetworkStats stats = mock(NetworkStats.class);
+// when(stats.getElapsedRealtime()).thenReturn(elapsedRealtime);
+
+ NetworkStats.Entry entry1 = mock(NetworkStats.Entry.class);
+// when(entry1.getIface()).thenReturn("wifi");
+ when(entry1.getUid()).thenReturn(APP_UID);
+ when(entry1.getMetered()).thenReturn(METERED_NO);
+ when(entry1.getRoaming()).thenReturn(ROAMING_NO);
+ when(entry1.getDefaultNetwork()).thenReturn(DEFAULT_NETWORK_NO);
+ when(entry1.getRxBytes()).thenReturn(rxBytes);
+ when(entry1.getRxPackets()).thenReturn(rxPackets);
+ when(entry1.getTxBytes()).thenReturn(txBytes);
+ when(entry1.getTxPackets()).thenReturn(txPackets);
+ when(entry1.getOperations()).thenReturn(100L);
+
+ NetworkStats.Entry entry2 = mock(NetworkStats.Entry.class);
+// when(entry2.getIface()).thenReturn("wifi");
+ when(entry2.getUid()).thenReturn(Process.WIFI_UID);
+ when(entry2.getMetered()).thenReturn(METERED_NO);
+ when(entry2.getRoaming()).thenReturn(ROAMING_NO);
+ when(entry2.getDefaultNetwork()).thenReturn(DEFAULT_NETWORK_NO);
+ when(entry2.getRxBytes()).thenReturn(1111L);
+ when(entry2.getRxPackets()).thenReturn(111L);
+ when(entry2.getTxBytes()).thenReturn(2222L);
+ when(entry2.getTxPackets()).thenReturn(22L);
+ when(entry2.getOperations()).thenReturn(111L);
+
+ when(stats.iterator()).thenAnswer(inv->List.of(entry1, entry2).iterator());
+
+ return stats;
+ } else {
+ return new NetworkStats(elapsedRealtime, 1)
+ .addEntry(new NetworkStats.Entry("wifi", APP_UID, 0, 0,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, rxPackets,
+ txBytes, txPackets, 100))
+ .addEntry(new NetworkStats.Entry("wifi", Process.WIFI_UID, 0, 0,
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 1111, 111,
+ 2222, 22, 111));
+ }
}
/** Sets up an WifiActivityEnergyInfo for ActivityController-model-based tests. */
private WifiActivityEnergyInfo setupPowerControllerBasedModelEnergyNumbersInfo() {
- return new WifiActivityEnergyInfo(10000,
- WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000);
+ return buildWifiActivityEnergyInfo(10000L, WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE,
+ 1000L, 2000L, 3000L, 4000L);
}
@Test
@@ -142,7 +187,7 @@
uid.setProcessStateForTest(
BatteryStats.Uid.PROCESS_STATE_FOREGROUND, 1000);
- batteryStats.updateWifiState(new WifiActivityEnergyInfo(2000,
+ batteryStats.updateWifiState(buildWifiActivityEnergyInfo(2000,
WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000),
POWER_DATA_UNAVAILABLE, 2000, 2000,
mNetworkStatsManager);
@@ -152,7 +197,7 @@
mStatsRule.setNetworkStats(buildNetworkStats(4000, 5000, 200, 7000, 80));
- batteryStats.updateWifiState(new WifiActivityEnergyInfo(4000,
+ batteryStats.updateWifiState(buildWifiActivityEnergyInfo(4000,
WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 5000, 6000, 7000, 8000),
POWER_DATA_UNAVAILABLE, 4000, 4000,
mNetworkStatsManager);
@@ -231,7 +276,7 @@
uid.setProcessStateForTest(
BatteryStats.Uid.PROCESS_STATE_FOREGROUND, 1000);
- batteryStats.updateWifiState(new WifiActivityEnergyInfo(2000,
+ batteryStats.updateWifiState(buildWifiActivityEnergyInfo(2000,
WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 1000, 2000, 3000, 4000),
1_000_000, 2000, 2000,
mNetworkStatsManager);
@@ -241,7 +286,7 @@
mStatsRule.setNetworkStats(buildNetworkStats(4000, 5000, 200, 7000, 80));
- batteryStats.updateWifiState(new WifiActivityEnergyInfo(4000,
+ batteryStats.updateWifiState(buildWifiActivityEnergyInfo(4000,
WifiActivityEnergyInfo.STACK_STATE_STATE_ACTIVE, 5000, 6000, 7000, 8000),
5_000_000, 4000, 4000,
mNetworkStatsManager);
@@ -329,4 +374,43 @@
assertThat(uidConsumer.getPowerModel(BatteryConsumer.POWER_COMPONENT_WIFI))
.isEqualTo(BatteryConsumer.POWER_MODEL_ENERGY_CONSUMPTION);
}
+
+ private WifiActivityEnergyInfo buildWifiActivityEnergyInfo(long timeSinceBoot,
+ int stackState, long txDuration, long rxDuration, long scanDuration,
+ long idleDuration) {
+ if (RavenwoodRule.isUnderRavenwood()) {
+ WifiActivityEnergyInfo info = mock(WifiActivityEnergyInfo.class);
+ when(info.getTimeSinceBootMillis()).thenReturn(timeSinceBoot);
+ when(info.getStackState()).thenReturn(stackState);
+ when(info.getControllerTxDurationMillis()).thenReturn(txDuration);
+ when(info.getControllerRxDurationMillis()).thenReturn(rxDuration);
+ when(info.getControllerScanDurationMillis()).thenReturn(scanDuration);
+ when(info.getControllerIdleDurationMillis()).thenReturn(idleDuration);
+ long energy = calculateEnergyMicroJoules(txDuration, rxDuration, idleDuration);
+ when(info.getControllerEnergyUsedMicroJoules()).thenReturn(energy);
+ return info;
+ } else {
+ return new WifiActivityEnergyInfo(timeSinceBoot, stackState, txDuration, rxDuration,
+ scanDuration, idleDuration);
+ }
+ }
+
+ // See WifiActivityEnergyInfo
+ private long calculateEnergyMicroJoules(
+ long txDurationMillis, long rxDurationMillis, long idleDurationMillis) {
+ PowerProfile powerProfile = mStatsRule.getPowerProfile();
+ final double idleCurrent = powerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
+ final double rxCurrent = powerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_RX);
+ final double txCurrent = powerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_TX);
+ final double voltage = powerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+
+ return (long) ((txDurationMillis * txCurrent
+ + rxDurationMillis * rxCurrent
+ + idleDurationMillis * idleCurrent)
+ * voltage);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
index 2f0257a..f86cb7b 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityServiceConnectionTest.java
@@ -16,22 +16,32 @@
package com.android.server.accessibility;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.Manifest;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.accessibilityservice.AccessibilityTrace;
+import android.accessibilityservice.BrailleDisplayController;
import android.accessibilityservice.GestureDescription;
import android.accessibilityservice.IAccessibilityServiceClient;
+import android.accessibilityservice.IBrailleDisplayConnection;
+import android.accessibilityservice.IBrailleDisplayController;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -40,6 +50,9 @@
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.hardware.display.DisplayManager;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -59,7 +72,9 @@
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Arrays;
@@ -88,21 +103,33 @@
@Mock AccessibilityUserState mMockUserState;
@Mock Context mMockContext;
- @Mock AccessibilityServiceInfo mMockServiceInfo;
+ AccessibilityServiceInfo mServiceInfo;
@Mock ResolveInfo mMockResolveInfo;
@Mock AccessibilitySecurityPolicy mMockSecurityPolicy;
- @Mock AccessibilityWindowManager mMockA11yWindowManager;
- @Mock ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
- @Mock AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
- @Mock AccessibilityTrace mMockA11yTrace;
- @Mock WindowManagerInternal mMockWindowManagerInternal;
- @Mock SystemActionPerformer mMockSystemActionPerformer;
- @Mock KeyEventDispatcher mMockKeyEventDispatcher;
+ @Mock
+ AccessibilityWindowManager mMockA11yWindowManager;
+ @Mock
+ ActivityTaskManagerInternal mMockActivityTaskManagerInternal;
+ @Mock
+ AbstractAccessibilityServiceConnection.SystemSupport mMockSystemSupport;
+ @Mock
+ AccessibilityTrace mMockA11yTrace;
+ @Mock
+ WindowManagerInternal mMockWindowManagerInternal;
+ @Mock
+ SystemActionPerformer mMockSystemActionPerformer;
+ @Mock
+ KeyEventDispatcher mMockKeyEventDispatcher;
@Mock
MagnificationProcessor mMockMagnificationProcessor;
- @Mock IBinder mMockIBinder;
- @Mock IAccessibilityServiceClient mMockServiceClient;
- @Mock MotionEventInjector mMockMotionEventInjector;
+ @Mock
+ IBinder mMockIBinder;
+ @Mock
+ IAccessibilityServiceClient mMockServiceClient;
+ @Mock
+ IBrailleDisplayController mMockBrailleDisplayController;
+ @Mock
+ MotionEventInjector mMockMotionEventInjector;
MessageCapturingHandler mHandler = new MessageCapturingHandler(null);
@@ -115,7 +142,8 @@
when(mMockSystemSupport.getMotionEventInjectorForDisplayLocked(
Display.DEFAULT_DISPLAY)).thenReturn(mMockMotionEventInjector);
- when(mMockServiceInfo.getResolveInfo()).thenReturn(mMockResolveInfo);
+ mServiceInfo = spy(new AccessibilityServiceInfo());
+ when(mServiceInfo.getResolveInfo()).thenReturn(mMockResolveInfo);
mMockResolveInfo.serviceInfo = mock(ServiceInfo.class);
mMockResolveInfo.serviceInfo.applicationInfo = mock(ApplicationInfo.class);
@@ -125,11 +153,12 @@
.thenReturn(new DisplayManager(mMockContext));
mConnection = new AccessibilityServiceConnection(mMockUserState, mMockContext,
- COMPONENT_NAME, mMockServiceInfo, SERVICE_ID, mHandler, new Object(),
+ COMPONENT_NAME, mServiceInfo, SERVICE_ID, mHandler, new Object(),
mMockSecurityPolicy, mMockSystemSupport, mMockA11yTrace,
mMockWindowManagerInternal, mMockSystemActionPerformer,
mMockA11yWindowManager, mMockActivityTaskManagerInternal);
when(mMockSecurityPolicy.canPerformGestures(mConnection)).thenReturn(true);
+ when(mMockSecurityPolicy.checkAccessibilityAccess(mConnection)).thenReturn(true);
}
@After
@@ -286,4 +315,135 @@
verify(mMockMagnificationProcessor).resetAllIfNeeded(anyInt());
}
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectBluetoothBrailleDisplay() throws Exception {
+ final String macAddress = "00:11:22:33:AA:BB";
+ final byte[] descriptor = {0x05, 0x41};
+ Bundle bd = new Bundle();
+ bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+ bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+ bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, macAddress);
+ bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true);
+ mConnection.setTestBrailleDisplayData(List.of(bd));
+
+ mConnection.connectBluetoothBrailleDisplay(macAddress, mMockBrailleDisplayController);
+
+ ArgumentCaptor<IBrailleDisplayConnection> connection =
+ ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+ verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+ // Cleanup the connection.
+ connection.getValue().disconnect();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectBluetoothBrailleDisplay_throwsForMissingBluetoothConnectPermission() {
+ doThrow(SecurityException.class).when(mMockContext)
+ .enforceCallingPermission(eq(Manifest.permission.BLUETOOTH_CONNECT), any());
+
+ assertThrows(SecurityException.class,
+ () -> mConnection.connectBluetoothBrailleDisplay("unused",
+ mMockBrailleDisplayController));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectBluetoothBrailleDisplay_throwsForNullMacAddress() {
+ assertThrows(NullPointerException.class,
+ () -> mConnection.connectBluetoothBrailleDisplay(null,
+ mMockBrailleDisplayController));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectBluetoothBrailleDisplay_throwsForMisformattedMacAddress() {
+ assertThrows(IllegalArgumentException.class,
+ () -> mConnection.connectBluetoothBrailleDisplay("12:34",
+ mMockBrailleDisplayController));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectUsbBrailleDisplay() throws Exception {
+ final String serialNumber = "myUsbDevice";
+ final byte[] descriptor = {0x05, 0x41};
+ Bundle bd = new Bundle();
+ bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, "/dev/null");
+ bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, descriptor);
+ bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, serialNumber);
+ bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, false);
+ mConnection.setTestBrailleDisplayData(List.of(bd));
+ UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+ when(usbDevice.getSerialNumber()).thenReturn(serialNumber);
+ UsbManager usbManager = Mockito.mock(UsbManager.class);
+ when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+ when(usbManager.hasPermission(eq(usbDevice), eq(COMPONENT_NAME.getPackageName()),
+ anyInt(), anyInt())).thenReturn(true);
+
+ mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+ ArgumentCaptor<IBrailleDisplayConnection> connection =
+ ArgumentCaptor.forClass(IBrailleDisplayConnection.class);
+ verify(mMockBrailleDisplayController).onConnected(connection.capture(), eq(descriptor));
+ // Cleanup the connection.
+ connection.getValue().disconnect();
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectUsbBrailleDisplay_throwsForMissingUsbPermission() {
+ UsbManager usbManager = Mockito.mock(UsbManager.class);
+ when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+ when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+ anyInt(), anyInt())).thenReturn(false);
+
+ assertThrows(SecurityException.class,
+ () -> mConnection.connectUsbBrailleDisplay(Mockito.mock(UsbDevice.class),
+ mMockBrailleDisplayController));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectUsbBrailleDisplay_throwsForNullDevice() {
+ assertThrows(NullPointerException.class,
+ () -> mConnection.connectUsbBrailleDisplay(null, mMockBrailleDisplayController));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID)
+ public void connectUsbBrailleDisplay_callsOnConnectionFailedForEmptySerialNumber()
+ throws Exception {
+ UsbManager usbManager = Mockito.mock(UsbManager.class);
+ when(mMockContext.getSystemService(Context.USB_SERVICE)).thenReturn(usbManager);
+ when(usbManager.hasPermission(notNull(), eq(COMPONENT_NAME.getPackageName()),
+ anyInt(), anyInt())).thenReturn(true);
+ UsbDevice usbDevice = Mockito.mock(UsbDevice.class);
+ when(usbDevice.getSerialNumber()).thenReturn("");
+
+ mConnection.connectUsbBrailleDisplay(usbDevice, mMockBrailleDisplayController);
+
+ verify(mMockBrailleDisplayController).onConnectionFailed(
+ BrailleDisplayController.BrailleDisplayCallback
+ .FLAG_ERROR_BRAILLE_DISPLAY_NOT_FOUND);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_RESETTABLE_DYNAMIC_PROPERTIES)
+ public void binderDied_resetA11yServiceInfo() {
+ final int flag = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS;
+ setServiceBinding(COMPONENT_NAME);
+ mConnection.bindLocked();
+ mConnection.onServiceConnected(COMPONENT_NAME, mMockIBinder);
+ AccessibilityServiceInfo info = mConnection.getServiceInfo();
+ assertThat(info.flags & flag).isEqualTo(0);
+
+ info = mConnection.getServiceInfo();
+ info.flags |= flag;
+ mConnection.setServiceInfo(info);
+ assertThat(mConnection.getServiceInfo().flags & flag).isEqualTo(flag);
+
+ mConnection.binderDied();
+ assertThat(mConnection.getServiceInfo().flags & flag).isEqualTo(0);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
new file mode 100644
index 0000000..7c278ce
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.accessibility;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.accessibilityservice.BrailleDisplayController;
+import android.os.Bundle;
+import android.testing.DexmakerShareClassLoaderRule;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.nio.file.Path;
+import java.util.List;
+
+/**
+ * Tests for internal details of {@link BrailleDisplayConnection}.
+ *
+ * <p>Prefer adding new tests in CTS where possible.
+ */
+public class BrailleDisplayConnectionTest {
+ private static final Path NULL_PATH = Path.of("/dev/null");
+
+ private BrailleDisplayConnection mBrailleDisplayConnection;
+ @Mock
+ private BrailleDisplayConnection.NativeInterface mNativeInterface;
+ @Mock
+ private AccessibilityServiceConnection mServiceConnection;
+
+ @Rule
+ public final Expect expect = Expect.create();
+
+ // To mock package-private class
+ @Rule
+ public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
+ new DexmakerShareClassLoaderRule();
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mBrailleDisplayConnection = new BrailleDisplayConnection(new Object(), mServiceConnection);
+ }
+
+ @Test
+ public void defaultNativeScanner_getReportDescriptor_returnsDescriptor() {
+ int descriptorSize = 4;
+ byte[] descriptor = {0xB, 0xE, 0xE, 0xF};
+ when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(descriptorSize);
+ when(mNativeInterface.getHidrawDesc(anyInt(), eq(descriptorSize))).thenReturn(descriptor);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isEqualTo(descriptor);
+ }
+
+ @Test
+ public void defaultNativeScanner_getReportDescriptor_invalidSize_returnsNull() {
+ when(mNativeInterface.getHidrawDescSize(anyInt())).thenReturn(0);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getDeviceReportDescriptor(NULL_PATH)).isNull();
+ }
+
+ @Test
+ public void defaultNativeScanner_getUniqueId_returnsUniq() {
+ String macAddress = "12:34:56:78";
+ when(mNativeInterface.getHidrawUniq(anyInt())).thenReturn(macAddress);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getUniqueId(NULL_PATH)).isEqualTo(macAddress);
+ }
+
+ @Test
+ public void defaultNativeScanner_getDeviceBusType_busUsb() {
+ when(mNativeInterface.getHidrawBusType(anyInt()))
+ .thenReturn(BrailleDisplayConnection.BUS_USB);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getDeviceBusType(NULL_PATH))
+ .isEqualTo(BrailleDisplayConnection.BUS_USB);
+ }
+
+ @Test
+ public void defaultNativeScanner_getDeviceBusType_busBluetooth() {
+ when(mNativeInterface.getHidrawBusType(anyInt()))
+ .thenReturn(BrailleDisplayConnection.BUS_BLUETOOTH);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+ assertThat(scanner.getDeviceBusType(NULL_PATH))
+ .isEqualTo(BrailleDisplayConnection.BUS_BLUETOOTH);
+ }
+
+ // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
+ // test Braille display data, but its own implementation should also be tested
+ // so that issues in this helper don't cause confusing failures in CTS.
+ @Test
+ public void setTestData_scannerReturnsTestData() {
+ Bundle bd1 = new Bundle(), bd2 = new Bundle();
+
+ Path path1 = Path.of("/dev/path1"), path2 = Path.of("/dev/path2");
+ bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path1.toString());
+ bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH, path2.toString());
+ byte[] desc1 = {0xB, 0xE}, desc2 = {0xE, 0xF};
+ bd1.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc1);
+ bd2.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR, desc2);
+ String uniq1 = "uniq1", uniq2 = "uniq2";
+ bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
+ bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+ int bus1 = BrailleDisplayConnection.BUS_USB, bus2 = BrailleDisplayConnection.BUS_BLUETOOTH;
+ bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+ bus1 == BrailleDisplayConnection.BUS_BLUETOOTH);
+ bd2.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
+ bus2 == BrailleDisplayConnection.BUS_BLUETOOTH);
+
+ BrailleDisplayConnection.BrailleDisplayScanner scanner =
+ mBrailleDisplayConnection.setTestData(List.of(bd1, bd2));
+
+ expect.that(scanner.getHidrawNodePaths()).containsExactly(path1, path2);
+ expect.that(scanner.getDeviceReportDescriptor(path1)).isEqualTo(desc1);
+ expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
+ expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
+ expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+ expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
+ expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 26934d8..4307ec5 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -682,19 +682,19 @@
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
setUpAndStartUserInBackground(TEST_USER_ID);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ null, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID1);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID2);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ null, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID3);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
}
@@ -739,21 +739,21 @@
mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ true);
- // delayedLocking set and no KeyEvictedCallback, so it should not lock.
+ // allowDelayedLocking set and no KeyEvictedCallback, so it should not lock.
setUpAndStartUserInBackground(TEST_USER_ID);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ null, /* expectLocking= */ false);
setUpAndStartUserInBackground(TEST_USER_ID1);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID2);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ null, /* expectLocking= */ true);
setUpAndStartUserInBackground(TEST_USER_ID3);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* delayedLocking= */ false,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID3, /* allowDelayedLocking= */ false,
/* keyEvictedCallback= */ mKeyEvictedCallback, /* expectLocking= */ true);
}
@@ -843,7 +843,7 @@
mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ false);
}
@@ -855,19 +855,20 @@
mSetFlagsRule.disableFlags(
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ true);
mSetFlagsRule.disableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE);
mSetFlagsRule.enableFlags(
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_PRIVATE);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID2, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ true);
}
+ /** Delayed-locking users (as opposed to devices) have no limits on how many can be unlocked. */
@Test
- public void testStopPrivateProfileWithDelayedLocking_maxRunningUsersBreached()
+ public void testStopPrivateProfileWithDelayedLocking_imperviousToNumberOfRunningUsers()
throws Exception {
mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
/* maxRunningUsers= */ 1, /* delayUserDataLocking= */ false);
@@ -875,10 +876,14 @@
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_PRIVATE);
setUpAndStartProfileInBackground(TEST_USER_ID2, UserManager.USER_TYPE_PROFILE_MANAGED);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
- /* keyEvictedCallback */ null, /* expectLocking= */ true);
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
+ /* keyEvictedCallback */ null, /* expectLocking= */ false);
}
+ /**
+ * Tests that when a device/user (managed profile) does not permit delayed locking, then
+ * even if allowDelayedLocking is true, the user will still be locked.
+ */
@Test
public void testStopManagedProfileWithDelayedLocking() throws Exception {
mUserController.setInitialConfig(/* mUserSwitchUiEnabled */ true,
@@ -886,7 +891,7 @@
mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
android.multiuser.Flags.FLAG_ENABLE_BIOMETRICS_TO_UNLOCK_PRIVATE_SPACE);
setUpAndStartProfileInBackground(TEST_USER_ID1, UserManager.USER_TYPE_PROFILE_MANAGED);
- assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* delayedLocking= */ true,
+ assertUserLockedOrUnlockedAfterStopping(TEST_USER_ID1, /* allowDelayedLocking= */ true,
/* keyEvictedCallback */ null, /* expectLocking= */ true);
}
@@ -1087,29 +1092,29 @@
mUserStates.put(userId, mUserController.getStartedUserState(userId));
}
- private void assertUserLockedOrUnlockedAfterStopping(int userId, boolean delayedLocking,
+ private void assertUserLockedOrUnlockedAfterStopping(int userId, boolean allowDelayedLocking,
KeyEvictedCallback keyEvictedCallback, boolean expectLocking) throws Exception {
- int r = mUserController.stopUser(userId, /* force= */ true, /* delayedLocking= */
- delayedLocking, null, keyEvictedCallback);
+ int r = mUserController.stopUser(userId, /* force= */ true, /* allowDelayedLocking= */
+ allowDelayedLocking, null, keyEvictedCallback);
assertThat(r).isEqualTo(ActivityManager.USER_OP_SUCCESS);
- assertUserLockedOrUnlockedState(userId, delayedLocking, expectLocking);
+ assertUserLockedOrUnlockedState(userId, allowDelayedLocking, expectLocking);
}
private void assertProfileLockedOrUnlockedAfterStopping(int userId, boolean expectLocking)
throws Exception {
boolean profileStopped = mUserController.stopProfile(userId);
assertThat(profileStopped).isTrue();
- assertUserLockedOrUnlockedState(userId, /* delayedLocking= */ false, expectLocking);
+ assertUserLockedOrUnlockedState(userId, /* allowDelayedLocking= */ false, expectLocking);
}
- private void assertUserLockedOrUnlockedState(int userId, boolean delayedLocking,
+ private void assertUserLockedOrUnlockedState(int userId, boolean allowDelayedLocking,
boolean expectLocking) throws InterruptedException, RemoteException {
// fake all interim steps
UserState ussUser = mUserStates.get(userId);
ussUser.setState(UserState.STATE_SHUTDOWN);
// Passing delayedLocking invalidates incorrect internal data passing but currently there is
// no easy way to get that information passed through lambda.
- mUserController.finishUserStopped(ussUser, delayedLocking);
+ mUserController.finishUserStopped(ussUser, allowDelayedLocking);
waitForHandlerToComplete(FgThread.getHandler(), HANDLER_WAIT_TIME_MS);
verify(mInjector.mStorageManagerMock, times(expectLocking ? 1 : 0))
.lockCeStorage(userId);
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index fa39364..b705077 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -31,6 +31,7 @@
import static org.testng.Assert.assertThrows;
import android.annotation.NonNull;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateInfo;
import android.hardware.devicestate.DeviceStateRequest;
import android.hardware.devicestate.IDeviceStateManagerCallback;
@@ -72,7 +73,8 @@
new DeviceState(0, "DEFAULT", 0 /* flags */);
private static final DeviceState OTHER_DEVICE_STATE =
new DeviceState(1, "OTHER", 0 /* flags */);
- private static final DeviceState DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP =
+ private static final DeviceState
+ DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP =
new DeviceState(2, "DEVICE_STATE_CANCEL_WHEN_REQUESTER_NOT_ON_TOP",
DeviceState.FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP /* flags */);
// A device state that is not reported as being supported for the default test provider.
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
index b3d25f2..cfdb586 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/OverrideRequestControllerTest.java
@@ -25,6 +25,7 @@
import static junit.framework.Assert.assertNull;
import android.annotation.Nullable;
+import android.hardware.devicestate.DeviceState;
import android.hardware.devicestate.DeviceStateRequest;
import android.os.Binder;
import android.platform.test.annotations.Presubmit;
@@ -48,8 +49,10 @@
@RunWith(AndroidJUnit4.class)
public final class OverrideRequestControllerTest {
- private static final DeviceState TEST_DEVICE_STATE_ZERO = new DeviceState(0, "TEST_STATE", 0);
- private static final DeviceState TEST_DEVICE_STATE_ONE = new DeviceState(1, "TEST_STATE", 0);
+ private static final DeviceState
+ TEST_DEVICE_STATE_ZERO = new DeviceState(0, "TEST_STATE", 0);
+ private static final DeviceState
+ TEST_DEVICE_STATE_ONE = new DeviceState(1, "TEST_STATE", 0);
private TestStatusChangeListener mStatusListener;
private OverrideRequestController mController;
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index 7e40f96..16909ab 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -40,12 +40,12 @@
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceState;
import android.os.PowerManager;
import androidx.annotation.NonNull;
import com.android.server.LocalServices;
-import com.android.server.devicestate.DeviceState;
import com.android.server.devicestate.DeviceStateProvider;
import com.android.server.input.InputManagerInternal;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 344a4b0..4dded1d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -16,6 +16,7 @@
package com.android.server.notification;
import static android.content.Context.DEVICE_POLICY_SERVICE;
+import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR;
import static android.os.UserManager.USER_TYPE_FULL_SECONDARY;
import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
@@ -62,6 +63,7 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
+import android.platform.test.annotations.EnableFlags;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -1983,6 +1985,22 @@
new ComponentName("pkg1", "cmp1"))).isFalse();
}
+ @Test
+ @EnableFlags(FLAG_LIFETIME_EXTENSION_REFACTOR)
+ public void testManagedServiceInfoIsSystemUi() {
+ ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles, mIpm,
+ APPROVAL_BY_COMPONENT);
+
+ ManagedServices.ManagedServiceInfo service0 = service.new ManagedServiceInfo(
+ mock(IInterface.class), ComponentName.unflattenFromString("a/a"), 0, false,
+ mock(ServiceConnection.class), 26, 34);
+
+ service0.isSystemUi = true;
+ assertThat(service0.isSystemUi()).isTrue();
+ service0.isSystemUi = false;
+ assertThat(service0.isSystemUi()).isFalse();
+ }
+
private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
throws RemoteException {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 068339b..2f740ea 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -2544,6 +2544,17 @@
assertThat(mBinderService.getActiveNotifications(sbn.getPackageName()).length).isEqualTo(1);
assertThat(mService.getNotificationRecordCount()).isEqualTo(1);
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
+
mSetFlagsRule.disableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
mBinderService.cancelNotificationWithTag(PKG, PKG, sbn.getTag(), sbn.getId(),
sbn.getUserId());
@@ -2576,6 +2587,17 @@
StatusBarNotification[] notifs = mBinderService.getActiveNotifications(PKG);
assertThat(notifs.length).isEqualTo(1);
assertThat(notifs[0].getId()).isEqualTo(1);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test
@@ -2984,18 +3006,29 @@
public void testCancelNotificationsFromListener_clearAll_NoClearLifetimeExt()
throws Exception {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR);
-
final NotificationRecord notif = generateNotificationRecord(
mTestNotificationChannel, 1, null, false);
- notif.getNotification().flags = FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
+ notif.getNotification().flags |= FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY;
mService.addNotification(notif);
-
+ verify(mWorkerHandler, times(0))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
mService.getBinderService().cancelNotificationsFromListener(null, null);
waitForIdle();
-
+ // Notification not cancelled.
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
assertThat(notifs.length).isEqualTo(1);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test
@@ -3216,6 +3249,17 @@
StatusBarNotification[] notifs =
mBinderService.getActiveNotifications(notif.getSbn().getPackageName());
assertEquals(1, notifs.length);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test
@@ -5658,6 +5702,17 @@
StatusBarNotification[] notifsAfter = mBinderService.getActiveNotifications(PKG);
assertThat(notifsAfter.length).isEqualTo(1);
assertThat(mService.getNotificationRecord(notif.getKey())).isEqualTo(notif);
+
+ // Checks that a post update is sent.
+ verify(mWorkerHandler, times(1))
+ .post(any(NotificationManagerService.PostNotificationRunnable.class));
+ ArgumentCaptor<NotificationRecord> captor =
+ ArgumentCaptor.forClass(NotificationRecord.class);
+ verify(mListeners, times(1)).prepareNotifyPostedLocked(captor.capture(), any(),
+ anyBoolean());
+ assertThat(captor.getValue().getNotification().flags
+ & FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY).isEqualTo(
+ FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
index 1fb7cd8..9e2b1ec 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java
@@ -32,14 +32,17 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+import static com.android.server.wm.DragDropController.MSG_UNHANDLED_DROP_LISTENER_TIMEOUT;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.PendingIntent;
@@ -49,9 +52,12 @@
import android.content.pm.ShortcutServiceInternal;
import android.graphics.PixelFormat;
import android.os.Binder;
+import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.Message;
import android.os.Parcelable;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
import android.view.DragEvent;
@@ -61,6 +67,7 @@
import android.view.View;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager;
+import android.window.IUnhandledDragListener;
import androidx.test.filters.SmallTest;
@@ -533,14 +540,98 @@
});
}
+ @Test
+ public void testUnhandledDragListenerNotCalledForNormalDrags() throws RemoteException {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ doDragAndDrop(0, ClipData.newPlainText("label", "Test"), 0, 0);
+ verify(listener, times(0)).onUnhandledDrop(any(), any());
+ }
+
+ @Test
+ public void testUnhandledDragListenerReceivesUnhandledDropOverWindow() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+ mTarget.reportDropResult(mWindow.mClient, false);
+ mTarget.onUnhandledDropCallback(true);
+ mToken = null;
+ try {
+ verify(listener, times(1)).onUnhandledDrop(any(), any());
+ } catch (RemoteException e) {
+ fail("Failed to verify unhandled drop: " + e);
+ }
+ });
+ }
+
+ @Test
+ public void testUnhandledDragListenerReceivesUnhandledDropOverNoValidWindow() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+ mTarget.onUnhandledDropCallback(true);
+ mToken = null;
+ try {
+ verify(listener, times(1)).onUnhandledDrop(any(), any());
+ } catch (RemoteException e) {
+ fail("Failed to verify unhandled drop: " + e);
+ }
+ });
+ }
+
+ @Test
+ public void testUnhandledDragListenerCallbackTimeout() {
+ assumeTrue(com.android.window.flags.Flags.delegateUnhandledDrags());
+
+ final IUnhandledDragListener listener = mock(IUnhandledDragListener.class);
+ doReturn(mock(Binder.class)).when(listener).asBinder();
+ mTarget.setUnhandledDragListener(listener);
+ final int invalidXY = 100_000;
+ startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> {
+ // Notify the unhandled drag listener
+ mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY);
+ mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY);
+
+ // Verify that the unhandled drop listener callback timeout has been scheduled
+ final Handler handler = mTarget.getHandler();
+ assertTrue(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+
+ // Force trigger the timeout and verify that it actually cleans up the drag & timeout
+ handler.handleMessage(Message.obtain(handler, MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+ assertFalse(handler.hasMessages(MSG_UNHANDLED_DROP_LISTENER_TIMEOUT));
+ assertFalse(mTarget.dragDropActiveLocked());
+ mToken = null;
+ });
+ }
+
private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) {
startDrag(flags, data, () -> {
mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY);
- mTarget.handleMotionEvent(false, dropX, dropY);
+ mTarget.handleMotionEvent(false /* keepHandling */, dropX, dropY);
mToken = mWindow.mClient.asBinder();
});
}
+ /**
+ * Starts a drag with the given parameters, calls Runnable `r` after drag is started.
+ */
private void startDrag(int flag, ClipData data, Runnable r) {
final SurfaceSession appSession = new SurfaceSession();
try {
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index 7d8eb90..ce890f6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -33,6 +33,7 @@
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.window.TransitionInfo.FLAG_CONFIG_AT_END;
import static android.window.TransitionInfo.FLAG_FILLS_TASK;
import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW;
@@ -2540,6 +2541,36 @@
}
@Test
+ public void testConfigAtEnd() {
+ final TransitionController controller = mDisplayContent.mTransitionController;
+ Transition transit = createTestTransition(TRANSIT_CHANGE, controller);
+ final TestTransitionPlayer player = registerTestTransitionPlayer();
+
+ final Task task = createTask(mDisplayContent);
+ final Rect taskBounds = new Rect(0, 0, 200, 300);
+ task.getConfiguration().windowConfiguration.setBounds(taskBounds);
+ final ActivityRecord activity = createActivityRecord(task);
+ activity.setVisibleRequested(true);
+ activity.setVisible(true);
+
+ controller.moveToCollecting(transit);
+ transit.collect(task);
+ transit.setConfigAtEnd(task);
+ task.getRequestedOverrideConfiguration().windowConfiguration.setBounds(
+ new Rect(10, 10, 200, 300));
+ task.onRequestedOverrideConfigurationChanged(task.getRequestedOverrideConfiguration());
+
+ controller.requestStartTransition(transit, task, null, null);
+ player.start();
+ assertTrue(activity.isConfigurationDispatchPaused());
+ // config-at-end flag must propagate up to task if activity was promoted.
+ assertTrue(player.mLastReady.getChange(
+ task.mRemoteToken.toWindowContainerToken()).hasFlags(FLAG_CONFIG_AT_END));
+ player.finish();
+ assertFalse(activity.isConfigurationDispatchPaused());
+ }
+
+ @Test
public void testReadyTrackerBasics() {
final TransitionController controller = new TestTransitionController(
mock(ActivityTaskManagerService.class));
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index aa9c0c8..03b695d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -48,6 +48,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -901,7 +902,8 @@
new Binder(),
0 /* index */,
WindowInsets.Type.systemOverlays(),
- new Rect(0, 0, 1080, 200));
+ new Rect(0, 0, 1080, 200),
+ null /* boundingRects */);
mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
assertThat(navigationBarInsetsReceiverTask.mLocalInsetsSources
@@ -910,6 +912,31 @@
}
@Test
+ public void testAddInsetsSource_withBoundingRects() {
+ final Task rootTask = createTask(mDisplayContent);
+
+ final Task navigationBarInsetsReceiverTask = createTaskInRootTask(rootTask, 0);
+ navigationBarInsetsReceiverTask.getConfiguration().windowConfiguration.setBounds(new Rect(
+ 0, 200, 1080, 700));
+
+ final Rect[] boundingRects = new Rect[]{
+ new Rect(0, 0, 10, 10), new Rect(100, 100, 200, 100)
+ };
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.addInsetsSource(
+ navigationBarInsetsReceiverTask.mRemoteToken.toWindowContainerToken(),
+ new Binder(),
+ 0 /* index */,
+ WindowInsets.Type.systemOverlays(),
+ new Rect(0, 0, 1080, 200),
+ boundingRects);
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
+
+ assertArrayEquals(boundingRects, navigationBarInsetsReceiverTask.mLocalInsetsSources
+ .valueAt(0).getBoundingRects());
+ }
+
+ @Test
public void testRemoveInsetsSource() {
final Task rootTask = createTask(mDisplayContent);
@@ -923,7 +950,8 @@
owner,
0 /* index */,
WindowInsets.Type.systemOverlays(),
- new Rect(0, 0, 1080, 200));
+ new Rect(0, 0, 1080, 200),
+ null /* boundingRects */);
mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
final WindowContainerTransaction wct2 = new WindowContainerTransaction();
diff --git a/services/usb/Android.bp b/services/usb/Android.bp
index 3a0a6ab..e8ffe54 100644
--- a/services/usb/Android.bp
+++ b/services/usb/Android.bp
@@ -34,8 +34,20 @@
"android.hardware.usb-V1.2-java",
"android.hardware.usb-V1.3-java",
"android.hardware.usb-V3-java",
+ "usb_flags_lib",
],
lint: {
baseline_filename: "lint-baseline.xml",
},
}
+
+aconfig_declarations {
+ name: "usb_flags",
+ package: "com.android.server.usb.flags",
+ srcs: ["**/usb_flags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "usb_flags_lib",
+ aconfig_declarations: "usb_flags",
+}
diff --git a/services/usb/java/com/android/server/usb/UsbHandlerManager.java b/services/usb/java/com/android/server/usb/UsbHandlerManager.java
index f311274..d83ff1f 100644
--- a/services/usb/java/com/android/server/usb/UsbHandlerManager.java
+++ b/services/usb/java/com/android/server/usb/UsbHandlerManager.java
@@ -37,7 +37,7 @@
*
* @hide
*/
-class UsbHandlerManager {
+public class UsbHandlerManager {
private static final String LOG_TAG = UsbHandlerManager.class.getSimpleName();
private final Context mContext;
diff --git a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
index f916660..2ff21ad 100644
--- a/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
@@ -18,6 +18,7 @@
import static com.android.internal.app.IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE;
+import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
@@ -62,6 +63,7 @@
import com.android.internal.util.dump.DualDumpOutputStream;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
+import com.android.server.usb.flags.Flags;
import com.android.server.utils.EventLogger;
import libcore.io.IoUtils;
@@ -80,8 +82,20 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
-class UsbProfileGroupSettingsManager {
+public class UsbProfileGroupSettingsManager {
+ /**
+ * <application> level property that an app can specify to restrict any overlaying of
+ * activities when usb device is attached.
+ *
+ *
+ * <p>This should only be set by privileged apps having {@link Manifest.permission#MANAGE_USB}
+ * permission.
+ * @hide
+ */
+ public static final String PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES =
+ "android.app.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES";
private static final String TAG = UsbProfileGroupSettingsManager.class.getSimpleName();
private static final boolean DEBUG = false;
@@ -101,6 +115,8 @@
private final PackageManager mPackageManager;
+ private final ActivityManager mActivityManager;
+
private final UserManager mUserManager;
private final @NonNull UsbSettingsManager mSettingsManager;
@@ -224,7 +240,7 @@
* @param settingsManager The settings manager of the service
* @param usbResolveActivityManager The resovle activity manager of the service
*/
- UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user,
+ public UsbProfileGroupSettingsManager(@NonNull Context context, @NonNull UserHandle user,
@NonNull UsbSettingsManager settingsManager,
@NonNull UsbHandlerManager usbResolveActivityManager) {
if (DEBUG) Slog.v(TAG, "Creating settings for " + user);
@@ -238,6 +254,7 @@
mContext = context;
mPackageManager = context.getPackageManager();
+ mActivityManager = context.getSystemService(ActivityManager.class);
mSettingsManager = settingsManager;
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
@@ -895,7 +912,10 @@
// Send broadcast to running activities with registered intent
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
- resolveActivity(intent, device, true /* showMtpNotification */);
+ //resolving activities only if there is no foreground activity restricting it.
+ if (!shouldRestrictOverlayActivities()) {
+ resolveActivity(intent, device, true /* showMtpNotification */);
+ }
}
private void resolveActivity(Intent intent, UsbDevice device, boolean showMtpNotification) {
@@ -918,6 +938,63 @@
resolveActivity(intent, matches, defaultActivity, device, null);
}
+ /**
+ * @return true if any application in foreground have set restrict_usb_overlay_activities as
+ * true in manifest file. The application needs to have MANAGE_USB permission.
+ */
+ private boolean shouldRestrictOverlayActivities() {
+
+ if (!Flags.allowRestrictionOfOverlayActivities()) return false;
+
+ List<ActivityManager.RunningAppProcessInfo> appProcessInfos = mActivityManager
+ .getRunningAppProcesses();
+
+ List<String> filteredAppProcessInfos = new ArrayList<>();
+ boolean shouldRestrictOverlayActivities;
+
+ //filtering out applications in foreground.
+ for (ActivityManager.RunningAppProcessInfo processInfo : appProcessInfos) {
+ if (processInfo.importance <= ActivityManager
+ .RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+ filteredAppProcessInfos.addAll(List.of(processInfo.pkgList));
+ }
+ }
+
+ if (DEBUG) Slog.d(TAG, "packages in foreground : " + filteredAppProcessInfos);
+
+ List<String> packagesHoldingManageUsbPermission =
+ mPackageManager.getPackagesHoldingPermissions(
+ new String[]{android.Manifest.permission.MANAGE_USB},
+ PackageManager.MATCH_SYSTEM_ONLY).stream()
+ .map(packageInfo -> packageInfo.packageName).collect(Collectors.toList());
+
+ //retaining only packages that hold the required permission
+ filteredAppProcessInfos.retainAll(packagesHoldingManageUsbPermission);
+
+ if (DEBUG) {
+ Slog.d(TAG, "packages in foreground with required permission : "
+ + filteredAppProcessInfos);
+ }
+
+ shouldRestrictOverlayActivities = filteredAppProcessInfos.stream().anyMatch(pkg -> {
+ try {
+ return mPackageManager.getProperty(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES, pkg)
+ .getBoolean();
+ } catch (NameNotFoundException e) {
+ if (DEBUG) {
+ Slog.d(TAG, "property PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES "
+ + "not present for " + pkg);
+ }
+ return false;
+ }
+ });
+
+ if (shouldRestrictOverlayActivities) {
+ Slog.d(TAG, "restricting starting of usb overlay activities");
+ }
+ return shouldRestrictOverlayActivities;
+ }
+
public void deviceAttachedForFixedHandler(UsbDevice device, ComponentName component) {
final Intent intent = createDeviceAttachedIntent(device);
diff --git a/services/usb/java/com/android/server/usb/UsbSettingsManager.java b/services/usb/java/com/android/server/usb/UsbSettingsManager.java
index 8e53ff4..0b854a8 100644
--- a/services/usb/java/com/android/server/usb/UsbSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbSettingsManager.java
@@ -33,7 +33,7 @@
/**
* Maintains all {@link UsbUserSettingsManager} for all users.
*/
-class UsbSettingsManager {
+public class UsbSettingsManager {
private static final String LOG_TAG = UsbSettingsManager.class.getSimpleName();
private static final boolean DEBUG = false;
@@ -70,7 +70,7 @@
*
* @return The settings for the user
*/
- @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) {
+ public @NonNull UsbUserSettingsManager getSettingsForUser(@UserIdInt int userId) {
synchronized (mSettingsByUser) {
UsbUserSettingsManager settings = mSettingsByUser.get(userId);
if (settings == null) {
diff --git a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
index c2b8d01..be729c5 100644
--- a/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
+++ b/services/usb/java/com/android/server/usb/UsbUserSettingsManager.java
@@ -49,7 +49,7 @@
import java.util.ArrayList;
import java.util.List;
-class UsbUserSettingsManager {
+public class UsbUserSettingsManager {
private static final String TAG = UsbUserSettingsManager.class.getSimpleName();
private static final boolean DEBUG = false;
@@ -81,7 +81,7 @@
*
* @return The resolve infos of the activities that can handle the intent
*/
- List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) {
+ public List<ResolveInfo> queryIntentActivities(@NonNull Intent intent) {
return mPackageManager.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA,
mUser.getIdentifier());
}
diff --git a/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
new file mode 100644
index 0000000..ea6e502
--- /dev/null
+++ b/services/usb/java/com/android/server/usb/flags/usb_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.usb.flags"
+
+flag {
+ name: "allow_restriction_of_overlay_activities"
+ namespace: "usb"
+ description: "This flag controls the restriction of usb overlay activities"
+ bug: "307231174"
+}
diff --git a/telecomm/java/android/telecom/Call.java b/telecomm/java/android/telecom/Call.java
index 874c10c..a52614d 100644
--- a/telecomm/java/android/telecom/Call.java
+++ b/telecomm/java/android/telecom/Call.java
@@ -269,6 +269,27 @@
"android.telecom.extra.DIAGNOSTIC_MESSAGE";
/**
+ * Boolean indicating that the call is a verified business call.
+ *
+ * {@link Connection#setExtras(Bundle)} or {@link Connection#putExtras(Bundle)}
+ * should be used to notify Telecom this extra has been set.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String EXTRA_IS_BUSINESS_CALL =
+ "android.telecom.extra.IS_BUSINESS_CALL";
+
+ /**
+ * String value indicating the asserted display name reported via
+ * ImsCallProfile#EXTRA_ASSERTED_DISPLAY_NAME.
+ *
+ * {@link Connection#setExtras(Bundle)} or {@link Connection#putExtras(Bundle)}
+ * should be used to notify Telecom this extra has been set.
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String EXTRA_ASSERTED_DISPLAY_NAME =
+ "android.telecom.extra.ASSERTED_DISPLAY_NAME";
+
+ /**
* Reject reason used with {@link #reject(int)} to indicate that the user is rejecting this
* call because they have declined to answer it. This typically means that they are unable
* to answer the call at this time and would prefer it be sent to voicemail.
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index f7793f3..697c8ec 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9778,6 +9778,13 @@
public static final String KEY_SUPPORTS_CALL_COMPOSER_BOOL = "supports_call_composer_bool";
/**
+ * Indicates if the carrier supports a business call composer.
+ */
+ @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL =
+ "supports_business_call_composer_bool";
+
+ /**
* Indicates the carrier server url that serves the call composer picture.
*/
public static final String KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING =
@@ -10861,6 +10868,7 @@
sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
+ sDefaults.putBoolean(KEY_SUPPORTS_BUSINESS_CALL_COMPOSER_BOOL, false);
sDefaults.putString(KEY_CALL_COMPOSER_PICTURE_SERVER_URL_STRING, "");
sDefaults.putBoolean(KEY_USE_ACS_FOR_RCS_BOOL, false);
sDefaults.putBoolean(KEY_NETWORK_TEMP_NOT_METERED_SUPPORTED_BOOL, true);
diff --git a/telephony/java/android/telephony/DomainSelectionService.java b/telephony/java/android/telephony/DomainSelectionService.java
index 3c11da5..4ff9712 100644
--- a/telephony/java/android/telephony/DomainSelectionService.java
+++ b/telephony/java/android/telephony/DomainSelectionService.java
@@ -831,7 +831,7 @@
@NonNull String tag, @NonNull String errorLogName) {
try {
CompletableFuture.runAsync(
- () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor).join();
+ () -> TelephonyUtils.runWithCleanCallingIdentity(r), executor);
} catch (CancellationException | CompletionException e) {
Rlog.w(tag, "Binder - " + errorLogName + " exception: " + e.getMessage());
}
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 61c7a42..041822b 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -10629,20 +10629,27 @@
}
/**
- * Call composer status OFF from user setting.
+ * Call composer status <b>OFF</b> from user setting.
*/
public static final int CALL_COMPOSER_STATUS_OFF = 0;
/**
- * Call composer status ON from user setting.
+ * Call composer status <b>ON</b> from user setting.
*/
public static final int CALL_COMPOSER_STATUS_ON = 1;
+ /**
+ * Call composer status <b>Business Only</b> from user setting.
+ */
+ @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final int CALL_COMPOSER_STATUS_BUSINESS_ONLY = 2;
+
/** @hide */
@IntDef(prefix = {"CALL_COMPOSER_STATUS_"},
value = {
CALL_COMPOSER_STATUS_ON,
CALL_COMPOSER_STATUS_OFF,
+ CALL_COMPOSER_STATUS_BUSINESS_ONLY
})
@Retention(RetentionPolicy.SOURCE)
public @interface CallComposerStatus {}
@@ -10663,9 +10670,16 @@
@RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
public void setCallComposerStatus(@CallComposerStatus int status) {
- if (status > CALL_COMPOSER_STATUS_ON
- || status < CALL_COMPOSER_STATUS_OFF) {
- throw new IllegalArgumentException("requested status is invalid");
+ if (com.android.server.telecom.flags.Flags.businessCallComposer()) {
+ if (status > CALL_COMPOSER_STATUS_BUSINESS_ONLY
+ || status < CALL_COMPOSER_STATUS_OFF) {
+ throw new IllegalArgumentException("requested status is invalid");
+ }
+ } else {
+ if (status > CALL_COMPOSER_STATUS_ON
+ || status < CALL_COMPOSER_STATUS_OFF) {
+ throw new IllegalArgumentException("requested status is invalid");
+ }
}
try {
ITelephony telephony = getITelephony();
diff --git a/telephony/java/android/telephony/ims/ImsCallProfile.java b/telephony/java/android/telephony/ims/ImsCallProfile.java
index d07edeb..cebfe01 100644
--- a/telephony/java/android/telephony/ims/ImsCallProfile.java
+++ b/telephony/java/android/telephony/ims/ImsCallProfile.java
@@ -16,6 +16,7 @@
package android.telephony.ims;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -299,6 +300,16 @@
"android.telephony.ims.extra.IS_BUSINESS_CALL";
/**
+ * The vendor IMS stack populates this {@code string} extra; it is used to hold the display name
+ * passed via the P-Asserted-Identity SIP header’s display-name field
+ *
+ * Reference: RFC3325
+ */
+ @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final String EXTRA_ASSERTED_DISPLAY_NAME =
+ "android.telephony.ims.extra.ASSERTED_DISPLAY_NAME";
+
+ /**
* Values for EXTRA_OIR / EXTRA_CNAP
*/
/**
diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
index 746246c..9789082 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -17,6 +17,7 @@
package android.telephony.ims.feature;
import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -59,6 +60,7 @@
import com.android.ims.internal.IImsMultiEndpoint;
import com.android.ims.internal.IImsUt;
import com.android.internal.telephony.util.TelephonyUtils;
+import com.android.server.telecom.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -513,7 +515,8 @@
CAPABILITY_TYPE_VIDEO,
CAPABILITY_TYPE_UT,
CAPABILITY_TYPE_SMS,
- CAPABILITY_TYPE_CALL_COMPOSER
+ CAPABILITY_TYPE_CALL_COMPOSER,
+ CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY
})
@Retention(RetentionPolicy.SOURCE)
public @interface MmTelCapability {}
@@ -550,11 +553,19 @@
*/
public static final int CAPABILITY_TYPE_CALL_COMPOSER = 1 << 4;
+
+ /**
+ * This MmTelFeature supports Business-only Call Composer
+ */
+ @FlaggedApi(Flags.FLAG_BUSINESS_CALL_COMPOSER)
+ public static final int CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY = 1 << 5;
+
/**
* This is used to check the upper range of MmTel capability
* @hide
*/
- public static final int CAPABILITY_TYPE_MAX = CAPABILITY_TYPE_CALL_COMPOSER + 1;
+ public static final int CAPABILITY_TYPE_MAX =
+ CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY + 1;
/**
* @hide
@@ -601,6 +612,8 @@
builder.append(isCapable(CAPABILITY_TYPE_SMS));
builder.append(" CALL_COMPOSER: ");
builder.append(isCapable(CAPABILITY_TYPE_CALL_COMPOSER));
+ builder.append(" BUSINESS_COMPOSER_ONLY: ");
+ builder.append(isCapable(CAPABILITY_TYPE_CALL_COMPOSER_BUSINESS_ONLY));
builder.append("]");
return builder.toString();
}
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index 711be02..9441fb5 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -501,4 +501,22 @@
* SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
*/
void stopSendingNtnSignalStrength(in IIntegerConsumer resultCallback);
+
+ /**
+ * Abort all outgoing satellite datagrams which vendor service has received from Telephony
+ * framework.
+ *
+ * This API helps modem to be in sync with framework when framework times out on sending
+ * datagrams.
+ *
+ * @param resultCallback The callback to receive the error code result of the operation.
+ *
+ * Valid result codes returned:
+ * SatelliteResult:SATELLITE_RESULT_SUCCESS
+ * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
+ * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
+ * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
+ * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
+ */
+ void abortSendingSatelliteDatagrams(in IIntegerConsumer resultCallback);
}
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index abacd15..f17ff17 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -265,6 +265,14 @@
"stopSendingNtnSignalStrength");
}
+ @Override
+ public void abortSendingSatelliteDatagrams(IIntegerConsumer resultCallback)
+ throws RemoteException {
+ executeMethodAsync(
+ () -> SatelliteImplBase.this.abortSendingSatelliteDatagrams(resultCallback),
+ "abortSendingSatelliteDatagrams");
+ }
+
// Call the methods with a clean calling identity on the executor and wait indefinitely for
// the future to return.
private void executeMethodAsync(Runnable r, String errorLogName) throws RemoteException {
@@ -783,4 +791,13 @@
public void stopSendingNtnSignalStrength(@NonNull IIntegerConsumer resultCallback){
// stub implementation
}
+
+ /**
+ * Requests to abort sending satellite datagrams
+ *
+ * @param resultCallback The {@link SatelliteError} result of the operation.
+ */
+ public void abortSendingSatelliteDatagrams(@NonNull IIntegerConsumer resultCallback){
+ // stub implementation
+ }
}
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index bd47b1f..ff2ee27 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3046,13 +3046,24 @@
boolean setSatellitePointingUiClassName(in String packageName, in String className);
/**
- * This API can be used by only CTS to update the timeout duration in milliseconds whether
- * the device is aligned with the satellite for demo mode
+ * This API can be used by only CTS to override the timeout durations used by the
+ * DatagramController module.
*
* @param timeoutMillis The timeout duration in millisecond.
* @return {@code true} if the timeout duration is set successfully, {@code false} otherwise.
*/
- boolean setSatelliteDeviceAlignedTimeoutDuration(long timeoutMillis);
+ boolean setDatagramControllerTimeoutDuration(
+ boolean reset, int timeoutType, long timeoutMillis);
+
+ /**
+ * This API can be used by only CTS to override the timeout durations used by the
+ * SatelliteController module.
+ *
+ * @param timeoutMillis The timeout duration in millisecond.
+ * @return {@code true} if the timeout duration is set successfully, {@code false} otherwise.
+ */
+ boolean setSatelliteControllerTimeoutDuration(
+ boolean reset, int timeoutType, long timeoutMillis);
/**
* This API can be used in only testing to override connectivity status in monitoring emergency
diff --git a/tests/UsbManagerTests/Android.bp b/tests/UsbManagerTests/Android.bp
index c02d8e9..70c7dad 100644
--- a/tests/UsbManagerTests/Android.bp
+++ b/tests/UsbManagerTests/Android.bp
@@ -29,12 +29,17 @@
static_libs: [
"frameworks-base-testutils",
"androidx.test.rules",
- "mockito-target-inline-minus-junit4",
+ "mockito-target-extended-minus-junit4",
"platform-test-annotations",
"truth",
"UsbManagerTestLib",
],
- jni_libs: ["libdexmakerjvmtiagent"],
+ jni_libs: [
+ // Required for ExtendedMockito
+ "libdexmakerjvmtiagent",
+ "libmultiplejvmtiagentsinterferenceagent",
+ "libstaticjvmtiagent",
+ ],
certificate: "platform",
platform_apis: true,
test_suites: ["device-tests"],
diff --git a/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
new file mode 100644
index 0000000..4780d8a
--- /dev/null
+++ b/tests/UsbManagerTests/src/com/android/server/usbtest/UsbProfileGroupSettingsManagerTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.usbtest;
+
+import static com.android.server.usb.UsbProfileGroupSettingsManager.PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.Property;
+import android.content.pm.UserInfo;
+import android.content.res.Resources;
+import android.hardware.usb.UsbDevice;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.server.usb.UsbHandlerManager;
+import com.android.server.usb.UsbProfileGroupSettingsManager;
+import com.android.server.usb.UsbSettingsManager;
+import com.android.server.usb.UsbUserSettingsManager;
+import com.android.server.usb.flags.Flags;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Unit tests for {@link com.android.server.usb.UsbProfileGroupSettingsManager}.
+ * Note: MUST claim MANAGE_USB permission in Manifest
+ */
+@RunWith(AndroidJUnit4.class)
+public class UsbProfileGroupSettingsManagerTest {
+
+ private static final String TEST_PACKAGE_NAME = "testPkg";
+ @Mock
+ private Context mContext;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private ActivityManager mActivityManager;
+ @Mock
+ private UserHandle mUserHandle;
+ @Mock
+ private UsbSettingsManager mUsbSettingsManager;
+ @Mock
+ private UsbHandlerManager mUsbHandlerManager;
+ @Mock
+ private UserManager mUserManager;
+ @Mock
+ private UsbUserSettingsManager mUsbUserSettingsManager;
+ @Mock private Property mProperty;
+ private ActivityManager.RunningAppProcessInfo mRunningAppProcessInfo;
+ private PackageInfo mPackageInfo;
+ private UsbProfileGroupSettingsManager mUsbProfileGroupSettingsManager;
+ private MockitoSession mStaticMockSession;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mRunningAppProcessInfo = new ActivityManager.RunningAppProcessInfo();
+ mRunningAppProcessInfo.pkgList = new String[]{TEST_PACKAGE_NAME};
+ mPackageInfo = new PackageInfo();
+ mPackageInfo.packageName = TEST_PACKAGE_NAME;
+ mPackageInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
+
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ when(mContext.getSystemService(ActivityManager.class)).thenReturn(mActivityManager);
+ when(mContext.getResources()).thenReturn(Mockito.mock(Resources.class));
+ when(mContext.createPackageContextAsUser(anyString(), anyInt(), any(UserHandle.class)))
+ .thenReturn(mContext);
+ when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager);
+
+ mUsbProfileGroupSettingsManager = new UsbProfileGroupSettingsManager(mContext, mUserHandle,
+ mUsbSettingsManager, mUsbHandlerManager);
+
+ mStaticMockSession = ExtendedMockito.mockitoSession()
+ .mockStatic(Flags.class)
+ .strictness(Strictness.WARN)
+ .startMocking();
+
+ when(mPackageManager.getPackageInfo(TEST_PACKAGE_NAME, 0)).thenReturn(mPackageInfo);
+ when(mPackageManager.getProperty(eq(PROPERTY_RESTRICT_USB_OVERLAY_ACTIVITIES),
+ eq(TEST_PACKAGE_NAME))).thenReturn(mProperty);
+ when(mUserManager.getEnabledProfiles(anyInt()))
+ .thenReturn(List.of(Mockito.mock(UserInfo.class)));
+ when(mUsbSettingsManager.getSettingsForUser(anyInt())).thenReturn(mUsbUserSettingsManager);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mStaticMockSession.finishMocking();
+ }
+
+ @Test
+ public void testDeviceAttached_flagTrueWithoutForegroundActivity_resolveActivityCalled() {
+ when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+ when(mActivityManager.getRunningAppProcesses()).thenReturn(new ArrayList<>());
+ when(mPackageManager.getPackagesHoldingPermissions(
+ new String[]{android.Manifest.permission.MANAGE_USB},
+ PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+ UsbDevice device = Mockito.mock(UsbDevice.class);
+ mUsbProfileGroupSettingsManager.deviceAttached(device);
+ verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+ }
+
+ @Test
+ public void testDeviceAttached_noForegroundActivityWithUsbPermission_resolveActivityCalled() {
+ when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+ when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+ when(mPackageManager.getPackagesHoldingPermissions(
+ new String[]{android.Manifest.permission.MANAGE_USB},
+ PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(new ArrayList<>());
+ UsbDevice device = Mockito.mock(UsbDevice.class);
+ mUsbProfileGroupSettingsManager.deviceAttached(device);
+ verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+ }
+
+ @Test
+ public void testDeviceAttached_foregroundActivityWithManifestField_resolveActivityNotCalled() {
+ when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+ when(mProperty.getBoolean()).thenReturn(true);
+ when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+ when(mPackageManager.getPackagesHoldingPermissions(
+ new String[]{android.Manifest.permission.MANAGE_USB},
+ PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+ UsbDevice device = Mockito.mock(UsbDevice.class);
+ mUsbProfileGroupSettingsManager.deviceAttached(device);
+ verify(mUsbUserSettingsManager, times(0))
+ .queryIntentActivities(any(Intent.class));
+ }
+
+ @Test
+ public void testDeviceAttached_foregroundActivityWithoutManifestField_resolveActivityCalled() {
+ when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(true);
+ when(mProperty.getBoolean()).thenReturn(false);
+ when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+ when(mPackageManager.getPackagesHoldingPermissions(
+ new String[]{android.Manifest.permission.MANAGE_USB},
+ PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+ UsbDevice device = Mockito.mock(UsbDevice.class);
+ mUsbProfileGroupSettingsManager.deviceAttached(device);
+ verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+ }
+
+ @Test
+ public void testDeviceAttached_flagFalseForegroundActivity_resolveActivityCalled() {
+ when(Flags.allowRestrictionOfOverlayActivities()).thenReturn(false);
+ when(mProperty.getBoolean()).thenReturn(true);
+ when(mActivityManager.getRunningAppProcesses()).thenReturn(List.of(mRunningAppProcessInfo));
+ when(mPackageManager.getPackagesHoldingPermissions(
+ new String[]{android.Manifest.permission.MANAGE_USB},
+ PackageManager.MATCH_SYSTEM_ONLY)).thenReturn(List.of(mPackageInfo));
+ UsbDevice device = Mockito.mock(UsbDevice.class);
+ mUsbProfileGroupSettingsManager.deviceAttached(device);
+ verify(mUsbUserSettingsManager).queryIntentActivities(any(Intent.class));
+ }
+}
diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java
index 60c25b7..be5c84c 100644
--- a/tests/testables/src/android/testing/TestableLooper.java
+++ b/tests/testables/src/android/testing/TestableLooper.java
@@ -14,6 +14,8 @@
package android.testing;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
@@ -85,6 +87,57 @@
setupQueue(looper);
}
+ /**
+ * Wrap the given runnable so that it will run blocking on the Looper that will be set up for
+ * the given test.
+ * <p>
+ * This method is required to support any TestRule which needs to run setup and/or teardown code
+ * on the TestableLooper. Whether using {@link AndroidTestingRunner} or
+ * {@link TestWithLooperRule}, the TestRule's Statement evaluates on the test instrumentation
+ * thread, rather than the TestableLooper thread, so access to the TestableLooper is required.
+ * However, {@link #get(Object)} will return {@code null} both before and after the inner
+ * statement is evaluated:
+ * <ul>
+ * <li>Before the test {@link #get} returns {@code null} because while the TestableLooperHolder
+ * is accessible in sLoopers, it has not been initialized with an actual TestableLooper yet.
+ * This method's use of the internal LooperFrameworkMethod ensures that all setup and teardown
+ * of the TestableLooper happen as it would for all other wrapped code blocks.
+ * <li>After the test {@link #get} can return {@code null} because many tests call
+ * {@link #remove} in the teardown method. The fact that this method returns a runnable allows
+ * it to be called before the test (when the TestableLooperHolder is still in sLoopers), and
+ * then executed as teardown after the test.
+ * </ul>
+ *
+ * @param test the test instance (just like passed to {@link #get(Object)})
+ * @param runnable the operation that should eventually be run on the TestableLooper
+ * @return a runnable that will block the thread on which it is called until the given runnable
+ * is finished. Will be {@code null} if there is no looper for the given test.
+ * @hide
+ */
+ @Nullable
+ public static RunnableWithException wrapWithRunBlocking(
+ Object test, @NonNull RunnableWithException runnable) {
+ TestableLooperHolder looperHolder = sLoopers.get(test);
+ if (looperHolder == null) {
+ return null;
+ }
+ try {
+ FrameworkMethod base = new FrameworkMethod(runnable.getClass().getMethod("run"));
+ LooperFrameworkMethod wrapped = new LooperFrameworkMethod(base, looperHolder);
+ return () -> {
+ try {
+ wrapped.invokeExplosively(runnable);
+ } catch (RuntimeException | Error e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new RuntimeException(e);
+ }
+ };
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
public Looper getLooper() {
return mLooper;
}
diff --git a/tools/hoststubgen/hoststubgen/Android.bp b/tools/hoststubgen/hoststubgen/Android.bp
index 57bcc04..c6dd29c 100644
--- a/tools/hoststubgen/hoststubgen/Android.bp
+++ b/tools/hoststubgen/hoststubgen/Android.bp
@@ -180,7 +180,6 @@
"framework-minus-apex.ravenwood",
],
static_libs: [
- "core-xml-for-device",
"hoststubgen-helper-libcore-runtime.ravenwood",
],
}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
index 631fc02..eba9910 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/CursorWindow_host.java
@@ -17,6 +17,7 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
+import android.os.Parcel;
import android.util.Base64;
import java.text.DecimalFormat;
@@ -31,6 +32,7 @@
private static final HashMap<Long, CursorWindow_host> sInstances = new HashMap<>();
private static long sNextId = 1;
+ private String mName;
private int mColumnNum;
private static class Row {
String[] fields;
@@ -41,6 +43,7 @@
public static long nativeCreate(String name, int cursorWindowSize) {
CursorWindow_host instance = new CursorWindow_host();
+ instance.mName = name;
long instanceId = sNextId++;
sInstances.put(instanceId, instance);
return instanceId;
@@ -50,6 +53,10 @@
sInstances.remove(windowPtr);
}
+ public static String nativeGetName(long windowPtr) {
+ return sInstances.get(windowPtr).mName;
+ }
+
public static boolean nativeSetNumColumns(long windowPtr, int columnNum) {
sInstances.get(windowPtr).mColumnNum = columnNum;
return true;
@@ -156,4 +163,30 @@
return null;
}
}
+
+ public static void nativeWriteToParcel(long windowPtr, Parcel parcel) {
+ CursorWindow_host window = sInstances.get(windowPtr);
+ parcel.writeString(window.mName);
+ 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);
+ }
+ }
+
+ public static long nativeCreateFromParcel(Parcel parcel) {
+ long windowPtr = nativeCreate(null, 0);
+ CursorWindow_host window = sInstances.get(windowPtr);
+ window.mName = parcel.readString();
+ window.mColumnNum = parcel.readInt();
+ int rowCount = parcel.readInt();
+ for (int row = 0; row < rowCount; row++) {
+ Row r = new Row();
+ r.fields = parcel.createStringArray();
+ r.types = parcel.createIntArray();
+ window.mRows.add(r);
+ }
+ return windowPtr;
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java
index a135623..4d39d88 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongArrayMultiStateCounter_host.java
@@ -143,6 +143,16 @@
updateValue(values, timestampMs);
}
+ public void addCounts(long[] delta) {
+ if (!mEnabled) {
+ return;
+ }
+
+ for (int i = 0; i < mArrayLength; i++) {
+ mStates[mCurrentState].mCounter[i] += delta[i];
+ }
+ }
+
public void getValues(long[] values, int state) {
System.arraycopy(mStates[state].mCounter, 0, values, 0, mArrayLength);
}
@@ -331,6 +341,10 @@
LongArrayContainer_host.getInstance(containerInstanceId), timestampMs);
}
+ public static void native_addCounts(long instanceId, long containerInstanceId) {
+ getInstance(instanceId).addCounts(LongArrayContainer_host.getInstance(containerInstanceId));
+ }
+
public static void native_getCounts(long instanceId, long containerInstanceId, int state) {
getInstance(instanceId).getValues(LongArrayContainer_host.getInstance(containerInstanceId),
state);
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java
new file mode 100644
index 0000000..a5d0fc6
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/LongMultiStateCounter_host.java
@@ -0,0 +1,263 @@
+/*
+ * 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.nativesubstitution;
+
+import android.os.BadParcelableException;
+import android.os.Parcel;
+
+import java.util.HashMap;
+
+/**
+ * Native implementation substitutions for the LongMultiStateCounter class.
+ */
+public class LongMultiStateCounter_host {
+
+ /**
+ * A reimplementation of {@link com.android.internal.os.LongMultiStateCounter}, only in
+ * Java instead of native. The majority of the code (in C++) can be found in
+ * /frameworks/native/libs/battery/MultiStateCounter.h
+ */
+ private static class LongMultiStateCounterRavenwood {
+ private final int mStateCount;
+ private int mCurrentState;
+ private long mLastStateChangeTimestampMs = -1;
+ private long mLastUpdateTimestampMs = -1;
+ private boolean mEnabled = true;
+
+ private static class State {
+ private long mTimeInStateSinceUpdate;
+ private long mCounter;
+ }
+
+ private final State[] mStates;
+ private long mValue;
+
+ LongMultiStateCounterRavenwood(int stateCount) {
+ mStateCount = stateCount;
+ mStates = new State[stateCount];
+ for (int i = 0; i < mStateCount; i++) {
+ mStates[i] = new State();
+ }
+ }
+
+ public void setEnabled(boolean enabled, long timestampMs) {
+ if (enabled == mEnabled) {
+ return;
+ }
+
+ if (!enabled) {
+ setState(mCurrentState, timestampMs);
+ mEnabled = false;
+ } else {
+ if (timestampMs < mLastUpdateTimestampMs) {
+ timestampMs = mLastUpdateTimestampMs;
+ }
+
+ if (mLastStateChangeTimestampMs >= 0) {
+ mLastStateChangeTimestampMs = timestampMs;
+ }
+ mEnabled = true;
+ }
+ }
+
+ public void setState(int state, long timestampMs) {
+ if (mEnabled && mLastStateChangeTimestampMs >= 0 && mLastUpdateTimestampMs >= 0) {
+ if (timestampMs < mLastUpdateTimestampMs) {
+ timestampMs = mLastUpdateTimestampMs;
+ }
+
+ if (timestampMs >= mLastStateChangeTimestampMs) {
+ mStates[mCurrentState].mTimeInStateSinceUpdate +=
+ timestampMs - mLastStateChangeTimestampMs;
+ } else {
+ for (int i = 0; i < mStateCount; i++) {
+ mStates[i].mTimeInStateSinceUpdate = 0;
+ }
+ }
+ }
+ mCurrentState = state;
+ mLastStateChangeTimestampMs = timestampMs;
+ }
+
+ public long updateValue(long value, long timestampMs) {
+ long returnValue = 0;
+ if (mEnabled || mLastUpdateTimestampMs < mLastStateChangeTimestampMs) {
+ if (timestampMs < mLastStateChangeTimestampMs) {
+ timestampMs = mLastStateChangeTimestampMs;
+ }
+
+ setState(mCurrentState, timestampMs);
+
+ if (mLastUpdateTimestampMs >= 0) {
+ if (timestampMs > mLastUpdateTimestampMs) {
+ long delta = value - mValue;
+ if (delta >= 0) {
+ returnValue = delta;
+ long timeSinceUpdate = timestampMs - mLastUpdateTimestampMs;
+ for (int i = 0; i < mStateCount; i++) {
+ long timeInState = mStates[i].mTimeInStateSinceUpdate;
+ if (timeInState > 0) {
+ mStates[i].mCounter += delta * timeInState / timeSinceUpdate;
+ mStates[i].mTimeInStateSinceUpdate = 0;
+ }
+ }
+ } else {
+ for (int i = 0; i < mStateCount; i++) {
+ mStates[i].mTimeInStateSinceUpdate = 0;
+ }
+ }
+ } else if (timestampMs < mLastUpdateTimestampMs) {
+ for (int i = 0; i < mStateCount; i++) {
+ mStates[i].mTimeInStateSinceUpdate = 0;
+ }
+ }
+ }
+ }
+ mValue = value;
+ mLastUpdateTimestampMs = timestampMs;
+ return returnValue;
+ }
+
+ public void incrementValue(long count, long timestampMs) {
+ updateValue(mValue + count, timestampMs);
+ }
+
+ public long getValue(int state) {
+ return mStates[state].mCounter;
+ }
+
+ public void reset() {
+ mLastStateChangeTimestampMs = -1;
+ mLastUpdateTimestampMs = -1;
+ for (int i = 0; i < mStateCount; i++) {
+ mStates[i].mTimeInStateSinceUpdate = 0;
+ mStates[i].mCounter = 0;
+ }
+ }
+
+ public void writeToParcel(Parcel parcel) {
+ parcel.writeInt(mStateCount);
+ for (int i = 0; i < mStateCount; i++) {
+ parcel.writeLong(mStates[i].mCounter);
+ }
+ }
+
+ public void initFromParcel(Parcel parcel) {
+ try {
+ for (int i = 0; i < mStateCount; i++) {
+ mStates[i].mCounter = parcel.readLong();
+ }
+ } catch (Exception e) {
+ throw new BadParcelableException(e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[");
+ for (int state = 0; state < mStateCount; state++) {
+ if (state != 0) {
+ sb.append(", ");
+ }
+ sb.append(state).append(": ").append(mStates[state].mCounter);
+ }
+ sb.append("]");
+ if (mLastUpdateTimestampMs >= 0) {
+ sb.append(" updated: ").append(mLastUpdateTimestampMs);
+ }
+ if (mLastStateChangeTimestampMs >= 0) {
+ sb.append(" currentState: ").append(mCurrentState);
+ if (mLastStateChangeTimestampMs > mLastUpdateTimestampMs) {
+ sb.append(" stateChanged: ").append(mLastStateChangeTimestampMs);
+ }
+ } else {
+ sb.append(" currentState: none");
+ }
+ return sb.toString();
+ }
+ }
+
+ private static final HashMap<Long, LongMultiStateCounterRavenwood> sInstances =
+ new HashMap<>();
+ private static long sNextId = 1;
+
+ public static long native_init(int stateCount) {
+ LongMultiStateCounterRavenwood instance = new LongMultiStateCounterRavenwood(stateCount);
+ long instanceId = sNextId++;
+ sInstances.put(instanceId, instance);
+ return instanceId;
+ }
+
+ private static LongMultiStateCounterRavenwood getInstance(long instanceId) {
+ return sInstances.get(instanceId);
+ }
+
+ public static void native_setEnabled(long instanceId, boolean enabled,
+ long timestampMs) {
+ getInstance(instanceId).setEnabled(enabled, timestampMs);
+ }
+
+ public static int native_getStateCount(long instanceId) {
+ return getInstance(instanceId).mStateCount;
+ }
+
+ public static long native_updateValue(long instanceId, long value, long timestampMs) {
+ return getInstance(instanceId).updateValue(value, timestampMs);
+ }
+
+ public static void native_setState(long instanceId, int state, long timestampMs) {
+ getInstance(instanceId).setState(state, timestampMs);
+ }
+
+ public static void native_incrementValue(long instanceId, long count, long timestampMs) {
+ getInstance(instanceId).incrementValue(count, timestampMs);
+ }
+
+ public static long native_getCount(long instanceId, int state) {
+ return getInstance(instanceId).getValue(state);
+ }
+
+ public static void native_reset(long instanceId) {
+ getInstance(instanceId).reset();
+ }
+
+ public static void native_writeToParcel(long instanceId, Parcel parcel, int flags) {
+ getInstance(instanceId).writeToParcel(parcel);
+ }
+
+ public static long native_initFromParcel(Parcel parcel) {
+ int stateCount = parcel.readInt();
+ if (stateCount < 0 || stateCount > 0xEFFF) {
+ throw new BadParcelableException("stateCount out of range");
+ }
+ // LongMultiStateCounter.cpp uses AParcel, which throws on out-of-data.
+ if (parcel.dataPosition() >= parcel.dataSize()) {
+ throw new RuntimeException("Bad parcel");
+ }
+ long instanceId = native_init(stateCount);
+ getInstance(instanceId).initFromParcel(parcel);
+ if (parcel.dataPosition() > parcel.dataSize()) {
+ throw new RuntimeException("Bad parcel");
+ }
+ return instanceId;
+ }
+
+ public static String native_toString(long instanceId) {
+ return getInstance(instanceId).toString();
+ }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 97e09b8..1089f82 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -49,6 +49,7 @@
class HostStubGen(val options: HostStubGenOptions) {
fun run() {
val errors = HostStubGenErrors()
+ val stats = HostStubGenStats()
// Load all classes.
val allClasses = loadClassStructures(options.inJar.get)
@@ -80,7 +81,14 @@
options.enableClassChecker.get,
allClasses,
errors,
+ stats,
)
+
+ // Dump statistics, if specified.
+ options.statsFile.ifSet {
+ PrintWriter(it).use { pw -> stats.dump(pw) }
+ log.i("Dump file created at $it")
+ }
}
/**
@@ -237,6 +245,7 @@
enableChecker: Boolean,
classes: ClassNodes,
errors: HostStubGenErrors,
+ stats: HostStubGenStats,
) {
log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar)
log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled")
@@ -254,7 +263,8 @@
while (inEntries.hasMoreElements()) {
val entry = inEntries.nextElement()
convertSingleEntry(inZip, entry, stubOutStream, implOutStream,
- filter, packageRedirector, enableChecker, classes, errors)
+ filter, packageRedirector, enableChecker, classes, errors,
+ stats)
}
log.i("Converted all entries.")
}
@@ -287,6 +297,7 @@
enableChecker: Boolean,
classes: ClassNodes,
errors: HostStubGenErrors,
+ stats: HostStubGenStats,
) {
log.d("Entry: %s", entry.name)
log.withIndent {
@@ -300,7 +311,7 @@
// If it's a class, convert it.
if (name.endsWith(".class")) {
processSingleClass(inZip, entry, stubOutStream, implOutStream, filter,
- packageRedirector, enableChecker, classes, errors)
+ packageRedirector, enableChecker, classes, errors, stats)
return
}
@@ -354,6 +365,7 @@
enableChecker: Boolean,
classes: ClassNodes,
errors: HostStubGenErrors,
+ stats: HostStubGenStats,
) {
val classInternalName = entry.name.replaceFirst("\\.class$".toRegex(), "")
val classPolicy = filter.getPolicyForClass(classInternalName)
@@ -370,7 +382,7 @@
stubOutStream.putNextEntry(newEntry)
convertClass(classInternalName, /*forImpl=*/false, bis,
stubOutStream, filter, packageRedirector, enableChecker, classes,
- errors)
+ errors, null)
stubOutStream.closeEntry()
}
}
@@ -383,7 +395,7 @@
implOutStream.putNextEntry(newEntry)
convertClass(classInternalName, /*forImpl=*/true, bis,
implOutStream, filter, packageRedirector, enableChecker, classes,
- errors)
+ errors, stats)
implOutStream.closeEntry()
}
}
@@ -403,6 +415,7 @@
enableChecker: Boolean,
classes: ClassNodes,
errors: HostStubGenErrors,
+ stats: HostStubGenStats?,
) {
val cr = ClassReader(input)
@@ -420,6 +433,7 @@
enablePostTrace = options.enablePostTrace.get,
enableNonStubMethodCallDetection = options.enableNonStubMethodCallDetection.get,
errors = errors,
+ stats = stats,
)
outVisitor = BaseAdapter.getVisitor(classInternalName, classes, outVisitor, filter,
packageRedirector, forImpl, visitorOptions)
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index d2ead18..9f5d524 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -108,6 +108,8 @@
var enablePostTrace: SetOnce<Boolean> = SetOnce(false),
var enableNonStubMethodCallDetection: SetOnce<Boolean> = SetOnce(false),
+
+ var statsFile: SetOnce<String?> = SetOnce(null),
) {
companion object {
@@ -252,6 +254,8 @@
"--verbose-log" -> setLogFile(LogLevel.Verbose, nextArg())
"--debug-log" -> setLogFile(LogLevel.Debug, nextArg())
+ "--stats-file" -> ret.statsFile.setNextStringArg()
+
else -> throw ArgumentsException("Unknown option: $arg")
}
} catch (e: SetOnce.SetMoreThanOnceException) {
@@ -387,6 +391,7 @@
enablePreTrace=$enablePreTrace,
enablePostTrace=$enablePostTrace,
enableNonStubMethodCallDetection=$enableNonStubMethodCallDetection,
+ statsFile=$statsFile,
}
""".trimIndent()
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
new file mode 100644
index 0000000..50518e1
--- /dev/null
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenStats.kt
@@ -0,0 +1,89 @@
+/*
+ * 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
+
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.filters.FilterPolicyWithReason
+import org.objectweb.asm.Opcodes
+import java.io.PrintWriter
+
+open class HostStubGenStats {
+ data class Stats(
+ var supported: Int = 0,
+ var total: Int = 0,
+ val children: MutableMap<String, Stats> = mutableMapOf<String, Stats>(),
+ )
+
+ private val stats = mutableMapOf<String, Stats>()
+
+ fun onVisitPolicyForMethod(fullClassName: String, methodName: String, descriptor: String,
+ policy: FilterPolicyWithReason, access: Int) {
+ // Ignore methods that aren't public
+ if ((access and Opcodes.ACC_PUBLIC) == 0) return
+ // Ignore methods that are abstract
+ if ((access and Opcodes.ACC_ABSTRACT) != 0) return
+ // Ignore methods where policy isn't relevant
+ if (policy.isIgnoredForStats) return
+
+ val packageName = resolvePackageName(fullClassName)
+ val className = resolveClassName(fullClassName)
+
+ // Ignore methods for certain generated code
+ if (className.endsWith("Proto")
+ or className.endsWith("ProtoEnums")
+ or className.endsWith("LogTags")
+ or className.endsWith("StatsLog")) {
+ return
+ }
+
+ val packageStats = stats.getOrPut(packageName) { Stats() }
+ val classStats = packageStats.children.getOrPut(className) { Stats() }
+
+ if (policy.policy.isSupported) {
+ packageStats.supported += 1
+ classStats.supported += 1
+ }
+ packageStats.total += 1
+ classStats.total += 1
+ }
+
+ fun dump(pw: PrintWriter) {
+ pw.printf("PackageName,ClassName,SupportedMethods,TotalMethods\n")
+ stats.forEach { (packageName, packageStats) ->
+ if (packageStats.supported > 0) {
+ packageStats.children.forEach { (className, classStats) ->
+ pw.printf("%s,%s,%d,%d\n", packageName, className,
+ classStats.supported, classStats.total)
+ }
+ }
+ }
+ }
+
+ private fun resolvePackageName(fullClassName: String): String {
+ val start = fullClassName.lastIndexOf('/')
+ return fullClassName.substring(0, start).toHumanReadableClassName()
+ }
+
+ private fun resolveClassName(fullClassName: String): String {
+ val start = fullClassName.lastIndexOf('/')
+ val end = fullClassName.indexOf('$')
+ if (end == -1) {
+ return fullClassName.substring(start + 1)
+ } else {
+ return fullClassName.substring(start + 1, end)
+ }
+ }
+}
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 9317996..4d21106 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicy.kt
@@ -111,6 +111,16 @@
}
}
+ /** Returns whether a policy is considered supported. */
+ val isSupported: Boolean
+ get() {
+ return when (this) {
+ // TODO: handle native method with no substitution as being unsupported
+ Stub, StubClass, Keep, KeepClass, SubstituteAndStub, SubstituteAndKeep -> true
+ else -> false
+ }
+ }
+
fun getSubstitutionBasePolicy(): FilterPolicy {
return when (this) {
SubstituteAndKeep -> Keep
@@ -136,4 +146,4 @@
fun withReason(reason: String): FilterPolicyWithReason {
return FilterPolicyWithReason(this, reason)
}
-}
\ No newline at end of file
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
index b64a2f5..eb03f66 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
@@ -63,4 +63,15 @@
override fun toString(): String {
return "[$policy - reason: $reason]"
}
-}
\ No newline at end of file
+
+ /** Returns whether this policy should be ignored for stats. */
+ val isIgnoredForStats: Boolean
+ get() {
+ return reason.contains("anonymous-inner-class")
+ || reason.contains("is-annotation")
+ || reason.contains("is-enum")
+ || reason.contains("is-synthetic-method")
+ || reason.contains("special-class")
+ || reason.contains("substitute-to")
+ }
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
index ea7d1d0..78b13fd 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/ImplicitOutputFilter.kt
@@ -119,14 +119,14 @@
if (cn.isEnum()) {
mn?.let { mn ->
if (isAutoGeneratedEnumMember(mn)) {
- return memberPolicy.withReason(classPolicy.reason).wrapReason("enum")
+ return memberPolicy.withReason(classPolicy.reason).wrapReason("is-enum")
}
}
}
// Keep (or stub) all members of annotations.
if (cn.isAnnotation()) {
- return memberPolicy.withReason(classPolicy.reason).wrapReason("annotation")
+ return memberPolicy.withReason(classPolicy.reason).wrapReason("is-annotation")
}
mn?.let {
@@ -134,7 +134,7 @@
// For synthetic methods (such as lambdas), let's just inherit the class's
// policy.
return memberPolicy.withReason(classPolicy.reason).wrapReason(
- "synthetic method")
+ "is-synthetic-method")
}
}
}
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 7fdd944..6ad83fb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -132,23 +132,24 @@
throw ParseException(
"Policy for AIDL classes already defined")
}
- aidlPolicy = policy.withReason("$FILTER_REASON (AIDL)")
+ aidlPolicy = policy.withReason(
+ "$FILTER_REASON (special-class AIDL)")
}
SpecialClass.FeatureFlags -> {
if (featureFlagsPolicy != null) {
throw ParseException(
"Policy for feature flags already defined")
}
- featureFlagsPolicy =
- policy.withReason("$FILTER_REASON (feature flags)")
+ featureFlagsPolicy = policy.withReason(
+ "$FILTER_REASON (special-class feature flags)")
}
SpecialClass.Sysprops -> {
if (syspropsPolicy != null) {
throw ParseException(
"Policy for sysprops already defined")
}
- syspropsPolicy =
- policy.withReason("$FILTER_REASON (sysprops)")
+ syspropsPolicy = policy.withReason(
+ "$FILTER_REASON (special-class sysprops)")
}
}
}
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 21cfd4b..45e140c 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/BaseAdapter.kt
@@ -16,6 +16,7 @@
package com.android.hoststubgen.visitors
import com.android.hoststubgen.HostStubGenErrors
+import com.android.hoststubgen.HostStubGenStats
import com.android.hoststubgen.LogLevel
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.UnifiedVisitor
@@ -50,6 +51,7 @@
*/
data class Options (
val errors: HostStubGenErrors,
+ val stats: HostStubGenStats?,
val enablePreTrace: Boolean,
val enablePostTrace: Boolean,
val enableNonStubMethodCallDetection: Boolean,
@@ -176,6 +178,7 @@
}
val p = filter.getPolicyForMethod(currentClassName, name, descriptor)
log.d("visitMethod: %s%s [%x] [%s] Policy: %s", name, descriptor, access, signature, p)
+ options.stats?.onVisitPolicyForMethod(currentClassName, name, descriptor, p, access)
log.withIndent {
// If it's a substitute-from method, then skip (== remove).