Merge "Tolerate "--dismiss-keyguard"" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index b5f398b..ce3e985 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -36,6 +36,7 @@
":com.android.hardware.input-aconfig-java{.generated_srcjars}",
":com.android.input.flags-aconfig-java{.generated_srcjars}",
":com.android.text.flags-aconfig-java{.generated_srcjars}",
+ ":framework-jobscheduler-job.flags-aconfig-java{.generated_srcjars}",
":telecom_flags_core_java_lib{.generated_srcjars}",
":telephony_flags_core_java_lib{.generated_srcjars}",
":android.companion.virtual.flags-aconfig-java{.generated_srcjars}",
@@ -664,6 +665,19 @@
aconfig_declarations: "device_policy_aconfig_flags",
}
+// JobScheduler
+aconfig_declarations {
+ name: "framework-jobscheduler-job.flags-aconfig",
+ package: "android.app.job",
+ srcs: ["apex/jobscheduler/framework/aconfig/job.aconfig"],
+}
+
+java_aconfig_library {
+ name: "framework-jobscheduler-job.flags-aconfig-java",
+ aconfig_declarations: "framework-jobscheduler-job.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Notifications
aconfig_declarations {
name: "android.service.notification.flags-aconfig",
diff --git a/Android.bp b/Android.bp
index a402c576..78ffd6f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -414,13 +414,25 @@
],
}
+// Collection of non updatable unbundled jars. The list here should match
+// |non_updatable_modules| variable in frameworks/base/api/api.go.
+java_library {
+ name: "framework-non-updatable-unbundled-impl-libs",
+ static_libs: [
+ "framework-location.impl",
+ "framework-nfc.impl",
+ ],
+ sdk_version: "core_platform",
+ installable: false,
+}
+
// Separated so framework-minus-apex-defaults can be used without the libs dependency
java_defaults {
name: "framework-minus-apex-with-libs-defaults",
defaults: ["framework-minus-apex-defaults"],
libs: [
"framework-virtualization.stubs.module_lib",
- "framework-location.impl",
+ "framework-non-updatable-unbundled-impl-libs",
],
}
@@ -451,7 +463,7 @@
stem: "framework",
apex_available: ["//apex_available:platform"],
visibility: [
- "//frameworks/base/location",
+ "//frameworks/base:__subpackages__",
],
compile_dex: false,
headers_only: true,
@@ -514,8 +526,8 @@
installable: false, // this lib is a build-only library
static_libs: [
"app-compat-annotations",
- "framework-location.impl",
"framework-minus-apex",
+ "framework-non-updatable-unbundled-impl-libs",
"framework-updatable-stubs-module_libs_api",
],
sdk_version: "core_platform",
diff --git a/THERMAL_OWNERS b/THERMAL_OWNERS
new file mode 100644
index 0000000..b95b7e8
--- /dev/null
+++ b/THERMAL_OWNERS
@@ -0,0 +1,3 @@
+lpy@google.com
+wvw@google.com
+xwxw@google.com
diff --git a/apct-tests/perftests/OWNERS b/apct-tests/perftests/OWNERS
index 4c57e64..8ff3f9b 100644
--- a/apct-tests/perftests/OWNERS
+++ b/apct-tests/perftests/OWNERS
@@ -1,12 +1,11 @@
-balejs@google.com
carmenjackson@google.com
-cfijalkovich@google.com
dualli@google.com
edgararriaga@google.com
-jpakaravoor@google.com
+jdduke@google.com
jreck@google.com #{LAST_RESORT_SUGGESTION}
kevinjeon@google.com
philipcuadra@google.com
+shayba@google.com
shombert@google.com
timmurray@google.com
wessam@google.com
diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig
new file mode 100644
index 0000000..f5e33a80
--- /dev/null
+++ b/apex/jobscheduler/framework/aconfig/job.aconfig
@@ -0,0 +1,8 @@
+package: "android.app.job"
+
+flag {
+ name: "job_debug_info_apis"
+ namespace: "backstage_power"
+ description: "Add APIs to let apps attach debug information to jobs"
+ bug: "293491637"
+}
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 9961c4f..742ed5f 100644
--- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
+++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
@@ -26,6 +26,7 @@
import static android.util.TimeUtils.formatDuration;
import android.annotation.BytesLong;
+import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -47,13 +48,17 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
+import android.os.Trace;
+import android.util.ArraySet;
import android.util.Log;
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.Objects;
+import java.util.Set;
/**
* Container of data passed to the {@link android.app.job.JobScheduler} fully encapsulating the
@@ -423,6 +428,15 @@
*/
public static final int CONSTRAINT_FLAG_STORAGE_NOT_LOW = 1 << 3;
+ /** @hide */
+ public static final int MAX_NUM_DEBUG_TAGS = 32;
+
+ /** @hide */
+ public static final int MAX_DEBUG_TAG_LENGTH = 127;
+
+ /** @hide */
+ public static final int MAX_TRACE_TAG_LENGTH = Trace.MAX_SECTION_NAME_LEN;
+
@UnsupportedAppUsage
private final int jobId;
private final PersistableBundle extras;
@@ -454,6 +468,9 @@
private final int mPriority;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private final int flags;
+ private final ArraySet<String> mDebugTags;
+ @Nullable
+ private final String mTraceTag;
/**
* Unique job id associated with this application (uid). This is the same job ID
@@ -724,6 +741,33 @@
}
/**
+ * @see JobInfo.Builder#addDebugTag(String)
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Set<String> getDebugTags() {
+ return Collections.unmodifiableSet(mDebugTags);
+ }
+
+ /**
+ * @see JobInfo.Builder#addDebugTag(String)
+ * @hide
+ */
+ @NonNull
+ public ArraySet<String> getDebugTagsArraySet() {
+ return mDebugTags;
+ }
+
+ /**
+ * @see JobInfo.Builder#setTraceTag(String)
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @Nullable
+ public String getTraceTag() {
+ return mTraceTag;
+ }
+
+ /**
* @see JobInfo.Builder#setExpedited(boolean)
*/
public boolean isExpedited() {
@@ -860,6 +904,12 @@
if (flags != j.flags) {
return false;
}
+ if (!mDebugTags.equals(j.mDebugTags)) {
+ return false;
+ }
+ if (!Objects.equals(mTraceTag, j.mTraceTag)) {
+ return false;
+ }
return true;
}
@@ -904,6 +954,12 @@
hashCode = 31 * hashCode + mBias;
hashCode = 31 * hashCode + mPriority;
hashCode = 31 * hashCode + flags;
+ if (mDebugTags.size() > 0) {
+ hashCode = 31 * hashCode + mDebugTags.hashCode();
+ }
+ if (mTraceTag != null) {
+ hashCode = 31 * hashCode + mTraceTag.hashCode();
+ }
return hashCode;
}
@@ -946,6 +1002,17 @@
mBias = in.readInt();
mPriority = in.readInt();
flags = in.readInt();
+ final int numDebugTags = in.readInt();
+ mDebugTags = new ArraySet<>();
+ for (int i = 0; i < numDebugTags; ++i) {
+ final String tag = in.readString();
+ if (tag == null) {
+ throw new IllegalStateException("malformed parcel");
+ }
+ mDebugTags.add(tag.intern());
+ }
+ final String traceTag = in.readString();
+ mTraceTag = traceTag == null ? null : traceTag.intern();
}
private JobInfo(JobInfo.Builder b) {
@@ -978,6 +1045,8 @@
mBias = b.mBias;
mPriority = b.mPriority;
flags = b.mFlags;
+ mDebugTags = b.mDebugTags;
+ mTraceTag = b.mTraceTag;
}
@Override
@@ -1024,6 +1093,14 @@
out.writeInt(mBias);
out.writeInt(mPriority);
out.writeInt(this.flags);
+ // Explicitly write out values here to avoid double looping to intern the strings
+ // when unparcelling.
+ final int numDebugTags = mDebugTags.size();
+ out.writeInt(numDebugTags);
+ for (int i = 0; i < numDebugTags; ++i) {
+ out.writeString(mDebugTags.valueAt(i));
+ }
+ out.writeString(mTraceTag);
}
public static final @android.annotation.NonNull Creator<JobInfo> CREATOR = new Creator<JobInfo>() {
@@ -1168,6 +1245,8 @@
private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY;
/** Easy way to track whether the client has tried to set a back-off policy. */
private boolean mBackoffPolicySet = false;
+ private final ArraySet<String> mDebugTags = new ArraySet<>();
+ private String mTraceTag;
/**
* Initialize a new Builder to construct a {@link JobInfo}.
@@ -1222,6 +1301,51 @@
mPriority = job.getPriority();
}
+ /**
+ * Add a debug tag to help track what this job is for. The tags may show in debug dumps
+ * or app metrics. Do not put personally identifiable information (PII) in the tag.
+ * <p>
+ * Tags have the following requirements:
+ * <ul>
+ * <li>Tags cannot be more than 127 characters.</li>
+ * <li>
+ * Since leading and trailing whitespace can lead to hard-to-debug issues,
+ * tags should not include leading or trailing whitespace.
+ * All tags will be {@link String#trim() trimmed}.
+ * </li>
+ * <li>An empty String (after trimming) is not allowed.</li>
+ * <li>Should not have personally identifiable information (PII).</li>
+ * <li>A job cannot have more than 32 tags.</li>
+ * </ul>
+ *
+ * @param tag A debug tag that helps describe what the job is for.
+ * @return This object for method chaining
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Builder addDebugTag(@NonNull String tag) {
+ mDebugTags.add(validateDebugTag(tag));
+ return this;
+ }
+
+ /** @hide */
+ @NonNull
+ public void addDebugTags(@NonNull Set<String> tags) {
+ mDebugTags.addAll(tags);
+ }
+
+ /**
+ * Remove a tag set via {@link #addDebugTag(String)}.
+ * @param tag The tag to remove
+ * @return This object for method chaining
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Builder removeDebugTag(@NonNull String tag) {
+ mDebugTags.remove(tag);
+ return this;
+ }
+
/** @hide */
@NonNull
@RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
@@ -1997,6 +2121,24 @@
}
/**
+ * Set a tag that will be used in {@link android.os.Trace traces}.
+ * Since this is a trace tag, it must follow the rules set in
+ * {@link android.os.Trace#beginSection(String)}, such as it cannot be more
+ * than 127 Unicode code units.
+ * Additionally, since leading and trailing whitespace can lead to hard-to-debug issues,
+ * they will be {@link String#trim() trimmed}.
+ * An empty String (after trimming) is not allowed.
+ * @param traceTag The tag to use in traces.
+ * @return This object for method chaining
+ */
+ @FlaggedApi(Flags.FLAG_JOB_DEBUG_INFO_APIS)
+ @NonNull
+ public Builder setTraceTag(@Nullable String traceTag) {
+ mTraceTag = validateTraceTag(traceTag);
+ return this;
+ }
+
+ /**
* @return The job object to hand to the JobScheduler. This object is immutable.
*/
public JobInfo build() {
@@ -2209,6 +2351,62 @@
"A user-initiated data transfer job must specify a valid network type");
}
}
+
+ if (mDebugTags.size() > MAX_NUM_DEBUG_TAGS) {
+ throw new IllegalArgumentException(
+ "Can't have more than " + MAX_NUM_DEBUG_TAGS + " tags");
+ }
+ final ArraySet<String> validatedDebugTags = new ArraySet<>();
+ for (int i = 0; i < mDebugTags.size(); ++i) {
+ validatedDebugTags.add(validateDebugTag(mDebugTags.valueAt(i)));
+ }
+ mDebugTags.clear();
+ mDebugTags.addAll(validatedDebugTags);
+
+ validateTraceTag(mTraceTag);
+ }
+
+ /**
+ * Returns a sanitized debug tag if valid, or throws an exception if not.
+ * @hide
+ */
+ @NonNull
+ public static String validateDebugTag(@Nullable String debugTag) {
+ if (debugTag == null) {
+ throw new NullPointerException("debug tag cannot be null");
+ }
+ debugTag = debugTag.trim();
+ if (debugTag.isEmpty()) {
+ throw new IllegalArgumentException("debug tag cannot be empty");
+ }
+ if (debugTag.length() > MAX_DEBUG_TAG_LENGTH) {
+ throw new IllegalArgumentException(
+ "debug tag cannot be more than " + MAX_DEBUG_TAG_LENGTH + " characters");
+ }
+ return debugTag.intern();
+ }
+
+ /**
+ * Returns a sanitized trace tag if valid, or throws an exception if not.
+ * @hide
+ */
+ @Nullable
+ public static String validateTraceTag(@Nullable String traceTag) {
+ if (traceTag == null) {
+ return null;
+ }
+ traceTag = traceTag.trim();
+ if (traceTag.isEmpty()) {
+ throw new IllegalArgumentException("trace tag cannot be empty");
+ }
+ if (traceTag.length() > MAX_TRACE_TAG_LENGTH) {
+ throw new IllegalArgumentException(
+ "traceTag tag cannot be more than " + MAX_TRACE_TAG_LENGTH + " characters");
+ }
+ if (traceTag.contains("|") || traceTag.contains("\n") || traceTag.contains("\0")) {
+ throw new IllegalArgumentException("Trace tag cannot contain |, \\n, or \\0");
+ }
+ return traceTag.intern();
}
/**
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
index a720baf..6f8014f 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/Alarm.java
@@ -22,7 +22,7 @@
import static android.app.AlarmManager.RTC_WAKEUP;
import static com.android.server.alarm.AlarmManagerService.PRIORITY_NORMAL;
-import static com.android.server.alarm.AlarmManagerService.clampPositive;
+import static com.android.server.alarm.AlarmManagerService.addClampPositive;
import android.app.AlarmManager;
import android.app.IAlarmListener;
@@ -148,7 +148,7 @@
mPolicyWhenElapsed[REQUESTER_POLICY_INDEX] = requestedWhenElapsed;
mWhenElapsed = requestedWhenElapsed;
this.windowLength = windowLength;
- mMaxWhenElapsed = clampPositive(requestedWhenElapsed + windowLength);
+ mMaxWhenElapsed = addClampPositive(requestedWhenElapsed, windowLength);
repeatInterval = interval;
operation = op;
listener = rec;
@@ -244,8 +244,8 @@
final long oldMaxWhenElapsed = mMaxWhenElapsed;
// windowLength should always be >= 0 here.
- final long maxRequestedElapsed = clampPositive(
- mPolicyWhenElapsed[REQUESTER_POLICY_INDEX] + windowLength);
+ final long maxRequestedElapsed = addClampPositive(
+ mPolicyWhenElapsed[REQUESTER_POLICY_INDEX], windowLength);
mMaxWhenElapsed = Math.max(maxRequestedElapsed, mWhenElapsed);
return (oldWhenElapsed != mWhenElapsed) || (oldMaxWhenElapsed != mMaxWhenElapsed);
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 384a480..1bd8da82 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -1417,15 +1417,15 @@
if (futurity < MIN_FUZZABLE_INTERVAL) {
futurity = 0;
}
- long maxElapsed = triggerAtTime + (long) (0.75 * futurity);
+ long maxElapsed = addClampPositive(triggerAtTime, (long) (0.75 * futurity));
// For non-repeating alarms, window is capped at a maximum of one hour from the requested
// delivery time. This allows for inexact-while-idle alarms to be slightly more reliable.
// In practice, the delivery window should generally be much smaller than that
// when the device is not idling.
if (interval == 0) {
- maxElapsed = Math.min(maxElapsed, triggerAtTime + INTERVAL_HOUR);
+ maxElapsed = Math.min(maxElapsed, addClampPositive(triggerAtTime, INTERVAL_HOUR));
}
- return clampPositive(maxElapsed);
+ return maxElapsed;
}
// The RTC clock has moved arbitrarily, so we need to recalculate all the RTC alarm deliveries.
@@ -1512,6 +1512,18 @@
return (val >= 0) ? val : Long.MAX_VALUE;
}
+ static long addClampPositive(long val1, long val2) {
+ long val = val1 + val2;
+ if (val >= 0) {
+ return val;
+ } else if (val1 >= 0 && val2 >= 0) {
+ /* Both are +ve, so overflow happened. */
+ return Long.MAX_VALUE;
+ } else {
+ return 0;
+ }
+ }
+
/**
* Sends alarms that were blocked due to user applied background restrictions - either because
* the user lifted those or the uid came to foreground.
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
index 721a8bd..6449edc 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java
@@ -557,6 +557,11 @@
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
traceTag, getId());
}
+ if (job.getAppTraceTag() != null) {
+ // Use the job's ID to distinguish traces since the ID will be unique per app.
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, "JobScheduler",
+ job.getAppTraceTag(), job.getJobId());
+ }
try {
mBatteryStats.noteJobStart(job.getBatteryName(), job.getSourceUid());
} catch (RemoteException e) {
@@ -1616,6 +1621,10 @@
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_SYSTEM_SERVER, "JobScheduler",
getId());
}
+ if (completedJob.getAppTraceTag() != null) {
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_APP, "JobScheduler",
+ completedJob.getJobId());
+ }
try {
mBatteryStats.noteJobFinish(mRunningJob.getBatteryName(), mRunningJob.getSourceUid(),
loggingInternalStopReason);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
index d466f0d..afcbdda 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java
@@ -510,6 +510,8 @@
private static final String XML_TAG_ONEOFF = "one-off";
private static final String XML_TAG_EXTRAS = "extras";
private static final String XML_TAG_JOB_WORK_ITEM = "job-work-item";
+ private static final String XML_TAG_DEBUG_INFO = "debug-info";
+ private static final String XML_TAG_DEBUG_TAG = "debug-tag";
private void migrateJobFilesAsync() {
synchronized (mLock) {
@@ -805,6 +807,7 @@
writeExecutionCriteriaToXml(out, jobStatus);
writeBundleToXml(jobStatus.getJob().getExtras(), out);
writeJobWorkItemsToXml(out, jobStatus);
+ writeDebugInfoToXml(out, jobStatus);
out.endTag(null, XML_TAG_JOB);
numJobs++;
@@ -991,6 +994,26 @@
}
}
+ private void writeDebugInfoToXml(@NonNull TypedXmlSerializer out,
+ @NonNull JobStatus jobStatus) throws IOException, XmlPullParserException {
+ final ArraySet<String> debugTags = jobStatus.getJob().getDebugTagsArraySet();
+ final int numTags = debugTags.size();
+ final String traceTag = jobStatus.getJob().getTraceTag();
+ if (traceTag == null && numTags == 0) {
+ return;
+ }
+ out.startTag(null, XML_TAG_DEBUG_INFO);
+ if (traceTag != null) {
+ out.attribute(null, "trace-tag", traceTag);
+ }
+ for (int i = 0; i < numTags; ++i) {
+ out.startTag(null, XML_TAG_DEBUG_TAG);
+ out.attribute(null, "tag", debugTags.valueAt(i));
+ out.endTag(null, XML_TAG_DEBUG_TAG);
+ }
+ out.endTag(null, XML_TAG_DEBUG_INFO);
+ }
+
private void writeJobWorkItemsToXml(@NonNull TypedXmlSerializer out,
@NonNull JobStatus jobStatus) throws IOException, XmlPullParserException {
// Write executing first since they're technically at the front of the queue.
@@ -1449,6 +1472,18 @@
jobWorkItems = readJobWorkItemsFromXml(parser);
}
+ if (eventType == XmlPullParser.START_TAG
+ && XML_TAG_DEBUG_INFO.equals(parser.getName())) {
+ try {
+ jobBuilder.setTraceTag(parser.getAttributeValue(null, "trace-tag"));
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Invalid trace tag persisted to disk", e);
+ }
+ parser.next();
+ jobBuilder.addDebugTags(readDebugTagsFromXml(parser));
+ eventType = parser.nextTag(); // Consume </debug-info>
+ }
+
final JobInfo builtJob;
try {
// Don't perform prefetch-deadline check here. Apps targeting S- shouldn't have
@@ -1721,6 +1756,33 @@
return null;
}
}
+
+ @NonNull
+ private Set<String> readDebugTagsFromXml(TypedXmlPullParser parser)
+ throws IOException, XmlPullParserException {
+ Set<String> debugTags = new ArraySet<>();
+
+ for (int eventType = parser.getEventType(); eventType != XmlPullParser.END_DOCUMENT;
+ eventType = parser.next()) {
+ final String tagName = parser.getName();
+ if (!XML_TAG_DEBUG_TAG.equals(tagName)) {
+ // We're no longer operating with debug tags.
+ break;
+ }
+ if (debugTags.size() < JobInfo.MAX_NUM_DEBUG_TAGS) {
+ final String debugTag;
+ try {
+ debugTag = JobInfo.validateDebugTag(parser.getAttributeValue(null, "tag"));
+ } catch (Exception e) {
+ Slog.wtf(TAG, "Invalid debug tag persisted to disk", e);
+ continue;
+ }
+ debugTags.add(debugTag);
+ }
+ }
+
+ return debugTags;
+ }
}
/** Set of all tracked jobs. */
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
index 0cf0cc5..e06006f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java
@@ -640,22 +640,27 @@
if (mCcConfig.mShouldReprocessNetworkCapabilities
|| (mFlexibilityController.isEnabled() != mCcConfig.mFlexIsEnabled)) {
AppSchedulingModuleThread.getHandler().post(() -> {
- boolean shouldUpdateJobs = false;
- for (int i = 0; i < mAvailableNetworks.size(); ++i) {
- CachedNetworkMetadata metadata = mAvailableNetworks.valueAt(i);
- if (metadata == null || metadata.networkCapabilities == null) {
- continue;
+ boolean flexAffinitiesChanged = false;
+ boolean flexAffinitiesSatisfied = false;
+ synchronized (mLock) {
+ for (int i = 0; i < mAvailableNetworks.size(); ++i) {
+ CachedNetworkMetadata metadata = mAvailableNetworks.valueAt(i);
+ if (metadata == null) {
+ continue;
+ }
+ if (updateTransportAffinitySatisfaction(metadata)) {
+ // Something changed. Update jobs.
+ flexAffinitiesChanged = true;
+ }
+ flexAffinitiesSatisfied |= metadata.satisfiesTransportAffinities;
}
- boolean satisfies = satisfiesTransportAffinities(metadata.networkCapabilities);
- if (metadata.satisfiesTransportAffinities != satisfies) {
- metadata.satisfiesTransportAffinities = satisfies;
- // Something changed. Update jobs.
- shouldUpdateJobs = true;
+ if (flexAffinitiesChanged) {
+ mFlexibilityController.setConstraintSatisfied(
+ JobStatus.CONSTRAINT_CONNECTIVITY,
+ flexAffinitiesSatisfied, sElapsedRealtimeClock.millis());
+ updateAllTrackedJobsLocked(false);
}
}
- if (shouldUpdateJobs) {
- updateAllTrackedJobsLocked(false);
- }
});
}
}
@@ -1059,6 +1064,22 @@
return false;
}
+ /**
+ * Updates {@link CachedNetworkMetadata#satisfiesTransportAffinities} in the given
+ * {@link CachedNetworkMetadata} object.
+ * @return true if the satisfaction changed
+ */
+ private boolean updateTransportAffinitySatisfaction(
+ @NonNull CachedNetworkMetadata cachedNetworkMetadata) {
+ final boolean satisfiesAffinities =
+ satisfiesTransportAffinities(cachedNetworkMetadata.networkCapabilities);
+ if (cachedNetworkMetadata.satisfiesTransportAffinities != satisfiesAffinities) {
+ cachedNetworkMetadata.satisfiesTransportAffinities = satisfiesAffinities;
+ return true;
+ }
+ return false;
+ }
+
private boolean satisfiesTransportAffinities(@Nullable NetworkCapabilities capabilities) {
if (!mFlexibilityController.isEnabled()) {
return true;
@@ -1552,7 +1573,9 @@
}
}
cnm.networkCapabilities = capabilities;
- cnm.satisfiesTransportAffinities = satisfiesTransportAffinities(capabilities);
+ if (updateTransportAffinitySatisfaction(cnm)) {
+ maybeUpdateFlexConstraintLocked(cnm);
+ }
maybeRegisterSignalStrengthCallbackLocked(capabilities);
updateTrackedJobsLocked(-1, network);
postAdjustCallbacks();
@@ -1566,8 +1589,13 @@
}
synchronized (mLock) {
final CachedNetworkMetadata cnm = mAvailableNetworks.remove(network);
- if (cnm != null && cnm.networkCapabilities != null) {
- maybeUnregisterSignalStrengthCallbackLocked(cnm.networkCapabilities);
+ if (cnm != null) {
+ if (cnm.networkCapabilities != null) {
+ maybeUnregisterSignalStrengthCallbackLocked(cnm.networkCapabilities);
+ }
+ if (cnm.satisfiesTransportAffinities) {
+ maybeUpdateFlexConstraintLocked(null);
+ }
}
for (int u = 0; u < mCurrentDefaultNetworkCallbacks.size(); ++u) {
UidDefaultNetworkCallback callback = mCurrentDefaultNetworkCallbacks.valueAt(u);
@@ -1639,6 +1667,37 @@
}
}
}
+
+ /**
+ * Maybe call {@link FlexibilityController#setConstraintSatisfied(int, boolean, long)}
+ * if the network affinity state has changed.
+ */
+ @GuardedBy("mLock")
+ private void maybeUpdateFlexConstraintLocked(
+ @Nullable CachedNetworkMetadata cachedNetworkMetadata) {
+ if (cachedNetworkMetadata != null
+ && cachedNetworkMetadata.satisfiesTransportAffinities) {
+ mFlexibilityController.setConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY,
+ true, sElapsedRealtimeClock.millis());
+ } else {
+ // This network doesn't satisfy transport affinities. Check if any other
+ // available networks do satisfy the affinities before saying that the
+ // transport affinity is no longer satisfied for flex.
+ boolean isTransportAffinitySatisfied = false;
+ for (int i = mAvailableNetworks.size() - 1; i >= 0; --i) {
+ final CachedNetworkMetadata cnm = mAvailableNetworks.valueAt(i);
+ if (cnm != null && cnm.satisfiesTransportAffinities) {
+ isTransportAffinitySatisfied = true;
+ break;
+ }
+ }
+ if (!isTransportAffinitySatisfied) {
+ mFlexibilityController.setConstraintSatisfied(
+ JobStatus.CONSTRAINT_CONNECTIVITY, false,
+ sElapsedRealtimeClock.millis());
+ }
+ }
+ }
};
private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() {
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 70f9a52..fed3c42 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
@@ -43,6 +43,8 @@
import android.util.Log;
import android.util.Slog;
import android.util.SparseArrayMap;
+import android.util.SparseLongArray;
+import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -68,11 +70,6 @@
| CONSTRAINT_CHARGING
| CONSTRAINT_IDLE;
- /** List of flexible constraints a job can opt into. */
- static final int OPTIONAL_FLEXIBLE_CONSTRAINTS = CONSTRAINT_BATTERY_NOT_LOW
- | CONSTRAINT_CHARGING
- | CONSTRAINT_IDLE;
-
/** List of all job flexible constraints whose satisfaction is job specific. */
private static final int JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS = CONSTRAINT_CONNECTIVITY;
@@ -83,9 +80,6 @@
private static final int NUM_JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS =
Integer.bitCount(JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS);
- static final int NUM_OPTIONAL_FLEXIBLE_CONSTRAINTS =
- Integer.bitCount(OPTIONAL_FLEXIBLE_CONSTRAINTS);
-
static final int NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS =
Integer.bitCount(SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS);
@@ -103,6 +97,9 @@
private long mRescheduledJobDeadline = FcConfig.DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
private long mMaxRescheduledDeadline = FcConfig.DEFAULT_MAX_RESCHEDULED_DEADLINE_MS;
+ private long mUnseenConstraintGracePeriodMs =
+ FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
+
@VisibleForTesting
@GuardedBy("mLock")
boolean mFlexibilityEnabled = FcConfig.DEFAULT_FLEXIBILITY_ENABLED;
@@ -132,6 +129,9 @@
@GuardedBy("mLock")
int mSatisfiedFlexibleConstraints;
+ @GuardedBy("mLock")
+ private final SparseLongArray mLastSeenConstraintTimesElapsed = new SparseLongArray();
+
@VisibleForTesting
@GuardedBy("mLock")
final FlexibilityTracker mFlexibilityTracker;
@@ -258,25 +258,68 @@
boolean isFlexibilitySatisfiedLocked(JobStatus js) {
return !mFlexibilityEnabled
|| mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP
- || getNumSatisfiedRequiredConstraintsLocked(js)
- >= js.getNumRequiredFlexibleConstraints()
+ || hasEnoughSatisfiedConstraintsLocked(js)
|| mService.isCurrentlyRunningLocked(js);
}
+ /**
+ * Returns whether there are enough constraints satisfied to allow running the job from flex's
+ * perspective. This takes into account unseen constraint combinations and expectations around
+ * whether additional constraints can ever be satisfied.
+ */
@VisibleForTesting
@GuardedBy("mLock")
- int getNumSatisfiedRequiredConstraintsLocked(JobStatus js) {
- return Integer.bitCount(mSatisfiedFlexibleConstraints)
- // Connectivity is job-specific, so must be handled separately.
- + (js.canApplyTransportAffinities()
- && js.areTransportAffinitiesSatisfied() ? 1 : 0);
+ boolean hasEnoughSatisfiedConstraintsLocked(@NonNull JobStatus js) {
+ final int satisfiedConstraints = mSatisfiedFlexibleConstraints
+ & (SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS
+ | (js.areTransportAffinitiesSatisfied() ? CONSTRAINT_CONNECTIVITY : 0));
+ final int numSatisfied = Integer.bitCount(satisfiedConstraints);
+ if (numSatisfied >= js.getNumRequiredFlexibleConstraints()) {
+ return true;
+ }
+ // We don't yet have the full number of required flex constraints. See if we should expect
+ // to be able to reach it. If not, then there's no point waiting anymore.
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ if (nowElapsed < mUnseenConstraintGracePeriodMs) {
+ // Too soon after boot. Not enough time to start predicting. Wait longer.
+ return false;
+ }
+
+ // The intention is to not force jobs to wait for constraint combinations that have never
+ // been seen together in a while. The job may still be allowed to wait for other constraint
+ // combinations. Thus, the logic is:
+ // If all the constraint combinations that have a count higher than the current satisfied
+ // count have not been seen recently enough, then assume they won't be seen anytime soon,
+ // so don't force the job to wait longer. If any combinations with a higher count have been
+ // seen recently, then the job can potentially wait for those combinations.
+ final int irrelevantConstraints = ~(SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS
+ | (js.canApplyTransportAffinities() ? CONSTRAINT_CONNECTIVITY : 0));
+ for (int i = mLastSeenConstraintTimesElapsed.size() - 1; i >= 0; --i) {
+ final int constraints = mLastSeenConstraintTimesElapsed.keyAt(i);
+ if ((constraints & irrelevantConstraints) != 0) {
+ // Ignore combinations that couldn't satisfy this job's needs.
+ continue;
+ }
+ final long lastSeenElapsed = mLastSeenConstraintTimesElapsed.valueAt(i);
+ final boolean seenRecently =
+ nowElapsed - lastSeenElapsed <= mUnseenConstraintGracePeriodMs;
+ if (Integer.bitCount(constraints) > numSatisfied && seenRecently) {
+ // We've seen a set of constraints with a higher count than what is currently
+ // satisfied recently enough, which means we can expect to see it again at some
+ // point. Keep waiting for now.
+ return false;
+ }
+ }
+
+ // We haven't seen any constraint set with more satisfied than the current satisfied count.
+ // There's no reason to expect additional constraints to be satisfied. Let the job run.
+ return true;
}
/**
* Sets the controller's constraint to a given state.
* Changes flexibility constraint satisfaction for affected jobs.
*/
- @VisibleForTesting
void setConstraintSatisfied(int constraint, boolean state, long nowElapsed) {
synchronized (mLock) {
final boolean old = (mSatisfiedFlexibleConstraints & constraint) != 0;
@@ -286,14 +329,34 @@
if (DEBUG) {
Slog.d(TAG, "setConstraintSatisfied: "
- + " constraint: " + constraint + " state: " + state);
+ + " constraint: " + constraint + " state: " + state);
+ }
+
+ // Mark now as the last time we saw this set of constraints.
+ mLastSeenConstraintTimesElapsed.put(mSatisfiedFlexibleConstraints, nowElapsed);
+ if (!state) {
+ // Mark now as the last time we saw this particular constraint.
+ // (Good for logging/dump purposes).
+ mLastSeenConstraintTimesElapsed.put(constraint, nowElapsed);
}
mSatisfiedFlexibleConstraints =
(mSatisfiedFlexibleConstraints & ~constraint) | (state ? constraint : 0);
- // Push the job update to the handler to avoid blocking other controllers and
- // potentially batch back-to-back controller state updates together.
- mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget();
+
+ if ((JOB_SPECIFIC_FLEXIBLE_CONSTRAINTS & constraint) != 0) {
+ // Job-specific constraint --> don't need to proceed with logic below that
+ // works with system-wide constraints.
+ return;
+ }
+
+ if (mFlexibilityEnabled) {
+ // Only attempt to update jobs if the flex logic is enabled. Otherwise, the status
+ // of the jobs won't change, so all the work will be a waste.
+
+ // Push the job update to the handler to avoid blocking other controllers and
+ // potentially batch back-to-back controller state updates together.
+ mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget();
+ }
}
}
@@ -543,7 +606,6 @@
if (!predicate.test(js)) {
continue;
}
- pw.print("#");
js.printUniqueId(pw);
pw.print(" from ");
UserHandle.formatUid(pw, js.getSourceUid());
@@ -645,7 +707,7 @@
final long nowElapsed = sElapsedRealtimeClock.millis();
final ArraySet<JobStatus> changedJobs = new ArraySet<>();
- for (int o = 0; o <= NUM_OPTIONAL_FLEXIBLE_CONSTRAINTS; ++o) {
+ for (int o = 0; o <= NUM_SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS; ++o) {
final ArraySet<JobStatus> jobsByNumConstraints = mFlexibilityTracker
.getJobsByNumRequiredConstraints(o);
@@ -687,6 +749,8 @@
FC_CONFIG_PREFIX + "max_rescheduled_deadline_ms";
static final String KEY_RESCHEDULED_JOB_DEADLINE_MS =
FC_CONFIG_PREFIX + "rescheduled_job_deadline_ms";
+ static final String KEY_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS =
+ FC_CONFIG_PREFIX + "unseen_constraint_grace_period_ms";
static final boolean DEFAULT_FLEXIBILITY_ENABLED = false;
@VisibleForTesting
@@ -698,6 +762,8 @@
final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80};
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;
+ @VisibleForTesting
+ static final long DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = 3 * DAY_IN_MILLIS;
/**
* If false the controller will not track new jobs
@@ -717,6 +783,11 @@
public long RESCHEDULED_JOB_DEADLINE_MS = DEFAULT_RESCHEDULED_JOB_DEADLINE_MS;
/** The max deadline for rescheduled jobs. */
public long MAX_RESCHEDULED_DEADLINE_MS = DEFAULT_MAX_RESCHEDULED_DEADLINE_MS;
+ /**
+ * How long to wait after last seeing a constraint combination before no longer waiting for
+ * it in order to run jobs.
+ */
+ public long UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
@GuardedBy("mLock")
public void processConstantLocked(@NonNull DeviceConfig.Properties properties,
@@ -780,6 +851,14 @@
mShouldReevaluateConstraints = true;
}
break;
+ case KEY_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS:
+ UNSEEN_CONSTRAINT_GRACE_PERIOD_MS =
+ properties.getLong(key, DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS);
+ if (mUnseenConstraintGracePeriodMs != UNSEEN_CONSTRAINT_GRACE_PERIOD_MS) {
+ mUnseenConstraintGracePeriodMs = UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
+ mShouldReevaluateConstraints = true;
+ }
+ break;
case KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS:
String dropPercentString = properties.getString(key, "");
PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS =
@@ -834,6 +913,8 @@
PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS).println();
pw.print(KEY_RESCHEDULED_JOB_DEADLINE_MS, RESCHEDULED_JOB_DEADLINE_MS).println();
pw.print(KEY_MAX_RESCHEDULED_DEADLINE_MS, MAX_RESCHEDULED_DEADLINE_MS).println();
+ pw.print(KEY_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS, UNSEEN_CONSTRAINT_GRACE_PERIOD_MS)
+ .println();
pw.decreaseIndent();
}
@@ -854,12 +935,34 @@
@Override
@GuardedBy("mLock")
public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
- pw.println("# Constraints Satisfied: " + Integer.bitCount(mSatisfiedFlexibleConstraints));
- pw.print("Satisfied Flexible Constraints: ");
+ pw.print("Satisfied Flexible Constraints:");
JobStatus.dumpConstraints(pw, mSatisfiedFlexibleConstraints);
pw.println();
pw.println();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ pw.println("Time since constraint combos last seen:");
+ pw.increaseIndent();
+ for (int i = 0; i < mLastSeenConstraintTimesElapsed.size(); ++i) {
+ final int constraints = mLastSeenConstraintTimesElapsed.keyAt(i);
+ if (constraints == mSatisfiedFlexibleConstraints) {
+ pw.print("0ms");
+ } else {
+ TimeUtils.formatDuration(
+ mLastSeenConstraintTimesElapsed.valueAt(i), nowElapsed, pw);
+ }
+ pw.print(":");
+ if (constraints != 0) {
+ // dumpConstraints prepends with a space, so no need to add a space after the :
+ JobStatus.dumpConstraints(pw, constraints);
+ } else {
+ pw.print(" none");
+ }
+ pw.println();
+ }
+ pw.decreaseIndent();
+
+ pw.println();
mFlexibilityTracker.dump(pw, predicate);
pw.println();
mFlexibilityAlarmQueue.dump(pw);
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 d6ada4c..b828f39 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
@@ -1054,6 +1054,12 @@
return mLoggingJobId;
}
+ /** Returns a trace tag using debug information provided by the app. */
+ @Nullable
+ public String getAppTraceTag() {
+ return job.getTraceTag();
+ }
+
/** Returns whether this job was scheduled by one app on behalf of another. */
public boolean isProxyJob() {
return mIsProxyJob;
@@ -2763,6 +2769,15 @@
pw.println("Has late constraint");
}
+ if (job.getTraceTag() != null) {
+ pw.print("Trace tag: ");
+ pw.println(job.getTraceTag());
+ }
+ if (job.getDebugTags().size() > 0) {
+ pw.print("Debug tags: ");
+ pw.println(job.getDebugTags());
+ }
+
pw.decreaseIndent();
}
diff --git a/api/Android.bp b/api/Android.bp
index 4d56b37..d6c14fb 100644
--- a/api/Android.bp
+++ b/api/Android.bp
@@ -80,7 +80,9 @@
"framework-location",
"framework-media",
"framework-mediaprovider",
+ "framework-nfc",
"framework-ondevicepersonalization",
+ "framework-pdf",
"framework-permission",
"framework-permission-s",
"framework-scheduling",
@@ -383,7 +385,10 @@
stub_only_libs: ["framework-protos"],
impl_only_libs: ["framework-minus-apex-headers"], // the framework, including hidden API
impl_library_visibility: ["//frameworks/base"],
- defaults_visibility: ["//frameworks/base/location"],
+ defaults_visibility: [
+ "//frameworks/base/location",
+ "//frameworks/base/nfc",
+ ],
plugins: ["error_prone_android_framework"],
errorprone: {
javacflags: [
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index f6f6929..28b2d4b 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -635,6 +635,7 @@
api_contributions: [
"framework-virtualization.stubs.source.test.api.contribution",
"framework-location.stubs.source.test.api.contribution",
+ "framework-nfc.stubs.source.test.api.contribution",
],
}
diff --git a/api/api.go b/api/api.go
index 8df6dab..71b1e10 100644
--- a/api/api.go
+++ b/api/api.go
@@ -32,6 +32,7 @@
const i18n = "i18n.module.public.api"
const virtualization = "framework-virtualization"
const location = "framework-location"
+const nfc = "framework-nfc"
var core_libraries_modules = []string{art, conscrypt, i18n}
@@ -43,7 +44,7 @@
// APIs.
// In addition, the modules in this list are allowed to contribute to test APIs
// stubs.
-var non_updatable_modules = []string{virtualization, location}
+var non_updatable_modules = []string{virtualization, location, nfc}
// The intention behind this soong plugin is to generate a number of "merged"
// API-related modules that would otherwise require a large amount of very
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
index 7659054..ec2b1f4 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
@@ -283,7 +283,10 @@
std::vector<int32_t> configs = toVector(env, rawConfigs);
// Configure uinput device, with user specified code and value.
for (auto& config : configs) {
- ::ioctl(static_cast<int>(handle), _IOW(UINPUT_IOCTL_BASE, code, int), config);
+ if (::ioctl(static_cast<int>(handle), _IOW(UINPUT_IOCTL_BASE, code, int), config) < 0) {
+ ALOGE("Error configuring device (ioctl %d, value 0x%x): %s", code, config,
+ strerror(errno));
+ }
}
}
diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java
index ad5e70f..b0fa34c 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Device.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Device.java
@@ -160,9 +160,16 @@
switch (msg.what) {
case MSG_OPEN_UINPUT_DEVICE:
SomeArgs args = (SomeArgs) msg.obj;
- mPtr = nativeOpenUinputDevice((String) args.arg1, args.argi1, args.argi2,
+ String name = (String) args.arg1;
+ mPtr = nativeOpenUinputDevice(name, args.argi1, args.argi2,
args.argi3, args.argi4, args.argi5, (String) args.arg2,
new DeviceCallback());
+ if (mPtr == 0) {
+ RuntimeException ex = new RuntimeException(
+ "Could not create uinput device \"" + name + "\"");
+ Log.e(TAG, "Couldn't create uinput device, exiting.", ex);
+ throw ex;
+ }
break;
case MSG_INJECT_EVENT:
if (mPtr != 0) {
diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java
index 01486c0..4498bc2 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Event.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Event.java
@@ -30,7 +30,7 @@
public class Event {
private static final String TAG = "UinputEvent";
- enum Command {
+ public enum Command {
REGISTER,
DELAY,
INJECT,
@@ -188,8 +188,8 @@
mEvent.mId = id;
}
- public void setCommand(String command) {
- mEvent.mCommand = Command.valueOf(command.toUpperCase());
+ public void setCommand(Command command) {
+ mEvent.mCommand = command;
}
public void setName(String name) {
diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
index 53d0be8..a2195c7 100644
--- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
@@ -57,7 +57,8 @@
String name = mReader.nextName();
switch (name) {
case "id" -> eb.setId(readInt());
- case "command" -> eb.setCommand(mReader.nextString());
+ case "command" -> eb.setCommand(
+ Event.Command.valueOf(mReader.nextString().toUpperCase()));
case "name" -> eb.setName(mReader.nextString());
case "vid" -> eb.setVid(readInt());
case "pid" -> eb.setPid(readInt());
diff --git a/core/api/current.txt b/core/api/current.txt
index b9719e1..f09036b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -8859,6 +8859,7 @@
method public int getBackoffPolicy();
method @Nullable public android.content.ClipData getClipData();
method public int getClipGrantFlags();
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public java.util.Set<java.lang.String> getDebugTags();
method public long getEstimatedNetworkDownloadBytes();
method public long getEstimatedNetworkUploadBytes();
method @NonNull public android.os.PersistableBundle getExtras();
@@ -8875,6 +8876,7 @@
method public int getPriority();
method @Nullable public android.net.NetworkRequest getRequiredNetwork();
method @NonNull public android.content.ComponentName getService();
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @Nullable public String getTraceTag();
method @NonNull public android.os.Bundle getTransientExtras();
method public long getTriggerContentMaxDelay();
method public long getTriggerContentUpdateDelay();
@@ -8911,8 +8913,10 @@
public static final class JobInfo.Builder {
ctor public JobInfo.Builder(int, @NonNull android.content.ComponentName);
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public android.app.job.JobInfo.Builder addDebugTag(@NonNull String);
method public android.app.job.JobInfo.Builder addTriggerContentUri(@NonNull android.app.job.JobInfo.TriggerContentUri);
method public android.app.job.JobInfo build();
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public android.app.job.JobInfo.Builder removeDebugTag(@NonNull String);
method public android.app.job.JobInfo.Builder setBackoffCriteria(long, int);
method public android.app.job.JobInfo.Builder setClipData(@Nullable android.content.ClipData, int);
method public android.app.job.JobInfo.Builder setEstimatedNetworkBytes(long, long);
@@ -8933,6 +8937,7 @@
method public android.app.job.JobInfo.Builder setRequiresCharging(boolean);
method public android.app.job.JobInfo.Builder setRequiresDeviceIdle(boolean);
method public android.app.job.JobInfo.Builder setRequiresStorageNotLow(boolean);
+ method @FlaggedApi("android.app.job.job_debug_info_apis") @NonNull public android.app.job.JobInfo.Builder setTraceTag(@Nullable String);
method public android.app.job.JobInfo.Builder setTransientExtras(@NonNull android.os.Bundle);
method public android.app.job.JobInfo.Builder setTriggerContentMaxDelay(long);
method public android.app.job.JobInfo.Builder setTriggerContentUpdateDelay(long);
diff --git a/core/api/lint-baseline.txt b/core/api/lint-baseline.txt
index 449249e..f331e7f 100644
--- a/core/api/lint-baseline.txt
+++ b/core/api/lint-baseline.txt
@@ -389,6 +389,12 @@
Method javax.microedition.khronos.egl.EGL10.eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
+InvalidNullabilityOverride: android.app.Notification.TvExtender#extend(android.app.Notification.Builder) parameter #0:
+ Invalid nullability on parameter `builder` in method `extend`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+InvalidNullabilityOverride: android.media.midi.MidiUmpDeviceService#onBind(android.content.Intent) parameter #0:
+ Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+
+
RequiresPermission: android.accounts.AccountManager#getAccountsByTypeAndFeatures(String, String[], android.accounts.AccountManagerCallback<android.accounts.Account[]>, android.os.Handler):
Method 'getAccountsByTypeAndFeatures' documentation mentions permissions without declaring @RequiresPermission
RequiresPermission: android.accounts.AccountManager#hasFeatures(android.accounts.Account, String[], android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler):
diff --git a/core/api/removed.txt b/core/api/removed.txt
index 989bb77..285dcc6a 100644
--- a/core/api/removed.txt
+++ b/core/api/removed.txt
@@ -112,6 +112,9 @@
method public abstract boolean setInstantAppCookie(@Nullable byte[]);
}
+ @IntDef(prefix={"FLAG_PERMISSION_"}, value={0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x100, 0x200, 0x2000, 0x1000, 0x800, 0x4000, 0x8000, 0x8, 0x10000, 0x20000}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PackageManager.PermissionFlags {
+ }
+
public final class SharedLibraryInfo implements android.os.Parcelable {
method public boolean isBuiltin();
method public boolean isDynamic();
@@ -318,6 +321,9 @@
method public CharSequence getBadgedLabelForUser(CharSequence, android.os.UserHandle);
}
+ @IntDef(flag=true, prefix={"RESTRICTION_"}, value={0x0, 0x1, 0x2, 0x4}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface UserManager.UserRestrictionSource {
+ }
+
}
package android.os.storage {
@@ -493,6 +499,13 @@
}
+package android.telephony.euicc {
+
+ @IntDef(prefix={"EUICC_OTA_"}, value={0x1, 0x2, 0x3, 0x4, 0x5}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccManager.OtaStatus {
+ }
+
+}
+
package android.text.format {
public class DateFormat {
@@ -554,6 +567,9 @@
field public static final int TYPE_STATUS_BAR_PANEL = 2014; // 0x7de
}
+ @IntDef(flag=true, prefix={"SYSTEM_FLAG_"}, value={0x80000, 0x10}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface WindowManager.LayoutParams.SystemFlags {
+ }
+
}
package android.view.accessibility {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index f3d1061..32d252e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3861,6 +3861,10 @@
field @RequiresPermission(android.Manifest.permission.ACCESS_SHORTCUTS) public static final int FLAG_GET_PERSONS_DATA = 2048; // 0x800
}
+ public class PackageInfo implements android.os.Parcelable {
+ method @FlaggedApi("android.content.pm.archiving") public long getArchiveTimeMillis();
+ }
+
public class PackageInstaller {
method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException;
method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull android.os.ParcelFileDescriptor, @Nullable String, int) throws android.content.pm.PackageInstaller.PackageParsingException;
@@ -3968,7 +3972,7 @@
method @Deprecated @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public abstract int getIntentVerificationStatusAsUser(@NonNull String, int);
method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public int getPackageUidAsUser(@NonNull String, @NonNull android.content.pm.PackageManager.PackageInfoFlags, int) throws android.content.pm.PackageManager.NameNotFoundException;
method @NonNull public String getPermissionControllerPackageName();
- method @android.content.pm.PackageManager.PermissionFlags @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, android.Manifest.permission.GET_RUNTIME_PERMISSIONS}) public abstract int getPermissionFlags(@NonNull String, @NonNull String, @NonNull android.os.UserHandle);
+ method @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS, android.Manifest.permission.GET_RUNTIME_PERMISSIONS}) public abstract int getPermissionFlags(@NonNull String, @NonNull String, @NonNull android.os.UserHandle);
method @NonNull @RequiresPermission(android.Manifest.permission.SUSPEND_APPS) public String[] getUnsuspendablePackages(@NonNull String[]);
method @RequiresPermission(android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS) public abstract void grantRuntimePermission(@NonNull String, @NonNull String, @NonNull android.os.UserHandle);
method @Deprecated public abstract int installExistingPackage(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException;
@@ -3999,7 +4003,7 @@
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public abstract void setUpdateAvailable(@NonNull String, boolean);
method @NonNull public boolean shouldShowNewAppInstalledNotification();
method @Deprecated @RequiresPermission(android.Manifest.permission.SET_PREFERRED_APPLICATIONS) public abstract boolean updateIntentVerificationStatusAsUser(@NonNull String, int, int);
- method @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS}) public abstract void updatePermissionFlags(@NonNull String, @NonNull String, @android.content.pm.PackageManager.PermissionFlags int, @android.content.pm.PackageManager.PermissionFlags int, @NonNull android.os.UserHandle);
+ method @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS}) public abstract void updatePermissionFlags(@NonNull String, @NonNull String, int, int, @NonNull android.os.UserHandle);
method @Deprecated @RequiresPermission(android.Manifest.permission.INTENT_FILTER_VERIFICATION_AGENT) public abstract void verifyIntentFilter(int, int, @NonNull java.util.List<java.lang.String>);
field public static final String ACTION_REQUEST_PERMISSIONS = "android.content.pm.action.REQUEST_PERMISSIONS";
field public static final String ACTION_REQUEST_PERMISSIONS_FOR_OTHER = "android.content.pm.action.REQUEST_PERMISSIONS_FOR_OTHER";
@@ -4116,9 +4120,7 @@
public static interface PackageManager.OnPermissionsChangedListener {
method public void onPermissionsChanged(int);
- }
-
- @IntDef(prefix={"FLAG_PERMISSION_"}, value={android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET, android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED, android.content.pm.PackageManager.FLAG_PERMISSION_POLICY_FIXED, android.content.pm.PackageManager.FLAG_PERMISSION_REVOKE_ON_UPGRADE, android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED, android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT, android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED, android.content.pm.PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED, android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT, android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT, android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT, android.content.pm.PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION, android.content.pm.PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE, android.content.pm.PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, android.content.pm.PackageManager.FLAG_PERMISSION_ONE_TIME, android.content.pm.PackageManager.FLAG_PERMISSION_AUTO_REVOKED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PackageManager.PermissionFlags {
+ method @FlaggedApi("android.permission.flags.device_aware_permission_apis") public default void onPermissionsChanged(int, @NonNull String);
}
public static final class PackageManager.UninstallCompleteCallback implements android.os.Parcelable {
@@ -4622,7 +4624,7 @@
}
public static interface HdmiClient.OnDeviceSelectedListener {
- method public void onDeviceSelected(@android.hardware.hdmi.HdmiControlManager.ControlCallbackResult int, int);
+ method public void onDeviceSelected(int, int);
}
public final class HdmiControlManager {
@@ -4820,9 +4822,6 @@
method public void onChange(@NonNull String);
}
- @IntDef({android.hardware.hdmi.HdmiControlManager.RESULT_SUCCESS, android.hardware.hdmi.HdmiControlManager.RESULT_TIMEOUT, android.hardware.hdmi.HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE, android.hardware.hdmi.HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE, android.hardware.hdmi.HdmiControlManager.RESULT_ALREADY_IN_PROGRESS, android.hardware.hdmi.HdmiControlManager.RESULT_EXCEPTION, android.hardware.hdmi.HdmiControlManager.RESULT_INCORRECT_MODE, android.hardware.hdmi.HdmiControlManager.RESULT_COMMUNICATION_FAILED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface HdmiControlManager.ControlCallbackResult {
- }
-
public static interface HdmiControlManager.HotplugEventListener {
method public void onReceived(android.hardware.hdmi.HdmiHotplugEvent);
}
@@ -4969,7 +4968,7 @@
}
public static interface HdmiSwitchClient.OnSelectListener {
- method public void onSelect(@android.hardware.hdmi.HdmiControlManager.ControlCallbackResult int);
+ method public void onSelect(int);
}
public class HdmiTimerRecordSources {
@@ -5686,14 +5685,14 @@
}
public final class ProgramSelector implements android.os.Parcelable {
- ctor public ProgramSelector(@android.hardware.radio.ProgramSelector.ProgramType int, @NonNull android.hardware.radio.ProgramSelector.Identifier, @Nullable android.hardware.radio.ProgramSelector.Identifier[], @Nullable long[]);
- method @NonNull public static android.hardware.radio.ProgramSelector createAmFmSelector(@android.hardware.radio.RadioManager.Band int, int);
- method @NonNull public static android.hardware.radio.ProgramSelector createAmFmSelector(@android.hardware.radio.RadioManager.Band int, int, int);
+ ctor public ProgramSelector(int, @NonNull android.hardware.radio.ProgramSelector.Identifier, @Nullable android.hardware.radio.ProgramSelector.Identifier[], @Nullable long[]);
+ method @NonNull public static android.hardware.radio.ProgramSelector createAmFmSelector(int, int);
+ method @NonNull public static android.hardware.radio.ProgramSelector createAmFmSelector(int, int, int);
method public int describeContents();
- method @NonNull public android.hardware.radio.ProgramSelector.Identifier[] getAllIds(@android.hardware.radio.ProgramSelector.IdentifierType int);
- method public long getFirstId(@android.hardware.radio.ProgramSelector.IdentifierType int);
+ method @NonNull public android.hardware.radio.ProgramSelector.Identifier[] getAllIds(int);
+ method public long getFirstId(int);
method @NonNull public android.hardware.radio.ProgramSelector.Identifier getPrimaryId();
- method @Deprecated @android.hardware.radio.ProgramSelector.ProgramType public int getProgramType();
+ method @Deprecated public int getProgramType();
method @NonNull public android.hardware.radio.ProgramSelector.Identifier[] getSecondaryIds();
method @Deprecated @NonNull public long[] getVendorIds();
method @NonNull public android.hardware.radio.ProgramSelector withSecondaryPreferred(@NonNull android.hardware.radio.ProgramSelector.Identifier);
@@ -5742,21 +5741,15 @@
}
public static final class ProgramSelector.Identifier implements android.os.Parcelable {
- ctor public ProgramSelector.Identifier(@android.hardware.radio.ProgramSelector.IdentifierType int, long);
+ ctor public ProgramSelector.Identifier(int, long);
method public int describeContents();
- method @android.hardware.radio.ProgramSelector.IdentifierType public int getType();
+ method public int getType();
method public long getValue();
method public boolean isCategoryType();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector.Identifier> CREATOR;
}
- @IntDef(prefix={"IDENTIFIER_TYPE_"}, value={android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_INVALID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_RDS_PI, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_MODULATION, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION}) @IntRange(from=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.IdentifierType {
- }
-
- @Deprecated @IntDef(prefix={"PROGRAM_TYPE_"}, value={android.hardware.radio.ProgramSelector.PROGRAM_TYPE_INVALID, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DAB, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DRMO, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_SXM}) @IntRange(from=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.ProgramType {
- }
-
public class RadioManager {
method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public void addAnnouncementListener(@NonNull java.util.Set<java.lang.Integer>, @NonNull android.hardware.radio.Announcement.OnListUpdatedListener);
method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public void addAnnouncementListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.Set<java.lang.Integer>, @NonNull android.hardware.radio.Announcement.OnListUpdatedListener);
@@ -5814,9 +5807,6 @@
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.radio.RadioManager.AmBandDescriptor> CREATOR;
}
- @IntDef(prefix={"BAND_"}, value={android.hardware.radio.RadioManager.BAND_INVALID, android.hardware.radio.RadioManager.BAND_AM, android.hardware.radio.RadioManager.BAND_FM, android.hardware.radio.RadioManager.BAND_AM_HD, android.hardware.radio.RadioManager.BAND_FM_HD}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface RadioManager.Band {
- }
-
public static class RadioManager.BandConfig implements android.os.Parcelable {
method public int describeContents();
method public int getLowerLimit();
@@ -5887,8 +5877,8 @@
method public boolean isBackgroundScanningSupported();
method public boolean isCaptureSupported();
method public boolean isInitializationRequired();
- method public boolean isProgramIdentifierSupported(@android.hardware.radio.ProgramSelector.IdentifierType int);
- method public boolean isProgramTypeSupported(@android.hardware.radio.ProgramSelector.ProgramType int);
+ method public boolean isProgramIdentifierSupported(int);
+ method public boolean isProgramTypeSupported(int);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.radio.RadioManager.ModuleProperties> CREATOR;
}
@@ -10572,7 +10562,7 @@
method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public java.util.List<android.os.UserHandle> getUserHandles(boolean);
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.GET_ACCOUNTS_PRIVILEGED}) public android.graphics.Bitmap getUserIcon();
method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}, conditional=true) public android.content.pm.UserProperties getUserProperties(@NonNull android.os.UserHandle);
- method @Deprecated @android.os.UserManager.UserRestrictionSource @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public int getUserRestrictionSource(String, android.os.UserHandle);
+ method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public int getUserRestrictionSource(String, android.os.UserHandle);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.QUERY_USERS}) public java.util.List<android.os.UserManager.EnforcingUser> getUserRestrictionSources(String, android.os.UserHandle);
method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public int getUserSwitchability();
method @NonNull @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.MANAGE_USERS"}) public java.util.Set<android.os.UserHandle> getVisibleUsers();
@@ -10632,14 +10622,11 @@
public static final class UserManager.EnforcingUser implements android.os.Parcelable {
method public int describeContents();
method public android.os.UserHandle getUserHandle();
- method @android.os.UserManager.UserRestrictionSource public int getUserRestrictionSource();
+ method public int getUserRestrictionSource();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.os.UserManager.EnforcingUser> CREATOR;
}
- @IntDef(flag=true, prefix={"RESTRICTION_"}, value={android.os.UserManager.RESTRICTION_NOT_SET, android.os.UserManager.RESTRICTION_SOURCE_SYSTEM, android.os.UserManager.RESTRICTION_SOURCE_DEVICE_OWNER, android.os.UserManager.RESTRICTION_SOURCE_PROFILE_OWNER}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface UserManager.UserRestrictionSource {
- }
-
public abstract class Vibrator {
method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull android.os.Vibrator.OnVibratorStateChangedListener);
method @RequiresPermission(android.Manifest.permission.ACCESS_VIBRATOR_STATE) public void addVibratorStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.os.Vibrator.OnVibratorStateChangedListener);
@@ -11943,13 +11930,13 @@
method public android.service.carrier.CarrierIdentifier getCarrierIdentifier();
method public String getIccid();
method @Nullable public String getNickname();
- method @android.service.euicc.EuiccProfileInfo.PolicyRule public int getPolicyRules();
- method @android.service.euicc.EuiccProfileInfo.ProfileClass public int getProfileClass();
+ method public int getPolicyRules();
+ method public int getProfileClass();
method public String getProfileName();
method public String getServiceProviderName();
- method @android.service.euicc.EuiccProfileInfo.ProfileState public int getState();
+ method public int getState();
method @Nullable public java.util.List<android.telephony.UiccAccessRule> getUiccAccessRules();
- method public boolean hasPolicyRule(@android.service.euicc.EuiccProfileInfo.PolicyRule int);
+ method public boolean hasPolicyRule(int);
method public boolean hasPolicyRules();
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.service.euicc.EuiccProfileInfo> CREATOR;
@@ -11970,23 +11957,14 @@
method public android.service.euicc.EuiccProfileInfo.Builder setCarrierIdentifier(android.service.carrier.CarrierIdentifier);
method public android.service.euicc.EuiccProfileInfo.Builder setIccid(String);
method public android.service.euicc.EuiccProfileInfo.Builder setNickname(String);
- method public android.service.euicc.EuiccProfileInfo.Builder setPolicyRules(@android.service.euicc.EuiccProfileInfo.PolicyRule int);
- method public android.service.euicc.EuiccProfileInfo.Builder setProfileClass(@android.service.euicc.EuiccProfileInfo.ProfileClass int);
+ method public android.service.euicc.EuiccProfileInfo.Builder setPolicyRules(int);
+ method public android.service.euicc.EuiccProfileInfo.Builder setProfileClass(int);
method public android.service.euicc.EuiccProfileInfo.Builder setProfileName(String);
method public android.service.euicc.EuiccProfileInfo.Builder setServiceProviderName(String);
- method public android.service.euicc.EuiccProfileInfo.Builder setState(@android.service.euicc.EuiccProfileInfo.ProfileState int);
+ method public android.service.euicc.EuiccProfileInfo.Builder setState(int);
method public android.service.euicc.EuiccProfileInfo.Builder setUiccAccessRule(@Nullable java.util.List<android.telephony.UiccAccessRule>);
}
- @IntDef(flag=true, prefix={"POLICY_RULE_"}, value={android.service.euicc.EuiccProfileInfo.POLICY_RULE_DO_NOT_DISABLE, android.service.euicc.EuiccProfileInfo.POLICY_RULE_DO_NOT_DELETE, android.service.euicc.EuiccProfileInfo.POLICY_RULE_DELETE_AFTER_DISABLING}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccProfileInfo.PolicyRule {
- }
-
- @IntDef(prefix={"PROFILE_CLASS_"}, value={android.service.euicc.EuiccProfileInfo.PROFILE_CLASS_TESTING, android.service.euicc.EuiccProfileInfo.PROFILE_CLASS_PROVISIONING, android.service.euicc.EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL, 0xffffffff}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccProfileInfo.ProfileClass {
- }
-
- @IntDef(prefix={"PROFILE_STATE_"}, value={android.service.euicc.EuiccProfileInfo.PROFILE_STATE_DISABLED, android.service.euicc.EuiccProfileInfo.PROFILE_STATE_ENABLED, 0xffffffff}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccProfileInfo.ProfileState {
- }
-
public abstract class EuiccService extends android.app.Service {
ctor public EuiccService();
method public void dump(@NonNull java.io.PrintWriter);
@@ -11997,14 +11975,14 @@
method @NonNull public android.service.euicc.DownloadSubscriptionResult onDownloadSubscription(int, int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean, boolean, @NonNull android.os.Bundle);
method @Deprecated public int onDownloadSubscription(int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean, boolean);
method @Deprecated public abstract int onEraseSubscriptions(int);
- method public int onEraseSubscriptions(int, @android.telephony.euicc.EuiccCardManager.ResetOption int);
+ method public int onEraseSubscriptions(int, int);
method public abstract android.service.euicc.GetDefaultDownloadableSubscriptionListResult onGetDefaultDownloadableSubscriptionList(int, boolean);
method public abstract android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, android.telephony.euicc.DownloadableSubscription, boolean);
method @NonNull public android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean);
method public abstract String onGetEid(int);
method @NonNull public abstract android.telephony.euicc.EuiccInfo onGetEuiccInfo(int);
method @NonNull public abstract android.service.euicc.GetEuiccProfileInfoListResult onGetEuiccProfileInfoList(int);
- method @android.telephony.euicc.EuiccManager.OtaStatus public abstract int onGetOtaStatus(int);
+ method public abstract int onGetOtaStatus(int);
method public abstract int onRetainSubscriptionsForFactoryReset(int);
method public abstract void onStartOtaIfNecessary(int, android.service.euicc.EuiccService.OtaStatusChangedCallback);
method @Deprecated public abstract int onSwitchToSubscription(int, @Nullable String, boolean);
@@ -12268,7 +12246,7 @@
public class PersistentDataBlockManager {
method @RequiresPermission(android.Manifest.permission.ACCESS_PDB_STATE) public int getDataBlockSize();
- method @android.service.persistentdata.PersistentDataBlockManager.FlashLockState @RequiresPermission(anyOf={android.Manifest.permission.READ_OEM_UNLOCK_STATE, "android.permission.OEM_UNLOCK_STATE"}) public int getFlashLockState();
+ method @RequiresPermission(anyOf={android.Manifest.permission.READ_OEM_UNLOCK_STATE, "android.permission.OEM_UNLOCK_STATE"}) public int getFlashLockState();
method public long getMaximumDataBlockSize();
method @Deprecated @RequiresPermission(anyOf={android.Manifest.permission.READ_OEM_UNLOCK_STATE, "android.permission.OEM_UNLOCK_STATE"}) public boolean getOemUnlockEnabled();
method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_PDB_STATE) public String getPersistentDataPackageName();
@@ -12281,9 +12259,6 @@
field public static final int FLASH_LOCK_UNLOCKED = 0; // 0x0
}
- @IntDef(prefix={"FLASH_LOCK_"}, value={android.service.persistentdata.PersistentDataBlockManager.FLASH_LOCK_UNKNOWN, android.service.persistentdata.PersistentDataBlockManager.FLASH_LOCK_LOCKED, android.service.persistentdata.PersistentDataBlockManager.FLASH_LOCK_UNLOCKED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PersistentDataBlockManager.FlashLockState {
- }
-
}
package android.service.quicksettings {
@@ -15075,10 +15050,10 @@
public class EuiccCardManager {
method public void authenticateServer(String, String, byte[], byte[], byte[], byte[], java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<byte[]>);
- method public void cancelSession(String, byte[], @android.telephony.euicc.EuiccCardManager.CancelReason int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<byte[]>);
+ method public void cancelSession(String, byte[], int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<byte[]>);
method public void deleteProfile(String, String, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.Void>);
method public void disableProfile(String, String, boolean, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.Void>);
- method public void listNotifications(String, @android.telephony.euicc.EuiccNotification.Event int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.telephony.euicc.EuiccNotification[]>);
+ method public void listNotifications(String, int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.telephony.euicc.EuiccNotification[]>);
method public void loadBoundProfilePackage(String, byte[], java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<byte[]>);
method public void prepareDownload(String, @Nullable byte[], byte[], byte[], byte[], java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<byte[]>);
method public void removeNotificationFromList(String, int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.Void>);
@@ -15091,9 +15066,9 @@
method public void requestProfile(String, String, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.service.euicc.EuiccProfileInfo>);
method public void requestRulesAuthTable(String, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.telephony.euicc.EuiccRulesAuthTable>);
method public void requestSmdsAddress(String, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.String>);
- method public void resetMemory(String, @android.telephony.euicc.EuiccCardManager.ResetOption int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.Void>);
+ method public void resetMemory(String, int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.Void>);
method public void retrieveNotification(String, int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.telephony.euicc.EuiccNotification>);
- method public void retrieveNotificationList(String, @android.telephony.euicc.EuiccNotification.Event int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.telephony.euicc.EuiccNotification[]>);
+ method public void retrieveNotificationList(String, int, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.telephony.euicc.EuiccNotification[]>);
method public void setDefaultSmdpAddress(String, String, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.Void>);
method public void setNickname(String, String, String, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<java.lang.Void>);
method @Deprecated public void switchToProfile(String, String, boolean, java.util.concurrent.Executor, android.telephony.euicc.EuiccCardManager.ResultCallback<android.service.euicc.EuiccProfileInfo>);
@@ -15113,12 +15088,6 @@
field public static final int RESULT_UNKNOWN_ERROR = -1; // 0xffffffff
}
- @IntDef(prefix={"CANCEL_REASON_"}, value={android.telephony.euicc.EuiccCardManager.CANCEL_REASON_END_USER_REJECTED, android.telephony.euicc.EuiccCardManager.CANCEL_REASON_POSTPONED, android.telephony.euicc.EuiccCardManager.CANCEL_REASON_TIMEOUT, android.telephony.euicc.EuiccCardManager.CANCEL_REASON_PPR_NOT_ALLOWED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccCardManager.CancelReason {
- }
-
- @IntDef(flag=true, prefix={"RESET_OPTION_"}, value={android.telephony.euicc.EuiccCardManager.RESET_OPTION_DELETE_OPERATIONAL_PROFILES, android.telephony.euicc.EuiccCardManager.RESET_OPTION_DELETE_FIELD_LOADED_TEST_PROFILES, android.telephony.euicc.EuiccCardManager.RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccCardManager.ResetOption {
- }
-
public static interface EuiccCardManager.ResultCallback<T> {
method public void onComplete(int, T);
}
@@ -15126,7 +15095,7 @@
public class EuiccManager {
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void continueOperation(android.content.Intent, android.os.Bundle);
method @Deprecated @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void eraseSubscriptions(@NonNull android.app.PendingIntent);
- method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void eraseSubscriptions(@android.telephony.euicc.EuiccCardManager.ResetOption int, @NonNull android.app.PendingIntent);
+ method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void eraseSubscriptions(int, @NonNull android.app.PendingIntent);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void getDefaultDownloadableSubscriptionList(android.app.PendingIntent);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public void getDownloadableSubscriptionMetadata(android.telephony.euicc.DownloadableSubscription, android.app.PendingIntent);
method @RequiresPermission(android.Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS) public int getOtaStatus();
@@ -15161,18 +15130,15 @@
field public static final String EXTRA_SUBSCRIPTION_NICKNAME = "android.telephony.euicc.extra.SUBSCRIPTION_NICKNAME";
}
- @IntDef(prefix={"EUICC_OTA_"}, value={android.telephony.euicc.EuiccManager.EUICC_OTA_IN_PROGRESS, android.telephony.euicc.EuiccManager.EUICC_OTA_FAILED, android.telephony.euicc.EuiccManager.EUICC_OTA_SUCCEEDED, android.telephony.euicc.EuiccManager.EUICC_OTA_NOT_NEEDED, android.telephony.euicc.EuiccManager.EUICC_OTA_STATUS_UNAVAILABLE}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccManager.OtaStatus {
- }
-
public final class EuiccNotification implements android.os.Parcelable {
- ctor public EuiccNotification(int, String, @android.telephony.euicc.EuiccNotification.Event int, @Nullable byte[]);
+ ctor public EuiccNotification(int, String, int, @Nullable byte[]);
method public int describeContents();
method @Nullable public byte[] getData();
- method @android.telephony.euicc.EuiccNotification.Event public int getEvent();
+ method public int getEvent();
method public int getSeq();
method public String getTargetAddr();
method public void writeToParcel(android.os.Parcel, int);
- field @android.telephony.euicc.EuiccNotification.Event public static final int ALL_EVENTS = 15; // 0xf
+ field public static final int ALL_EVENTS = 15; // 0xf
field @NonNull public static final android.os.Parcelable.Creator<android.telephony.euicc.EuiccNotification> CREATOR;
field public static final int EVENT_DELETE = 8; // 0x8
field public static final int EVENT_DISABLE = 4; // 0x4
@@ -15180,13 +15146,10 @@
field public static final int EVENT_INSTALL = 1; // 0x1
}
- @IntDef(flag=true, prefix={"EVENT_"}, value={android.telephony.euicc.EuiccNotification.EVENT_INSTALL, android.telephony.euicc.EuiccNotification.EVENT_ENABLE, android.telephony.euicc.EuiccNotification.EVENT_DISABLE, android.telephony.euicc.EuiccNotification.EVENT_DELETE}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccNotification.Event {
- }
-
public final class EuiccRulesAuthTable implements android.os.Parcelable {
method public int describeContents();
- method public int findIndex(@android.service.euicc.EuiccProfileInfo.PolicyRule int, android.service.carrier.CarrierIdentifier);
- method public boolean hasPolicyRuleFlag(int, @android.telephony.euicc.EuiccRulesAuthTable.PolicyRuleFlag int);
+ method public int findIndex(int, android.service.carrier.CarrierIdentifier);
+ method public boolean hasPolicyRuleFlag(int, int);
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.telephony.euicc.EuiccRulesAuthTable> CREATOR;
field public static final int POLICY_RULE_FLAG_CONSENT_REQUIRED = 1; // 0x1
@@ -15198,9 +15161,6 @@
method public android.telephony.euicc.EuiccRulesAuthTable build();
}
- @IntDef(flag=true, prefix={"POLICY_RULE_FLAG_"}, value={android.telephony.euicc.EuiccRulesAuthTable.POLICY_RULE_FLAG_CONSENT_REQUIRED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccRulesAuthTable.PolicyRuleFlag {
- }
-
}
package android.telephony.gba {
@@ -17100,7 +17060,7 @@
}
public abstract class Window {
- method public void addSystemFlags(@android.view.WindowManager.LayoutParams.SystemFlags int);
+ method public void addSystemFlags(int);
}
public interface WindowManager extends android.view.ViewManager {
@@ -17119,9 +17079,6 @@
field @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW) public static final int SYSTEM_FLAG_SHOW_FOR_ALL_USERS = 16; // 0x10
}
- @IntDef(flag=true, prefix={"SYSTEM_FLAG_"}, value={android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, android.view.WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface WindowManager.LayoutParams.SystemFlags {
- }
-
}
package android.view.accessibility {
diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt
index 8652402..dec1ee5 100644
--- a/core/api/system-lint-baseline.txt
+++ b/core/api/system-lint-baseline.txt
@@ -517,6 +517,18 @@
Methods must not throw generic exceptions (`java.lang.Throwable`)
+InvalidNullabilityOverride: android.service.textclassifier.TextClassifierService#onUnbind(android.content.Intent) parameter #0:
+ Invalid nullability on parameter `intent` in method `onUnbind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+InvalidNullabilityOverride: android.service.voice.HotwordDetectionService#getSystemService(String) parameter #0:
+ Invalid nullability on parameter `name` in method `getSystemService`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+InvalidNullabilityOverride: android.service.voice.VisualQueryDetectionService#getSystemService(String) parameter #0:
+ Invalid nullability on parameter `name` in method `getSystemService`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+InvalidNullabilityOverride: android.service.voice.VisualQueryDetectionService#openFileInput(String):
+ Invalid nullability on method `openFileInput` return. Overrides of unannotated super method cannot be Nullable.
+InvalidNullabilityOverride: android.service.voice.VisualQueryDetectionService#openFileInput(String) parameter #0:
+ Invalid nullability on parameter `filename` in method `openFileInput`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+
+
KotlinKeyword: android.app.Notification#when:
Avoid field names that are Kotlin hard keywords ("when"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords
diff --git a/core/api/system-removed.txt b/core/api/system-removed.txt
index 402a718..51b8a11 100644
--- a/core/api/system-removed.txt
+++ b/core/api/system-removed.txt
@@ -112,6 +112,22 @@
method @Deprecated public void requestRemoteDeviceToBecomeActiveSource(@NonNull android.hardware.hdmi.HdmiDeviceInfo);
}
+ @IntDef({android.hardware.hdmi.HdmiControlManager.RESULT_SUCCESS, android.hardware.hdmi.HdmiControlManager.RESULT_TIMEOUT, android.hardware.hdmi.HdmiControlManager.RESULT_SOURCE_NOT_AVAILABLE, android.hardware.hdmi.HdmiControlManager.RESULT_TARGET_NOT_AVAILABLE, android.hardware.hdmi.HdmiControlManager.RESULT_ALREADY_IN_PROGRESS, android.hardware.hdmi.HdmiControlManager.RESULT_EXCEPTION, android.hardware.hdmi.HdmiControlManager.RESULT_INCORRECT_MODE, android.hardware.hdmi.HdmiControlManager.RESULT_COMMUNICATION_FAILED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface HdmiControlManager.ControlCallbackResult {
+ }
+
+}
+
+package android.hardware.radio {
+
+ @IntDef(prefix={"IDENTIFIER_TYPE_"}, value={android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_INVALID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_RDS_PI, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_MODULATION, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION}) @IntRange(from=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.IdentifierType {
+ }
+
+ @Deprecated @IntDef(prefix={"PROGRAM_TYPE_"}, value={android.hardware.radio.ProgramSelector.PROGRAM_TYPE_INVALID, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DAB, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DRMO, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_SXM}) @IntRange(from=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.ProgramType {
+ }
+
+ @IntDef(prefix={"BAND_"}, value={android.hardware.radio.RadioManager.BAND_INVALID, android.hardware.radio.RadioManager.BAND_AM, android.hardware.radio.RadioManager.BAND_FM, android.hardware.radio.RadioManager.BAND_AM_HD, android.hardware.radio.RadioManager.BAND_FM_HD}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface RadioManager.Band {
+ }
+
}
package android.media.tv {
@@ -145,6 +161,19 @@
}
+package android.service.euicc {
+
+ @IntDef(flag=true, prefix={"POLICY_RULE_"}, value={android.service.euicc.EuiccProfileInfo.POLICY_RULE_DO_NOT_DISABLE, android.service.euicc.EuiccProfileInfo.POLICY_RULE_DO_NOT_DELETE, android.service.euicc.EuiccProfileInfo.POLICY_RULE_DELETE_AFTER_DISABLING}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccProfileInfo.PolicyRule {
+ }
+
+ @IntDef(prefix={"PROFILE_CLASS_"}, value={android.service.euicc.EuiccProfileInfo.PROFILE_CLASS_TESTING, android.service.euicc.EuiccProfileInfo.PROFILE_CLASS_PROVISIONING, android.service.euicc.EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL, 0xffffffff}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccProfileInfo.ProfileClass {
+ }
+
+ @IntDef(prefix={"PROFILE_STATE_"}, value={android.service.euicc.EuiccProfileInfo.PROFILE_STATE_DISABLED, android.service.euicc.EuiccProfileInfo.PROFILE_STATE_ENABLED, 0xffffffff}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccProfileInfo.ProfileState {
+ }
+
+}
+
package android.service.notification {
public abstract class NotificationListenerService extends android.app.Service {
@@ -165,6 +194,13 @@
}
+package android.service.persistentdata {
+
+ @IntDef(prefix={"FLASH_LOCK_"}, value={android.service.persistentdata.PersistentDataBlockManager.FLASH_LOCK_UNKNOWN, android.service.persistentdata.PersistentDataBlockManager.FLASH_LOCK_LOCKED, android.service.persistentdata.PersistentDataBlockManager.FLASH_LOCK_UNLOCKED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface PersistentDataBlockManager.FlashLockState {
+ }
+
+}
+
package android.service.search {
public abstract class SearchUiService extends android.app.Service {
@@ -213,6 +249,22 @@
}
+package android.telephony.euicc {
+
+ @IntDef(prefix={"CANCEL_REASON_"}, value={android.telephony.euicc.EuiccCardManager.CANCEL_REASON_END_USER_REJECTED, android.telephony.euicc.EuiccCardManager.CANCEL_REASON_POSTPONED, android.telephony.euicc.EuiccCardManager.CANCEL_REASON_TIMEOUT, android.telephony.euicc.EuiccCardManager.CANCEL_REASON_PPR_NOT_ALLOWED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccCardManager.CancelReason {
+ }
+
+ @IntDef(flag=true, prefix={"RESET_OPTION_"}, value={android.telephony.euicc.EuiccCardManager.RESET_OPTION_DELETE_OPERATIONAL_PROFILES, android.telephony.euicc.EuiccCardManager.RESET_OPTION_DELETE_FIELD_LOADED_TEST_PROFILES, android.telephony.euicc.EuiccCardManager.RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccCardManager.ResetOption {
+ }
+
+ @IntDef(flag=true, prefix={"EVENT_"}, value={android.telephony.euicc.EuiccNotification.EVENT_INSTALL, android.telephony.euicc.EuiccNotification.EVENT_ENABLE, android.telephony.euicc.EuiccNotification.EVENT_DISABLE, android.telephony.euicc.EuiccNotification.EVENT_DELETE}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccNotification.Event {
+ }
+
+ @IntDef(flag=true, prefix={"POLICY_RULE_FLAG_"}, value={android.telephony.euicc.EuiccRulesAuthTable.POLICY_RULE_FLAG_CONSENT_REQUIRED}) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface EuiccRulesAuthTable.PolicyRuleFlag {
+ }
+
+}
+
package android.telephony.ims {
public interface DelegateStateCallback {
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 3a91e25..bf26bd0 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -511,6 +511,16 @@
Method javax.microedition.khronos.egl.EGL10.eglCreatePixmapSurface(javax.microedition.khronos.egl.EGLDisplay, javax.microedition.khronos.egl.EGLConfig, Object, int[]): @Deprecated annotation (present) and @deprecated doc tag (not present) do not match
+InvalidNullabilityOverride: android.window.WindowProviderService#getSystemService(String) parameter #0:
+ Invalid nullability on parameter `name` in method `getSystemService`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+InvalidNullabilityOverride: android.window.WindowProviderService#onConfigurationChanged(android.content.res.Configuration) parameter #0:
+ Invalid nullability on parameter `configuration` in method `onConfigurationChanged`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+InvalidNullabilityOverride: android.window.WindowProviderService#registerComponentCallbacks(android.content.ComponentCallbacks) parameter #0:
+ Invalid nullability on parameter `callback` in method `registerComponentCallbacks`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+InvalidNullabilityOverride: android.window.WindowProviderService#unregisterComponentCallbacks(android.content.ComponentCallbacks) parameter #0:
+ Invalid nullability on parameter `callback` in method `unregisterComponentCallbacks`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
+
+
KotlinKeyword: android.app.Notification#when:
Avoid field names that are Kotlin hard keywords ("when"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index c136db6..02eaf0b 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -56,7 +56,7 @@
import android.app.backup.BackupAnnotations.OperationType;
import android.app.compat.CompatChanges;
import android.app.sdksandbox.sandboxactivity.ActivityContextInfo;
-import android.app.sdksandbox.sandboxactivity.ActivityContextInfoProvider;
+import android.app.sdksandbox.sandboxactivity.SdkSandboxActivityAuthority;
import android.app.servertransaction.ActivityLifecycleItem;
import android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
import android.app.servertransaction.ActivityRelaunchItem;
@@ -3795,8 +3795,10 @@
r.activityInfo.targetActivity);
}
- boolean isSandboxActivityContext = sandboxActivitySdkBasedContext()
- && r.intent.isSandboxActivity(mSystemContext);
+ boolean isSandboxActivityContext =
+ sandboxActivitySdkBasedContext()
+ && SdkSandboxActivityAuthority.isSdkSandboxActivity(
+ mSystemContext, r.intent);
boolean isSandboxedSdkContextUsed = false;
ContextImpl activityBaseContext;
if (isSandboxActivityContext) {
@@ -4041,11 +4043,12 @@
*/
@Nullable
private ContextImpl createBaseContextForSandboxActivity(@NonNull ActivityClientRecord r) {
- ActivityContextInfoProvider contextInfoProvider = ActivityContextInfoProvider.getInstance();
+ SdkSandboxActivityAuthority sdkSandboxActivityAuthority =
+ SdkSandboxActivityAuthority.getInstance();
ActivityContextInfo contextInfo;
try {
- contextInfo = contextInfoProvider.getActivityContextInfo(r.intent);
+ contextInfo = sdkSandboxActivityAuthority.getActivityContextInfo(r.intent);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Passed intent does not match an expected sandbox activity", e);
return null;
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index b74b075..c5e132f 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -9927,10 +9927,11 @@
if (i != firstInteresting) {
sb.append('\n');
}
- if (!sFullLog && sb.length() + trace[i].toString().length() > 600) {
+ final String traceString = trace[i].toString();
+ if (!sFullLog && sb.length() + traceString.length() > 600) {
break;
}
- sb.append(trace[i]);
+ sb.append(traceString);
}
return sb.toString();
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 665ba11..c7a86fb 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -12605,8 +12605,12 @@
return (mFlags & FLAG_ACTIVITY_NEW_DOCUMENT) == FLAG_ACTIVITY_NEW_DOCUMENT;
}
- // TODO(b/299109198): Refactor into the {@link SdkSandboxManagerLocal}
- /** @hide */
+ /**
+ * @deprecated Use {@link SdkSandboxActivityAuthority#isSdkSandboxActivity} instead.
+ * Once the other API is finalized this method will be removed.
+ * @hide
+ */
+ @Deprecated
@android.ravenwood.annotation.RavenwoodThrow
public boolean isSandboxActivity(@NonNull Context context) {
if (mAction != null && mAction.equals(ACTION_START_SANDBOXED_ACTIVITY)) {
diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java
index 63c11b7..1cfdb8b 100644
--- a/core/java/android/content/pm/PackageInfo.java
+++ b/core/java/android/content/pm/PackageInfo.java
@@ -16,8 +16,11 @@
package android.content.pm;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.Parcel;
@@ -244,6 +247,14 @@
public Attribution[] attributions;
/**
+ * The time at which the app was archived for the user. Units are as
+ * per {@link System#currentTimeMillis()}.
+ * @hide
+ */
+ @CurrentTimeMillisLong
+ private long mArchiveTimeMillis;
+
+ /**
* Flag for {@link #requestedPermissionsFlags}: the requested permission
* is required for the application to run; the user can not optionally
* disable it. Currently all permissions are required.
@@ -508,6 +519,24 @@
return overlayTarget != null && mOverlayIsStatic;
}
+ /**
+ * Returns the time at which the app was archived for the user. Units are as
+ * per {@link System#currentTimeMillis()}.
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(Flags.FLAG_ARCHIVING)
+ public @CurrentTimeMillisLong long getArchiveTimeMillis() {
+ return mArchiveTimeMillis;
+ }
+
+ /**
+ * @hide
+ */
+ public void setArchiveTimeMillis(@CurrentTimeMillisLong long value) {
+ mArchiveTimeMillis = value;
+ }
+
@Override
public String toString() {
return "PackageInfo{"
@@ -575,6 +604,7 @@
}
dest.writeBoolean(isApex);
dest.writeBoolean(isActiveApex);
+ dest.writeLong(mArchiveTimeMillis);
dest.restoreAllowSquashing(prevAllowSquashing);
}
@@ -640,5 +670,6 @@
}
isApex = source.readBoolean();
isActiveApex = source.readBoolean();
+ mArchiveTimeMillis = source.readLong();
}
}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index f87c8d0..c3b3423 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -43,6 +43,7 @@
import android.app.PropertyInvalidatedCache;
import android.app.admin.DevicePolicyManager;
import android.app.usage.StorageStatsManager;
+import android.companion.virtual.VirtualDeviceManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
@@ -710,12 +711,31 @@
*/
@SystemApi
public interface OnPermissionsChangedListener {
+ /**
+ * Called when the permissions for a UID change for the default device.
+ *
+ * @param uid The UID with a change.
+ * @see Context#DEVICE_ID_DEFAULT
+ */
+ void onPermissionsChanged(int uid);
/**
- * Called when the permissions for a UID change.
- * @param uid The UID with a change.
+ * Called when the permissions for a UID change for a device, including virtual devices.
+ *
+ * @param uid The UID of permission change event.
+ * @param persistentDeviceId The persistent device ID of permission change event.
+ *
+ * @see VirtualDeviceManager.VirtualDevice#getPersistentDeviceId()
+ * @see VirtualDeviceManager#PERSISTENT_DEVICE_ID_DEFAULT
*/
- public void onPermissionsChanged(int uid);
+ @FlaggedApi(android.permission.flags.Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS)
+ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) {
+ Objects.requireNonNull(persistentDeviceId);
+ if (Objects.equals(persistentDeviceId,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT)) {
+ onPermissionsChanged(uid);
+ }
+ }
}
/** @hide */
@@ -6384,9 +6404,8 @@
/**
* Permission flags set when granting or revoking a permission.
*
- * @hide
+ * @removed mistakenly exposed as system-api previously
*/
- @SystemApi
@IntDef(prefix = { "FLAG_PERMISSION_" }, value = {
FLAG_PERMISSION_USER_SET,
FLAG_PERMISSION_USER_FIXED,
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index 440585c..09741e52 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -149,6 +149,7 @@
public static final int POWER_STATUS_TRANSIENT_TO_ON = 2;
public static final int POWER_STATUS_TRANSIENT_TO_STANDBY = 3;
+ /** @removed mistakenly exposed previously */
@IntDef ({
RESULT_SUCCESS,
RESULT_TIMEOUT,
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index 7f2d8a0..5985c39 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -22,6 +22,7 @@
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Log;
import android.view.KeyEvent;
import java.util.Arrays;
@@ -80,7 +81,10 @@
+ event.getKeyCode()
+ " sent to a VirtualDpad input device.");
}
- mVirtualDevice.sendDpadKeyEvent(mToken, event);
+ if (!mVirtualDevice.sendDpadKeyEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send key event to virtual dpad "
+ + mConfig.getInputDeviceName());
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
index 931e1ff..affa4ed 100644
--- a/core/java/android/hardware/input/VirtualInputDevice.java
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -20,6 +20,7 @@
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Log;
import java.io.Closeable;
@@ -32,6 +33,8 @@
*/
abstract class VirtualInputDevice implements Closeable {
+ protected static final String TAG = "VirtualInputDevice";
+
/**
* The virtual device to which this VirtualInputDevice belongs to.
*/
@@ -67,6 +70,7 @@
@Override
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void close() {
+ Log.d(TAG, "Closing virtual input device " + mConfig.getInputDeviceName());
try {
mVirtualDevice.unregisterInputDevice(mToken);
} catch (RemoteException e) {
diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
index a8caa58..a87980c 100644
--- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java
+++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
@@ -19,6 +19,10 @@
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.os.Parcel;
+import android.view.Display;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
/**
* Common configurations to create virtual input devices.
@@ -27,6 +31,15 @@
*/
@SystemApi
public abstract class VirtualInputDeviceConfig {
+
+ /**
+ * The maximum length of a device name (in bytes in UTF-8 encoding).
+ *
+ * This limitation comes directly from uinput.
+ * See also UINPUT_MAX_NAME_SIZE in linux/uinput.h
+ */
+ private static final int DEVICE_NAME_MAX_LENGTH = 80;
+
/** The vendor id uniquely identifies the company who manufactured the device. */
private final int mVendorId;
/**
@@ -44,18 +57,33 @@
mVendorId = builder.mVendorId;
mProductId = builder.mProductId;
mAssociatedDisplayId = builder.mAssociatedDisplayId;
- mInputDeviceName = builder.mInputDeviceName;
+ mInputDeviceName = Objects.requireNonNull(builder.mInputDeviceName);
+
+ if (mAssociatedDisplayId == Display.INVALID_DISPLAY) {
+ throw new IllegalArgumentException(
+ "Display association is required for virtual input devices.");
+ }
+
+ // Comparison is greater or equal because the device name must fit into a const char*
+ // including the \0-terminator. Therefore the actual number of bytes that can be used
+ // for device name is DEVICE_NAME_MAX_LENGTH - 1
+ if (mInputDeviceName.getBytes(StandardCharsets.UTF_8).length >= DEVICE_NAME_MAX_LENGTH) {
+ throw new IllegalArgumentException("Input device name exceeds maximum length of "
+ + DEVICE_NAME_MAX_LENGTH + "bytes: " + mInputDeviceName);
+ }
}
protected VirtualInputDeviceConfig(@NonNull Parcel in) {
mVendorId = in.readInt();
mProductId = in.readInt();
mAssociatedDisplayId = in.readInt();
- mInputDeviceName = in.readString8();
+ mInputDeviceName = Objects.requireNonNull(in.readString8());
}
/**
* The vendor id uniquely identifies the company who manufactured the device.
+ *
+ * @see Builder#setVendorId(int) (int)
*/
public int getVendorId() {
return mVendorId;
@@ -64,6 +92,8 @@
/**
* The product id uniquely identifies which product within the address space of a given vendor,
* identified by the device's vendor id.
+ *
+ * @see Builder#setProductId(int)
*/
public int getProductId() {
return mProductId;
@@ -71,6 +101,8 @@
/**
* The associated display ID of the virtual input device.
+ *
+ * @see Builder#setAssociatedDisplayId(int)
*/
public int getAssociatedDisplayId() {
return mAssociatedDisplayId;
@@ -78,6 +110,8 @@
/**
* The name of the virtual input device.
+ *
+ * @see Builder#setInputDeviceName(String)
*/
@NonNull
public String getInputDeviceName() {
@@ -117,11 +151,12 @@
private int mVendorId;
private int mProductId;
- private int mAssociatedDisplayId;
- @NonNull
+ private int mAssociatedDisplayId = Display.INVALID_DISPLAY;
private String mInputDeviceName;
- /** @see VirtualInputDeviceConfig#getVendorId(). */
+ /**
+ * Sets the vendor id of the device, identifying the company who manufactured the device.
+ */
@NonNull
public T setVendorId(int vendorId) {
mVendorId = vendorId;
@@ -129,24 +164,40 @@
}
- /** @see VirtualInputDeviceConfig#getProductId(). */
+ /**
+ * Sets the product id of the device, uniquely identifying the device within the address
+ * space of a given vendor, identified by the device's vendor id.
+ */
@NonNull
public T setProductId(int productId) {
mProductId = productId;
return self();
}
- /** @see VirtualInputDeviceConfig#getAssociatedDisplayId(). */
+ /**
+ * Sets the associated display ID of the virtual input device. Required.
+ *
+ * <p>The input device is restricted to the display with the given ID and may not send
+ * events to any other display.</p>
+ */
@NonNull
public T setAssociatedDisplayId(int displayId) {
mAssociatedDisplayId = displayId;
return self();
}
- /** @see VirtualInputDeviceConfig#getInputDeviceName(). */
+ /**
+ * Sets the name of the virtual input device. Required.
+ *
+ * <p>The name must be unique among all input devices that belong to the same virtual
+ * device.</p>
+ *
+ * <p>The maximum allowed length of the name is 80 bytes in UTF-8 encoding, enforced by
+ * {@code UINPUT_MAX_NAME_SIZE}.</p>
+ */
@NonNull
public T setInputDeviceName(@NonNull String deviceName) {
- mInputDeviceName = deviceName;
+ mInputDeviceName = Objects.requireNonNull(deviceName);
return self();
}
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index c90f893..6eb2ae3 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -22,6 +22,7 @@
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Log;
import android.view.KeyEvent;
/**
@@ -57,7 +58,10 @@
"Unsupported key code " + event.getKeyCode()
+ " sent to a VirtualKeyboard input device.");
}
- mVirtualDevice.sendKeyEvent(mToken, event);
+ if (!mVirtualDevice.sendKeyEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send key event to virtual keyboard "
+ + mConfig.getInputDeviceName());
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index 51f3f69..fb0f700 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -23,6 +23,7 @@
import android.graphics.PointF;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Log;
import android.view.MotionEvent;
/**
@@ -52,7 +53,10 @@
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendButtonEvent(@NonNull VirtualMouseButtonEvent event) {
try {
- mVirtualDevice.sendButtonEvent(mToken, event);
+ if (!mVirtualDevice.sendButtonEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send button event to virtual mouse "
+ + mConfig.getInputDeviceName());
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -69,7 +73,10 @@
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendScrollEvent(@NonNull VirtualMouseScrollEvent event) {
try {
- mVirtualDevice.sendScrollEvent(mToken, event);
+ if (!mVirtualDevice.sendScrollEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send scroll event to virtual mouse "
+ + mConfig.getInputDeviceName());
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -85,7 +92,10 @@
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendRelativeEvent(@NonNull VirtualMouseRelativeEvent event) {
try {
- mVirtualDevice.sendRelativeEvent(mToken, event);
+ if (!mVirtualDevice.sendRelativeEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send relative event to virtual mouse "
+ + mConfig.getInputDeviceName());
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
index 61d72e2..3dbb385 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpad.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
@@ -22,6 +22,7 @@
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Log;
/**
* A virtual navigation touchpad representing a touch-based input mechanism on a remote device.
@@ -53,7 +54,10 @@
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
try {
- mVirtualDevice.sendTouchEvent(mToken, event);
+ if (!mVirtualDevice.sendTouchEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send touch event to virtual navigation touchpad "
+ + mConfig.getInputDeviceName());
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index 4ac439e..2c800aa 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -22,6 +22,7 @@
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
import android.os.RemoteException;
+import android.util.Log;
/**
* A virtual touchscreen representing a touch-based display input mechanism on a remote device.
@@ -47,7 +48,10 @@
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
try {
- mVirtualDevice.sendTouchEvent(mToken, event);
+ if (!mVirtualDevice.sendTouchEvent(mToken, event)) {
+ Log.w(TAG, "Failed to send touch event to virtual touchscreen "
+ + mConfig.getInputDeviceName());
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index c7ec052..7e5c141 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -109,7 +109,10 @@
/** @deprecated use {@link ProgramIdentifier} instead */
@Deprecated
public static final int PROGRAM_TYPE_VENDOR_END = 1999;
- /** @deprecated use {@link ProgramIdentifier} instead */
+ /**
+ * @deprecated use {@link ProgramIdentifier} instead
+ * @removed mistakenly exposed previously
+ */
@Deprecated
@IntDef(prefix = { "PROGRAM_TYPE_" }, value = {
PROGRAM_TYPE_INVALID,
@@ -397,6 +400,7 @@
*/
@Deprecated
public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = IDENTIFIER_TYPE_VENDOR_END;
+ /** @removed mistakenly exposed previously */
@IntDef(prefix = { "IDENTIFIER_TYPE_" }, value = {
IDENTIFIER_TYPE_INVALID,
IDENTIFIER_TYPE_AMFM_FREQUENCY,
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 237ec01..f0f7e8a 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -123,6 +123,7 @@
/** AM HD radio or DRM band.
* @see BandDescriptor */
public static final int BAND_AM_HD = 3;
+ /** @removed mistakenly exposed previously */
@IntDef(prefix = { "BAND_" }, value = {
BAND_INVALID,
BAND_AM,
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java
index f16e243..e2d215e 100644
--- a/core/java/android/inputmethodservice/AbstractInputMethodService.java
+++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java
@@ -33,6 +33,8 @@
import android.view.inputmethod.InputMethodSession;
import android.window.WindowProviderService;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -72,8 +74,9 @@
* {@code null} if {@link #onCreateInputMethodInterface()} is not yet called.
* @hide
*/
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
@Nullable
- protected final InputMethod getInputMethodInternal() {
+ public final InputMethod getInputMethodInternal() {
return mInputMethod;
}
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index ba80811..18d3e5e 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -149,6 +149,7 @@
import android.window.WindowMetricsHelper;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.IInlineSuggestionsRequestCallback;
import com.android.internal.inputmethod.IInputContentUriToken;
import com.android.internal.inputmethod.IInputMethod;
@@ -3997,6 +3998,16 @@
}
/**
+ * Returns whether the IME navigation bar is currently shown, for testing purposes.
+ *
+ * @hide
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public final boolean isImeNavigationBarShownForTesting() {
+ return mNavigationBarController.isShown();
+ }
+
+ /**
* Used to inject custom {@link InputMethodServiceInternal}.
*
* @return the {@link InputMethodServiceInternal} to be used.
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index 8be4c58..9c55b0e 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -77,6 +77,10 @@
default void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) {
}
+ default boolean isShown() {
+ return false;
+ }
+
default String toDebugString() {
return "No-op implementation";
}
@@ -117,6 +121,13 @@
mImpl.onNavButtonFlagsChanged(navButtonFlags);
}
+ /**
+ * Returns whether the IME navigation bar is currently shown.
+ */
+ boolean isShown() {
+ return mImpl.isShown();
+ }
+
String toDebugString() {
return mImpl.toDebugString();
}
@@ -561,6 +572,12 @@
}
@Override
+ public boolean isShown() {
+ return mNavigationBarFrame != null
+ && mNavigationBarFrame.getVisibility() == View.VISIBLE;
+ }
+
+ @Override
public String toDebugString() {
return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar
+ " mNavigationBarFrame=" + mNavigationBarFrame
diff --git a/core/java/android/net/NetworkStack.java b/core/java/android/net/NetworkStack.java
index 19ba6a1..dbb3127 100644
--- a/core/java/android/net/NetworkStack.java
+++ b/core/java/android/net/NetworkStack.java
@@ -23,7 +23,6 @@
import android.os.IBinder;
import android.os.ServiceManager;
-import com.android.net.flags.Flags;
import com.android.net.module.util.PermissionUtils;
/**
* Constants and utilities for client code communicating with the network stack service.
@@ -104,16 +103,4 @@
final @NonNull String... otherPermissions) {
PermissionUtils.enforceNetworkStackPermissionOr(context, otherPermissions);
}
-
- /**
- * Get setting of the "set_data_saver_via_cm" flag.
- *
- * @hide
- */
- // A workaround for aconfig. Currently, aconfig value read from platform and mainline code can
- // be inconsistent. To avoid the problem, CTS for mainline code can get the flag value by this
- // method.
- public static boolean getDataSaverViaCmFlag() {
- return Flags.setDataSaverViaCm();
- }
}
diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
index 597c948..e331c95 100644
--- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java
+++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java
@@ -21,6 +21,7 @@
package android.nfc.cardemulation;
import android.annotation.FlaggedApi;
+import android.compat.annotation.UnsupportedAppUsage;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
@@ -134,8 +135,9 @@
/**
* @hide
*/
+ @UnsupportedAppUsage
public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
- List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
+ ArrayList<AidGroup> staticAidGroups, ArrayList<AidGroup> dynamicAidGroups,
boolean requiresUnlock, int bannerResource, int uid,
String settingsActivityName, String offHost, String staticOffHost) {
this(info, onHost, description, staticAidGroups, dynamicAidGroups,
@@ -147,7 +149,7 @@
* @hide
*/
public ApduServiceInfo(ResolveInfo info, boolean onHost, String description,
- List<AidGroup> staticAidGroups, List<AidGroup> dynamicAidGroups,
+ ArrayList<AidGroup> staticAidGroups, ArrayList<AidGroup> dynamicAidGroups,
boolean requiresUnlock, int bannerResource, int uid,
String settingsActivityName, String offHost, String staticOffHost,
boolean isEnabled) {
diff --git a/core/java/android/os/OWNERS b/core/java/android/os/OWNERS
index 8f7725ec..655debc 100644
--- a/core/java/android/os/OWNERS
+++ b/core/java/android/os/OWNERS
@@ -87,3 +87,7 @@
# PerformanceHintManager
per-file PerformanceHintManager.java = file:/ADPF_OWNERS
+
+# IThermal interfaces
+per-file IThermal* = file:/THERMAL_OWNERS
+
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 2419a4c..08d6e02 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -248,7 +248,7 @@
@SystemApi
public static final int RESTRICTION_SOURCE_PROFILE_OWNER = 0x4;
- /** @hide */
+ /** @removed mistakenly exposed as system-api previously */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = { "RESTRICTION_" }, value = {
RESTRICTION_NOT_SET,
@@ -256,7 +256,6 @@
RESTRICTION_SOURCE_DEVICE_OWNER,
RESTRICTION_SOURCE_PROFILE_OWNER
})
- @SystemApi
public @interface UserRestrictionSource {}
/**
diff --git a/core/java/android/permission/IOnPermissionsChangeListener.aidl b/core/java/android/permission/IOnPermissionsChangeListener.aidl
index cc52a72..afacf1a 100644
--- a/core/java/android/permission/IOnPermissionsChangeListener.aidl
+++ b/core/java/android/permission/IOnPermissionsChangeListener.aidl
@@ -21,5 +21,5 @@
* {@hide}
*/
oneway interface IOnPermissionsChangeListener {
- void onPermissionsChanged(int uid);
+ void onPermissionsChanged(int uid, String deviceId);
}
diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java
index e10ea10..7a158c5 100644
--- a/core/java/android/permission/PermissionManager.java
+++ b/core/java/android/permission/PermissionManager.java
@@ -1725,7 +1725,7 @@
}
private final class OnPermissionsChangeListenerDelegate
- extends IOnPermissionsChangeListener.Stub implements Handler.Callback{
+ extends IOnPermissionsChangeListener.Stub implements Handler.Callback {
private static final int MSG_PERMISSIONS_CHANGED = 1;
private final PackageManager.OnPermissionsChangedListener mListener;
@@ -1738,8 +1738,8 @@
}
@Override
- public void onPermissionsChanged(int uid) {
- mHandler.obtainMessage(MSG_PERMISSIONS_CHANGED, uid, 0).sendToTarget();
+ public void onPermissionsChanged(int uid, String deviceId) {
+ mHandler.obtainMessage(MSG_PERMISSIONS_CHANGED, uid, 0, deviceId).sendToTarget();
}
@Override
@@ -1747,7 +1747,8 @@
switch (msg.what) {
case MSG_PERMISSIONS_CHANGED: {
final int uid = msg.arg1;
- mListener.onPermissionsChanged(uid);
+ final String deviceId = msg.obj.toString();
+ mListener.onPermissionsChanged(uid, deviceId);
return true;
}
default:
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 9bdd0c2..0133bd8 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -36,10 +36,3 @@
description: "Collect sepolicy hash from sysfs"
bug: "308471499"
}
-
-flag {
- name: "extend_ecm_to_all_settings"
- namespace: "responsible_apis"
- description: "Allow all app settings to be restrictable via configuration"
- bug: "297372999"
-}
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
new file mode 100644
index 0000000..4e5588c
--- /dev/null
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -0,0 +1,22 @@
+package: "android.security"
+
+flag {
+ name: "extend_ecm_to_all_settings"
+ namespace: "responsible_apis"
+ description: "Allow all app settings to be restrictable via configuration"
+ bug: "297372999"
+}
+
+flag {
+ name: "asm_restrictions_enabled"
+ namespace: "responsible_apis"
+ description: "Enables ASM restrictions for activity starts and finishes"
+ bug: "230590090"
+}
+
+flag {
+ name: "asm_toasts_enabled"
+ namespace: "responsible_apis"
+ description: "Enables toasts when ASM restrictions are triggered"
+ bug: "230590090"
+}
diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java
index 5b096c6..db0b7ff 100644
--- a/core/java/android/service/notification/ZenDeviceEffects.java
+++ b/core/java/android/service/notification/ZenDeviceEffects.java
@@ -178,6 +178,16 @@
return mMaximizeDoze;
}
+ /**
+ * Whether any of the effects are set up.
+ * @hide
+ */
+ public boolean hasEffects() {
+ return mGrayscale || mSuppressAmbientDisplay || mDimWallpaper || mNightMode
+ || mDisableAutoBrightness || mDisableTapToWake || mDisableTiltToWake
+ || mDisableTouch || mMinimizeRadioUsage || mMaximizeDoze;
+ }
+
/** {@link Parcelable.Creator} that instantiates {@link ZenDeviceEffects} objects. */
@NonNull
public static final Creator<ZenDeviceEffects> CREATOR = new Creator<ZenDeviceEffects>() {
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index fedad89..f1d35b5 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -25,6 +25,7 @@
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
+import android.annotation.FlaggedApi;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AlarmManager;
@@ -185,6 +186,18 @@
private static final String RULE_ATT_ICON = "rule_icon";
private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
+ private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
+ private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
+ "zdeSuppressAmbientDisplay";
+ private static final String DEVICE_EFFECT_DIM_WALLPAPER = "zdeDimWallpaper";
+ private static final String DEVICE_EFFECT_USE_NIGHT_MODE = "zdeUseNightMode";
+ private static final String DEVICE_EFFECT_DISABLE_AUTO_BRIGHTNESS = "zdeDisableAutoBrightness";
+ private static final String DEVICE_EFFECT_DISABLE_TAP_TO_WAKE = "zdeDisableTapToWake";
+ private static final String DEVICE_EFFECT_DISABLE_TILT_TO_WAKE = "zdeDisableTiltToWake";
+ private static final String DEVICE_EFFECT_DISABLE_TOUCH = "zdeDisableTouch";
+ private static final String DEVICE_EFFECT_MINIMIZE_RADIO_USAGE = "zdeMinimizeRadioUsage";
+ private static final String DEVICE_EFFECT_MAXIMIZE_DOZE = "zdeMaximizeDoze";
+
@UnsupportedAppUsage
public boolean allowAlarms = DEFAULT_ALLOW_ALARMS;
public boolean allowMedia = DEFAULT_ALLOW_MEDIA;
@@ -630,6 +643,7 @@
rt.modified = safeBoolean(parser, RULE_ATT_MODIFIED, false);
rt.zenPolicy = readZenPolicyXml(parser);
if (Flags.modesApi()) {
+ rt.zenDeviceEffects = readZenDeviceEffectsXml(parser);
rt.allowManualInvocation = safeBoolean(parser, RULE_ATT_ALLOW_MANUAL, false);
rt.iconResId = safeInt(parser, RULE_ATT_ICON, 0);
rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
@@ -667,6 +681,9 @@
if (rule.zenPolicy != null) {
writeZenPolicyXml(rule.zenPolicy, out);
}
+ if (Flags.modesApi() && rule.zenDeviceEffects != null) {
+ writeZenDeviceEffectsXml(rule.zenDeviceEffects, out);
+ }
out.attributeBoolean(null, RULE_ATT_MODIFIED, rule.modified);
if (Flags.modesApi()) {
out.attributeBoolean(null, RULE_ATT_ALLOW_MANUAL, rule.allowManualInvocation);
@@ -859,6 +876,57 @@
}
}
+ @Nullable
+ private static ZenDeviceEffects readZenDeviceEffectsXml(TypedXmlPullParser parser) {
+ ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder()
+ .setShouldDisplayGrayscale(
+ safeBoolean(parser, DEVICE_EFFECT_DISPLAY_GRAYSCALE, false))
+ .setShouldSuppressAmbientDisplay(
+ safeBoolean(parser, DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY, false))
+ .setShouldDimWallpaper(safeBoolean(parser, DEVICE_EFFECT_DIM_WALLPAPER, false))
+ .setShouldUseNightMode(safeBoolean(parser, DEVICE_EFFECT_USE_NIGHT_MODE, false))
+ .setShouldDisableAutoBrightness(
+ safeBoolean(parser, DEVICE_EFFECT_DISABLE_AUTO_BRIGHTNESS, false))
+ .setShouldDisableTapToWake(
+ safeBoolean(parser, DEVICE_EFFECT_DISABLE_TAP_TO_WAKE, false))
+ .setShouldDisableTiltToWake(
+ safeBoolean(parser, DEVICE_EFFECT_DISABLE_TILT_TO_WAKE, false))
+ .setShouldDisableTouch(safeBoolean(parser, DEVICE_EFFECT_DISABLE_TOUCH, false))
+ .setShouldMinimizeRadioUsage(
+ safeBoolean(parser, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, false))
+ .setShouldMaximizeDoze(safeBoolean(parser, DEVICE_EFFECT_MAXIMIZE_DOZE, false))
+ .build();
+
+ return deviceEffects.hasEffects() ? deviceEffects : null;
+ }
+
+ private static void writeZenDeviceEffectsXml(ZenDeviceEffects deviceEffects,
+ TypedXmlSerializer out) throws IOException {
+ writeBooleanIfTrue(out, DEVICE_EFFECT_DISPLAY_GRAYSCALE,
+ deviceEffects.shouldDisplayGrayscale());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY,
+ deviceEffects.shouldSuppressAmbientDisplay());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_DIM_WALLPAPER, deviceEffects.shouldDimWallpaper());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_USE_NIGHT_MODE, deviceEffects.shouldUseNightMode());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_DISABLE_AUTO_BRIGHTNESS,
+ deviceEffects.shouldDisableAutoBrightness());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_DISABLE_TAP_TO_WAKE,
+ deviceEffects.shouldDisableTapToWake());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_DISABLE_TILT_TO_WAKE,
+ deviceEffects.shouldDisableTiltToWake());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_DISABLE_TOUCH, deviceEffects.shouldDisableTouch());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE,
+ deviceEffects.shouldMinimizeRadioUsage());
+ writeBooleanIfTrue(out, DEVICE_EFFECT_MAXIMIZE_DOZE, deviceEffects.shouldMaximizeDoze());
+ }
+
+ private static void writeBooleanIfTrue(TypedXmlSerializer out, String att, boolean value)
+ throws IOException {
+ if (value) {
+ out.attributeBoolean(null, att, true);
+ }
+ }
+
public static boolean isValidHour(int val) {
return val >= 0 && val < 24;
}
@@ -1755,6 +1823,8 @@
// package name, only used for manual rules when they have turned DND on.
public String enabler;
public ZenPolicy zenPolicy;
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ @Nullable public ZenDeviceEffects zenDeviceEffects;
public boolean modified; // rule has been modified from initial creation
public String pkg;
public int type = AutomaticZenRule.TYPE_UNKNOWN;
@@ -1784,6 +1854,9 @@
enabler = source.readString();
}
zenPolicy = source.readParcelable(null, android.service.notification.ZenPolicy.class);
+ if (Flags.modesApi()) {
+ zenDeviceEffects = source.readParcelable(null, ZenDeviceEffects.class);
+ }
modified = source.readInt() == 1;
pkg = source.readString();
if (Flags.modesApi()) {
@@ -1828,6 +1901,9 @@
dest.writeInt(0);
}
dest.writeParcelable(zenPolicy, 0);
+ if (Flags.modesApi()) {
+ dest.writeParcelable(zenDeviceEffects, 0);
+ }
dest.writeInt(modified ? 1 : 0);
dest.writeString(pkg);
if (Flags.modesApi()) {
@@ -1859,7 +1935,8 @@
.append(",condition=").append(condition);
if (Flags.modesApi()) {
- sb.append(",allowManualInvocation=").append(allowManualInvocation)
+ sb.append(",deviceEffects=").append(zenDeviceEffects)
+ .append(",allowManualInvocation=").append(allowManualInvocation)
.append(",iconResId=").append(iconResId)
.append(",triggerDescription=").append(triggerDescription)
.append(",type=").append(type);
@@ -1917,6 +1994,7 @@
if (Flags.modesApi()) {
return finalEquals
+ && Objects.equals(other.zenDeviceEffects, zenDeviceEffects)
&& other.allowManualInvocation == allowManualInvocation
&& other.iconResId == iconResId
&& Objects.equals(other.triggerDescription, triggerDescription)
@@ -1930,8 +2008,9 @@
public int hashCode() {
if (Flags.modesApi()) {
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
- component, configurationActivity, pkg, id, enabler, zenPolicy, modified,
- allowManualInvocation, iconResId, triggerDescription, type);
+ component, configurationActivity, pkg, id, enabler, zenPolicy,
+ zenDeviceEffects, modified, allowManualInvocation, iconResId,
+ triggerDescription, type);
}
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index eb55e40..f345d7c 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -452,11 +452,12 @@
public static final String FIELD_CREATION_TIME = "creationTime";
public static final String FIELD_ENABLER = "enabler";
public static final String FIELD_ZEN_POLICY = "zenPolicy";
+ public static final String FIELD_ZEN_DEVICE_EFFECTS = "zenDeviceEffects";
public static final String FIELD_MODIFIED = "modified";
public static final String FIELD_PKG = "pkg";
public static final String FIELD_ALLOW_MANUAL = "allowManualInvocation";
public static final String FIELD_ICON_RES = "iconResId";
- public static final String FIELD_TRIGGER = "triggerDescription";
+ public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription";
public static final String FIELD_TYPE = "type";
// NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule
@@ -534,19 +535,25 @@
if (!Objects.equals(from.pkg, to.pkg)) {
addField(FIELD_PKG, new FieldDiff<>(from.pkg, to.pkg));
}
- if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
- addField(FIELD_TRIGGER,
- new FieldDiff<>(from.triggerDescription, to.triggerDescription));
- }
- if (from.type != to.type) {
- addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
- }
- if (from.allowManualInvocation != to.allowManualInvocation) {
- addField(FIELD_ALLOW_MANUAL,
- new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
- }
- if (!Objects.equals(from.iconResId, to.iconResId)) {
- addField(FIELD_ICON_RES, new FieldDiff(from.iconResId, to.iconResId));
+ if (android.app.Flags.modesApi()) {
+ if (!Objects.equals(from.zenDeviceEffects, to.zenDeviceEffects)) {
+ addField(FIELD_ZEN_DEVICE_EFFECTS,
+ new FieldDiff<>(from.zenDeviceEffects, to.zenDeviceEffects));
+ }
+ if (!Objects.equals(from.triggerDescription, to.triggerDescription)) {
+ addField(FIELD_TRIGGER_DESCRIPTION,
+ new FieldDiff<>(from.triggerDescription, to.triggerDescription));
+ }
+ if (from.type != to.type) {
+ addField(FIELD_TYPE, new FieldDiff<>(from.type, to.type));
+ }
+ if (from.allowManualInvocation != to.allowManualInvocation) {
+ addField(FIELD_ALLOW_MANUAL,
+ new FieldDiff<>(from.allowManualInvocation, to.allowManualInvocation));
+ }
+ if (!Objects.equals(from.iconResId, to.iconResId)) {
+ addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResId, to.iconResId));
+ }
}
}
diff --git a/core/java/android/service/persistentdata/PersistentDataBlockManager.java b/core/java/android/service/persistentdata/PersistentDataBlockManager.java
index 9167153..6da3206 100644
--- a/core/java/android/service/persistentdata/PersistentDataBlockManager.java
+++ b/core/java/android/service/persistentdata/PersistentDataBlockManager.java
@@ -66,6 +66,7 @@
*/
public static final int FLASH_LOCK_LOCKED = 1;
+ /** @removed mistakenly exposed previously */
@IntDef(prefix = { "FLASH_LOCK_" }, value = {
FLASH_LOCK_UNKNOWN,
FLASH_LOCK_LOCKED,
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 1acc384..cabab6c 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -49,8 +49,9 @@
/** The insets source ID of IME */
public static final int ID_IME = createId(null, 0, ime());
+
/** The insets source ID of the IME caption bar ("fake" IME navigation bar). */
- static final int ID_IME_CAPTION_BAR =
+ public static final int ID_IME_CAPTION_BAR =
InsetsSource.createId(null /* owner */, 1 /* index */, captionBar());
/**
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index d5b4e41..7b45600 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -3991,9 +3991,7 @@
// when the values are applicable.
setPreferredFrameRate(mPreferredFrameRate);
setPreferredFrameRateCategory(mPreferredFrameRateCategory);
- mLastPreferredFrameRateCategory = mPreferredFrameRateCategory;
mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
- mLastPreferredFrameRate = mPreferredFrameRate;
mPreferredFrameRate = 0;
}
@@ -11982,8 +11980,11 @@
? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory;
try {
- mFrameRateTransaction.setFrameRateCategory(mSurfaceControl,
- frameRateCategory, false).apply();
+ if (mLastPreferredFrameRateCategory != frameRateCategory) {
+ mFrameRateTransaction.setFrameRateCategory(mSurfaceControl,
+ frameRateCategory, false).applyAsyncUnsafe();
+ mLastPreferredFrameRateCategory = frameRateCategory;
+ }
} catch (Exception e) {
Log.e(mTag, "Unable to set frame rate category", e);
}
@@ -12003,8 +12004,11 @@
}
try {
- mFrameRateTransaction.setFrameRate(mSurfaceControl,
- preferredFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT).apply();
+ if (mLastPreferredFrameRate != preferredFrameRate) {
+ mFrameRateTransaction.setFrameRate(mSurfaceControl, preferredFrameRate,
+ Surface.FRAME_RATE_COMPATIBILITY_DEFAULT).applyAsyncUnsafe();
+ mLastPreferredFrameRate = preferredFrameRate;
+ }
} catch (Exception e) {
Log.e(mTag, "Unable to set frame rate", e);
}
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 92ce9f7..046ea77 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3301,9 +3301,8 @@
/**
* An internal annotation for flags that can be specified to {@link #softInputMode}.
*
- * @hide
+ * @removed mistakenly exposed as system-api previously
*/
- @SystemApi
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = { "SYSTEM_FLAG_" }, value = {
SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS,
diff --git a/core/java/com/android/internal/app/procstats/ProcessState.java b/core/java/com/android/internal/app/procstats/ProcessState.java
index 755113b..0dbdb36 100644
--- a/core/java/com/android/internal/app/procstats/ProcessState.java
+++ b/core/java/com/android/internal/app/procstats/ProcessState.java
@@ -690,18 +690,6 @@
}
}
- public void reportCachedKill(ArrayMap<String, ProcessStateHolder> pkgList, long pss) {
- ensureNotDead();
- mCommonProcess.addCachedKill(1, pss, pss, pss);
- if (!mCommonProcess.mMultiPackage) {
- return;
- }
-
- for (int ip=pkgList.size()-1; ip>=0; ip--) {
- pullFixedProc(pkgList, ip).addCachedKill(1, pss, pss, pss);
- }
- }
-
public ProcessState pullFixedProc(String pkgName) {
if (mMultiPackage) {
// The array map is still pointing to a common process state
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 18c60a7..91dfc60 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -1245,7 +1245,7 @@
void android_os_Process_removeAllProcessGroups(JNIEnv* env, jobject clazz)
{
- return removeAllProcessGroups();
+ return removeAllEmptyProcessGroups();
}
static jint android_os_Process_nativePidFdOpen(JNIEnv* env, jobject, jint pid, jint flags) {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 926e0fa..7b075e6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -822,6 +822,7 @@
<protected-broadcast android:name="android.intent.action.PROFILE_REMOVED" />
<protected-broadcast android:name="com.android.internal.telephony.cat.SMS_SENT_ACTION" />
<protected-broadcast android:name="com.android.internal.telephony.cat.SMS_DELIVERY_ACTION" />
+ <protected-broadcast android:name="com.android.internal.telephony.data.ACTION_RETRY" />
<protected-broadcast android:name="android.companion.virtual.action.VIRTUAL_DEVICE_REMOVED" />
<protected-broadcast android:name="com.android.internal.intent.action.FLASH_NOTIFICATION_START_PREVIEW" />
<protected-broadcast android:name="com.android.internal.intent.action.FLASH_NOTIFICATION_STOP_PREVIEW" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index f827d28..68bad45 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -6806,6 +6806,10 @@
<!-- Whether or not ActivityManager PSS profiling is disabled. -->
<bool name="config_am_disablePssProfiling">false</bool>
+ <!-- The modifier used to adjust AM's PSS threshold for downgrading services to service B if
+ RSS is being collected instead. -->
+ <item name="config_am_pssToRssThresholdModifier" format="float" type="dimen">1.5</item>
+
<!-- Whether unlocking and waking a device are sequenced -->
<bool name="config_orderUnlockAndWake">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f3aa936..24b39bc 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5270,6 +5270,7 @@
<!-- For ActivityManager PSS profiling configurability -->
<java-symbol type="bool" name="config_am_disablePssProfiling" />
+ <java-symbol type="dimen" name="config_am_pssToRssThresholdModifier" />
<java-symbol type="raw" name="default_ringtone_vibration_effect" />
diff --git a/core/tests/coretests/src/android/view/InsetsSourceTest.java b/core/tests/coretests/src/android/view/InsetsSourceTest.java
index 9595332..e1bcd4a 100644
--- a/core/tests/coretests/src/android/view/InsetsSourceTest.java
+++ b/core/tests/coretests/src/android/view/InsetsSourceTest.java
@@ -16,6 +16,7 @@
package android.view;
+import static android.view.InsetsSource.ID_IME_CAPTION_BAR;
import static android.view.WindowInsets.Type.FIRST;
import static android.view.WindowInsets.Type.LAST;
import static android.view.WindowInsets.Type.SIZE;
@@ -52,12 +53,15 @@
private final InsetsSource mSource = new InsetsSource(0 /* id */, navigationBars());
private final InsetsSource mImeSource = new InsetsSource(1 /* id */, ime());
+ private final InsetsSource mImeCaptionSource = new InsetsSource(
+ ID_IME_CAPTION_BAR, captionBar());
private final InsetsSource mCaptionSource = new InsetsSource(2 /* id */, captionBar());
@Before
public void setUp() {
mSource.setVisible(true);
mImeSource.setVisible(true);
+ mImeCaptionSource.setVisible(true);
mCaptionSource.setVisible(true);
}
@@ -110,6 +114,18 @@
}
@Test
+ public void testCalculateInsets_imeCaptionBar() {
+ mImeCaptionSource.setFrame(new Rect(0, 400, 500, 500));
+ Insets insets = mImeCaptionSource.calculateInsets(new Rect(0, 0, 500, 500), false);
+ assertEquals(Insets.of(0, 0, 0, 100), insets);
+
+ // Place caption bar at top; IME caption bar must always return bottom insets
+ mImeCaptionSource.setFrame(new Rect(0, 0, 500, 100));
+ insets = mImeCaptionSource.calculateInsets(new Rect(0, 0, 500, 500), false);
+ assertEquals(Insets.of(0, 0, 0, 100), insets);
+ }
+
+ @Test
public void testCalculateInsets_caption_resizing() {
mCaptionSource.setFrame(new Rect(0, 0, 100, 100));
Insets insets = mCaptionSource.calculateInsets(new Rect(0, 0, 200, 200), false);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
index 4d73c20..ca3d8d1 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java
@@ -375,7 +375,8 @@
return TaskFragmentAnimationParams.DEFAULT;
}
return new TaskFragmentAnimationParams.Builder()
- .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
+ // TODO(b/263047900): Update extensions API.
+ // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
.build();
}
}
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index faf7c39..b5c32bb 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -854,7 +854,8 @@
return new SplitAttributes.Builder()
.setSplitType(splitTypeToUpdate)
.setLayoutDirection(splitAttributes.getLayoutDirection())
- .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
+ // TODO(b/263047900): Update extensions API.
+ // .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor())
.build();
}
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
index 9607b78..60beb0b 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java
@@ -17,6 +17,7 @@
package androidx.window.extensions;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
import static com.google.common.truth.Truth.assertThat;
import android.app.ActivityTaskManager;
@@ -69,6 +70,7 @@
.isEqualTo(SplitAttributes.LayoutDirection.LOCALE);
assertThat(splitAttributes.getSplitType())
.isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f));
- assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0);
+ // TODO(b/263047900): Update extensions API.
+ // assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0);
}
}
diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml
index 6a6f2b0..e4abae4 100644
--- a/libs/WindowManager/Shell/res/values/config.xml
+++ b/libs/WindowManager/Shell/res/values/config.xml
@@ -141,4 +141,7 @@
window. If false, the splash screen will be a solid color splash screen whenever the
app has not provided a windowSplashScreenAnimatedIcon. -->
<bool name="config_canUseAppIconForSplashScreen">true</bool>
+
+ <!-- Whether CompatUIController is enabled -->
+ <bool name="config_enableCompatUIController">true</bool>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/config_tv.xml b/libs/WindowManager/Shell/res/values/config_tv.xml
new file mode 100644
index 0000000..3da5539
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values/config_tv.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<resources>
+ <integer name="config_tvPipEnterFadeOutDuration">500</integer>
+ <integer name="config_tvPipEnterFadeInDuration">1500</integer>
+ <integer name="config_tvPipExitFadeOutDuration">500</integer>
+ <integer name="config_tvPipExitFadeInDuration">500</integer>
+</resources>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
index d520ff7..8b6c7b6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipBoundsState.java
@@ -258,7 +258,7 @@
ActivityTaskManager.getService().onPictureInPictureStateChanged(
new PictureInPictureUiState(stashedState != STASH_TYPE_NONE /* isStashed */)
);
- } catch (RemoteException e) {
+ } catch (RemoteException | IllegalStateException e) {
ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: Unable to set alert PiP state change.", TAG);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 54cf84c..27dc870 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -221,34 +221,57 @@
Context context,
ShellInit shellInit,
ShellCommandHandler shellCommandHandler,
- CompatUIController compatUI,
+ Optional<CompatUIController> compatUI,
Optional<UnfoldAnimationController> unfoldAnimationController,
Optional<RecentTasksController> recentTasksOptional,
- @ShellMainThread ShellExecutor mainExecutor
- ) {
+ @ShellMainThread ShellExecutor mainExecutor) {
if (!context.getResources().getBoolean(R.bool.config_registerShellTaskOrganizerOnInit)) {
// TODO(b/238217847): Force override shell init if registration is disabled
shellInit = new ShellInit(mainExecutor);
}
- return new ShellTaskOrganizer(shellInit, shellCommandHandler, compatUI,
- unfoldAnimationController, recentTasksOptional, mainExecutor);
+ return new ShellTaskOrganizer(
+ shellInit,
+ shellCommandHandler,
+ compatUI.orElse(null),
+ unfoldAnimationController,
+ recentTasksOptional,
+ mainExecutor);
}
@WMSingleton
@Provides
- static CompatUIController provideCompatUIController(Context context,
+ static Optional<CompatUIController> provideCompatUIController(
+ Context context,
ShellInit shellInit,
ShellController shellController,
- DisplayController displayController, DisplayInsetsController displayInsetsController,
- DisplayImeController imeController, SyncTransactionQueue syncQueue,
- @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy,
- DockStateReader dockStateReader, CompatUIConfiguration compatUIConfiguration,
+ DisplayController displayController,
+ DisplayInsetsController displayInsetsController,
+ DisplayImeController imeController,
+ SyncTransactionQueue syncQueue,
+ @ShellMainThread ShellExecutor mainExecutor,
+ Lazy<Transitions> transitionsLazy,
+ DockStateReader dockStateReader,
+ CompatUIConfiguration compatUIConfiguration,
CompatUIShellCommandHandler compatUIShellCommandHandler,
AccessibilityManager accessibilityManager) {
- return new CompatUIController(context, shellInit, shellController, displayController,
- displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy,
- dockStateReader, compatUIConfiguration, compatUIShellCommandHandler,
- accessibilityManager);
+ if (!context.getResources().getBoolean(R.bool.config_enableCompatUIController)) {
+ return Optional.empty();
+ }
+ return Optional.of(
+ new CompatUIController(
+ context,
+ shellInit,
+ shellController,
+ displayController,
+ displayInsetsController,
+ imeController,
+ syncQueue,
+ mainExecutor,
+ transitionsLazy,
+ dockStateReader,
+ compatUIConfiguration,
+ compatUIShellCommandHandler,
+ accessibilityManager));
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
index a9675f9..1947097 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
@@ -20,6 +20,8 @@
import android.os.Handler;
import android.os.SystemClock;
+import androidx.annotation.NonNull;
+
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.common.DisplayController;
@@ -41,7 +43,6 @@
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip.PipTaskOrganizer;
-import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm;
import com.android.wm.shell.pip.tv.TvPipBoundsController;
@@ -78,11 +79,12 @@
PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
+ PipTransitionState pipTransitionState,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
TvPipMenuController tvPipMenuController,
PipMediaController pipMediaController,
- PipTransitionController pipTransitionController,
+ TvPipTransition tvPipTransition,
TvPipNotificationController tvPipNotificationController,
TaskStackListenerImpl taskStackListener,
PipParamsChangedForwarder pipParamsChangedForwarder,
@@ -99,9 +101,10 @@
pipDisplayLayoutState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
+ pipTransitionState,
pipAppOpsListener,
pipTaskOrganizer,
- pipTransitionController,
+ tvPipTransition,
tvPipMenuController,
pipMediaController,
tvPipNotificationController,
@@ -151,25 +154,23 @@
return new LegacySizeSpecSource(context, pipDisplayLayoutState);
}
- // Handler needed for loadDrawableAsync() in PipControlsViewController
@WMSingleton
@Provides
- static PipTransitionController provideTvPipTransition(
+ static TvPipTransition provideTvPipTransition(
Context context,
- ShellInit shellInit,
- ShellTaskOrganizer shellTaskOrganizer,
- Transitions transitions,
+ @NonNull ShellInit shellInit,
+ @NonNull ShellTaskOrganizer shellTaskOrganizer,
+ @NonNull Transitions transitions,
TvPipBoundsState tvPipBoundsState,
- PipDisplayLayoutState pipDisplayLayoutState,
- PipTransitionState pipTransitionState,
- TvPipMenuController pipMenuController,
+ TvPipMenuController tvPipMenuController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+ PipTransitionState pipTransitionState,
PipAnimationController pipAnimationController,
- PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
+ PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
+ PipDisplayLayoutState pipDisplayLayoutState) {
return new TvPipTransition(context, shellInit, shellTaskOrganizer, transitions,
- tvPipBoundsState, pipDisplayLayoutState, pipTransitionState, pipMenuController,
- tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
- Optional.empty());
+ tvPipBoundsState, tvPipMenuController, tvPipBoundsAlgorithm, pipTransitionState,
+ pipAnimationController, pipSurfaceTransactionHelper, pipDisplayLayoutState);
}
@WMSingleton
@@ -207,7 +208,7 @@
PipTransitionState pipTransitionState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
PipAnimationController pipAnimationController,
- PipTransitionController pipTransitionController,
+ TvPipTransition tvPipTransition,
PipParamsChangedForwarder pipParamsChangedForwarder,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
Optional<SplitScreenController> splitScreenControllerOptional,
@@ -217,7 +218,7 @@
return new TvPipTaskOrganizer(context,
syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipDisplayLayoutState,
tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController,
- pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+ pipSurfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
splitScreenControllerOptional, displayController, pipUiEventLogger,
shellTaskOrganizer, mainExecutor);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index cbed4b5..a58d94e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -81,15 +81,35 @@
*/
public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
Rect sourceBounds, Rect destinationBounds) {
+ mTmpDestinationRectF.set(destinationBounds);
+ return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */);
+ }
+
+ /**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, RectF destinationBounds) {
return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */);
}
/**
+ * Operates the scale (setMatrix) on a given transaction and leash
+ * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+ */
+ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
+ Rect sourceBounds, Rect destinationBounds, float degrees) {
+ mTmpDestinationRectF.set(destinationBounds);
+ return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees);
+ }
+
+ /**
* Operates the scale (setMatrix) on a given transaction and leash, along with a rotation.
* @return same {@link PipSurfaceTransactionHelper} instance for method chaining
*/
public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash,
- Rect sourceBounds, Rect destinationBounds, float degrees) {
+ Rect sourceBounds, RectF destinationBounds, float degrees) {
mTmpSourceRectF.set(sourceBounds);
// We want the matrix to position the surface relative to the screen coordinates so offset
// the source to 0,0
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index c05601b..b4067d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -297,9 +297,9 @@
// changed RunningTaskInfo when it finishes.
private ActivityManager.RunningTaskInfo mDeferredTaskInfo;
private WindowContainerToken mToken;
- private SurfaceControl mLeash;
+ protected SurfaceControl mLeash;
protected PipTransitionState mPipTransitionState;
- private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
+ protected PipSurfaceTransactionHelper.SurfaceControlTransactionFactory
mSurfaceControlTransactionFactory;
protected PictureInPictureParams mPictureInPictureParams;
private IntConsumer mOnDisplayIdChangeCallback;
@@ -973,7 +973,7 @@
return;
}
- cancelCurrentAnimator();
+ cancelAnimationOnTaskVanished();
onExitPipFinished(info);
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
@@ -981,6 +981,10 @@
}
}
+ protected void cancelAnimationOnTaskVanished() {
+ cancelCurrentAnimator();
+ }
+
@Override
public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) {
Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken");
@@ -1100,7 +1104,7 @@
}
/** Called when exiting PIP transition is finished to do the state cleanup. */
- void onExitPipFinished(TaskInfo info) {
+ public void onExitPipFinished(TaskInfo info) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"onExitPipFinished: %s, state=%s leash=%s",
info.topActivity, mPipTransitionState, mLeash);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
index a48e969f..72c0cd7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java
@@ -44,6 +44,7 @@
import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import java.util.Collections;
import java.util.Set;
/**
@@ -101,12 +102,29 @@
&& mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
&& !mTvPipBoundsState.isTvPipManuallyCollapsed();
if (isPipExpanded) {
- updateGravityOnExpansionToggled(/* expanding= */ true);
+ updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded);
}
mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
return adjustBoundsForTemporaryDecor(getTvPipPlacement().getBounds());
}
+ @Override
+ public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: getEntryDestinationBoundsIgnoringKeepClearAreas()", TAG);
+
+ updateExpandedPipSize();
+ final boolean isPipExpanded = mTvPipBoundsState.isTvExpandedPipSupported()
+ && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0
+ && !mTvPipBoundsState.isTvPipManuallyCollapsed();
+ if (isPipExpanded) {
+ updateGravityOnExpansionToggled(/* expanding= */ isPipExpanded);
+ }
+ mTvPipBoundsState.setTvPipExpanded(isPipExpanded);
+ return adjustBoundsForTemporaryDecor(getTvPipPlacement(Collections.emptySet(),
+ Collections.emptySet()).getUnstashedBounds());
+ }
+
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
@Override
public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
@@ -133,16 +151,25 @@
*/
@NonNull
public Placement getTvPipPlacement() {
+ final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
+ final Set<Rect> unrestrictedKeepClearAreas =
+ mTvPipBoundsState.getUnrestrictedKeepClearAreas();
+
+ return getTvPipPlacement(restrictedKeepClearAreas, unrestrictedKeepClearAreas);
+ }
+
+ /**
+ * Calculates the PiP bounds.
+ */
+ @NonNull
+ private Placement getTvPipPlacement(Set<Rect> restrictedKeepClearAreas,
+ Set<Rect> unrestrictedKeepClearAreas) {
final Size pipSize = getPipSize();
final Rect displayBounds = mTvPipBoundsState.getDisplayBounds();
final Size screenSize = new Size(displayBounds.width(), displayBounds.height());
final Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
- final Set<Rect> restrictedKeepClearAreas = mTvPipBoundsState.getRestrictedKeepClearAreas();
- final Set<Rect> unrestrictedKeepClearAreas =
- mTvPipBoundsState.getUnrestrictedKeepClearAreas();
-
mKeepClearAlgorithm.setGravity(mTvPipBoundsState.getTvPipGravity());
mKeepClearAlgorithm.setScreenSize(screenSize);
mKeepClearAlgorithm.setMovementBounds(insetBounds);
@@ -189,8 +216,11 @@
int updatedGravity;
if (expanding) {
- // Save collapsed gravity.
- mTvPipBoundsState.setTvPipPreviousCollapsedGravity(mTvPipBoundsState.getTvPipGravity());
+ if (!mTvPipBoundsState.isTvPipExpanded()) {
+ // Save collapsed gravity.
+ mTvPipBoundsState.setTvPipPreviousCollapsedGravity(
+ mTvPipBoundsState.getTvPipGravity());
+ }
if (mTvPipBoundsState.getTvFixedPipOrientation() == ORIENTATION_HORIZONTAL) {
updatedGravity = Gravity.CENTER_HORIZONTAL | currentY;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
index 2b3a93e..5ee3734e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsState.java
@@ -131,6 +131,7 @@
mTvFixedPipOrientation = ORIENTATION_UNDETERMINED;
mTvPipGravity = mDefaultGravity;
mPreviousCollapsedGravity = mDefaultGravity;
+ mIsTvPipExpanded = false;
mTvPipManuallyCollapsed = false;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
index 72115fd..cd3d38b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java
@@ -56,6 +56,7 @@
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipTaskOrganizer;
import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
import com.android.wm.shell.sysui.ShellController;
@@ -122,6 +123,7 @@
private final PipDisplayLayoutState mPipDisplayLayoutState;
private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
private final TvPipBoundsController mTvPipBoundsController;
+ private final PipTransitionState mPipTransitionState;
private final PipAppOpsListener mAppOpsListener;
private final PipTaskOrganizer mPipTaskOrganizer;
private final PipMediaController mPipMediaController;
@@ -157,6 +159,7 @@
PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
+ PipTransitionState pipTransitionState,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
@@ -177,6 +180,7 @@
pipDisplayLayoutState,
tvPipBoundsAlgorithm,
tvPipBoundsController,
+ pipTransitionState,
pipAppOpsListener,
pipTaskOrganizer,
pipTransitionController,
@@ -199,6 +203,7 @@
PipDisplayLayoutState pipDisplayLayoutState,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
TvPipBoundsController tvPipBoundsController,
+ PipTransitionState pipTransitionState,
PipAppOpsListener pipAppOpsListener,
PipTaskOrganizer pipTaskOrganizer,
PipTransitionController pipTransitionController,
@@ -212,6 +217,7 @@
Handler mainHandler,
ShellExecutor mainExecutor) {
mContext = context;
+ mPipTransitionState = pipTransitionState;
mMainHandler = mainHandler;
mMainExecutor = mainExecutor;
mShellController = shellController;
@@ -365,7 +371,6 @@
"%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState));
mPipTaskOrganizer.exitPip(mResizeAnimationDuration, false /* requestEnterSplit */);
- onPipDisappeared();
}
private void togglePipExpansion() {
@@ -420,6 +425,11 @@
@Override
public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) {
+ if (!mPipTransitionState.hasEnteredPip()) {
+ // Do not schedule a move animation while we're still transitioning into/out of PiP
+ return;
+ }
+
mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds,
animationDuration, null);
mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds);
@@ -447,7 +457,7 @@
return;
}
mPipTaskOrganizer.removePip();
- onPipDisappeared();
+ mTvPipMenuController.closeMenu();
}
@Override
@@ -477,7 +487,7 @@
mPipNotificationController.dismiss();
mActionBroadcastReceiver.unregister();
- mTvPipMenuController.closeMenu();
+ mTvPipMenuController.detach();
mTvPipActionsProvider.reset();
mTvPipBoundsState.resetTvPipState();
mTvPipBoundsController.reset();
@@ -501,8 +511,6 @@
public void onPipTransitionCanceled(int direction) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
- mTvPipMenuController.onPipTransitionFinished(
- PipAnimationController.isInPipDirection(direction));
mTvPipActionsProvider.updatePipExpansionState(mTvPipBoundsState.isTvPipExpanded());
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
index ee55211..c6803f7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java
@@ -262,8 +262,8 @@
@Override
public void detach() {
- closeMenu();
detachPipMenu();
+ switchToMenuMode(MODE_NO_MENU);
mLeash = null;
}
@@ -320,10 +320,21 @@
@Override
public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction pipTx,
Rect pipBounds, float alpha) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
- "%s: movePipMenu: %s, alpha %s", TAG, pipBounds.toShortString(), alpha);
+ movePipMenu(pipTx, pipBounds, alpha);
+ }
- if (pipBounds.isEmpty()) {
+ /**
+ * Move the PiP menu with the given bounds and update its opacity.
+ * The PiP SurfaceControl is given if there is a need to synchronize the movements
+ * on the same frame as PiP.
+ */
+ public void movePipMenu(@Nullable SurfaceControl.Transaction pipTx, @Nullable Rect pipBounds,
+ float alpha) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: movePipMenu: %s, alpha %s", TAG,
+ pipBounds != null ? pipBounds.toShortString() : null, alpha);
+
+ if ((pipBounds == null || pipBounds.isEmpty()) && alpha == ALPHA_NO_CHANGE) {
if (pipTx == null) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: no transaction given", TAG);
@@ -334,28 +345,36 @@
return;
}
- final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
- final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
- final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds);
if (pipTx == null) {
pipTx = new SurfaceControl.Transaction();
}
- pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top);
- pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top);
+
+ final SurfaceControl frontSurface = getSurfaceControl(mPipMenuView);
+ final SurfaceControl backSurface = getSurfaceControl(mPipBackgroundView);
+
+ if (pipBounds != null) {
+ final Rect menuDestBounds = calculateMenuSurfaceBounds(pipBounds);
+ pipTx.setPosition(frontSurface, menuDestBounds.left, menuDestBounds.top);
+ pipTx.setPosition(backSurface, menuDestBounds.left, menuDestBounds.top);
+ updateMenuBounds(pipBounds);
+ }
if (alpha != ALPHA_NO_CHANGE) {
pipTx.setAlpha(frontSurface, alpha);
pipTx.setAlpha(backSurface, alpha);
}
- // Synchronize drawing the content in the front and back surfaces together with the pip
- // transaction and the position change for the front and back surfaces
- final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip");
- syncGroup.add(mPipMenuView.getRootSurfaceControl(), null);
- syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null);
- updateMenuBounds(pipBounds);
- syncGroup.addTransaction(pipTx);
- syncGroup.markSyncReady();
+ if (pipBounds != null) {
+ // Synchronize drawing the content in the front and back surfaces together with the pip
+ // transaction and the position change for the front and back surfaces
+ final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup("TvPip");
+ syncGroup.add(mPipMenuView.getRootSurfaceControl(), null);
+ syncGroup.add(mPipBackgroundView.getRootSurfaceControl(), null);
+ syncGroup.addTransaction(pipTx);
+ syncGroup.markSyncReady();
+ } else {
+ pipTx.apply();
+ }
}
private boolean isMenuAttached() {
@@ -388,14 +407,19 @@
final Rect menuBounds = calculateMenuSurfaceBounds(pipBounds);
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
"%s: updateMenuBounds: %s", TAG, menuBounds.toShortString());
- mSystemWindows.updateViewLayout(mPipBackgroundView,
- getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
- menuBounds.height()));
- mSystemWindows.updateViewLayout(mPipMenuView,
- getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
- menuBounds.height()));
- if (mPipMenuView != null) {
- mPipMenuView.setPipBounds(pipBounds);
+
+ boolean needsRelayout = mPipBackgroundView.getLayoutParams().width != menuBounds.width()
+ || mPipBackgroundView.getLayoutParams().height != menuBounds.height();
+ if (needsRelayout) {
+ mSystemWindows.updateViewLayout(mPipBackgroundView,
+ getPipMenuLayoutParams(mContext, BACKGROUND_WINDOW_TITLE, menuBounds.width(),
+ menuBounds.height()));
+ mSystemWindows.updateViewLayout(mPipMenuView,
+ getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, menuBounds.width(),
+ menuBounds.height()));
+ if (mPipMenuView != null) {
+ mPipMenuView.setPipBounds(pipBounds);
+ }
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
index f86f987..202d36f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java
@@ -168,6 +168,9 @@
* that the edu text will be marqueed
*/
private boolean isEduTextMarqueed() {
+ if (mEduTextView.getLayout() == null) {
+ return false;
+ }
final int availableWidth = (int) mEduTextView.getWidth()
- mEduTextView.getCompoundPaddingLeft()
- mEduTextView.getCompoundPaddingRight();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index f315afb..21223c9a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -35,7 +35,6 @@
import com.android.wm.shell.pip.PipParamsChangedForwarder;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
import com.android.wm.shell.pip.PipTaskOrganizer;
-import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
import com.android.wm.shell.splitscreen.SplitScreenController;
@@ -46,6 +45,7 @@
* TV specific changes to the PipTaskOrganizer.
*/
public class TvPipTaskOrganizer extends PipTaskOrganizer {
+ private final TvPipTransition mTvPipTransition;
public TvPipTaskOrganizer(Context context,
@NonNull SyncTransactionQueue syncTransactionQueue,
@@ -56,7 +56,7 @@
@NonNull PipMenuController pipMenuController,
@NonNull PipAnimationController pipAnimationController,
@NonNull PipSurfaceTransactionHelper surfaceTransactionHelper,
- @NonNull PipTransitionController pipTransitionController,
+ @NonNull TvPipTransition tvPipTransition,
@NonNull PipParamsChangedForwarder pipParamsChangedForwarder,
Optional<SplitScreenController> splitScreenOptional,
@NonNull DisplayController displayController,
@@ -65,9 +65,10 @@
ShellExecutor mainExecutor) {
super(context, syncTransactionQueue, pipTransitionState, pipBoundsState,
pipDisplayLayoutState, boundsHandler, pipMenuController, pipAnimationController,
- surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+ surfaceTransactionHelper, tvPipTransition, pipParamsChangedForwarder,
splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer,
mainExecutor);
+ mTvPipTransition = tvPipTransition;
}
@Override
@@ -105,4 +106,14 @@
// when the menu alpha is 0 (e.g. when a fade-in animation starts).
return true;
}
+
+ @Override
+ protected void cancelAnimationOnTaskVanished() {
+ mTvPipTransition.cancelAnimations();
+ if (mLeash != null) {
+ mSurfaceControlTransactionFactory.getTransaction()
+ .setAlpha(mLeash, 0f)
+ .apply();
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
index f24b2b3..571c839 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java
@@ -16,43 +16,822 @@
package com.android.wm.shell.pip.tv;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_CLOSE;
+import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.view.WindowManager.TRANSIT_PIP;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.view.WindowManager.transitTypeToString;
+
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
+import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE;
+import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP;
+import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP;
+import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP;
+
+import android.animation.AnimationHandler;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.TaskInfo;
import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.os.IBinder;
+import android.os.Trace;
+import android.view.SurfaceControl;
+import android.view.WindowManager;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipDisplayLayoutState;
import com.android.wm.shell.pip.PipAnimationController;
import com.android.wm.shell.pip.PipSurfaceTransactionHelper;
-import com.android.wm.shell.pip.PipTransition;
+import com.android.wm.shell.pip.PipTransitionController;
import com.android.wm.shell.pip.PipTransitionState;
-import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
-import java.util.Optional;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
/**
* PiP Transition for TV.
*/
-public class TvPipTransition extends PipTransition {
+public class TvPipTransition extends PipTransitionController {
+ private static final String TAG = "TvPipTransition";
+ private static final float ZOOM_ANIMATION_SCALE_FACTOR = 0.97f;
+
+ private final PipTransitionState mPipTransitionState;
+ private final PipAnimationController mPipAnimationController;
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final TvPipMenuController mTvPipMenuController;
+ private final PipDisplayLayoutState mPipDisplayLayoutState;
+ private final PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory
+ mTransactionFactory;
+
+ private final ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
+ ThreadLocal.withInitial(() -> {
+ AnimationHandler handler = new AnimationHandler();
+ handler.setProvider(new SfVsyncFrameCallbackProvider());
+ return handler;
+ });
+
+ private final long mEnterFadeOutDuration;
+ private final long mEnterFadeInDuration;
+ private final long mExitFadeOutDuration;
+ private final long mExitFadeInDuration;
+
+ @Nullable
+ private Animator mCurrentAnimator;
+
+ /**
+ * The Task window that is currently in PIP windowing mode.
+ */
+ @Nullable
+ private WindowContainerToken mCurrentPipTaskToken;
+
+ @Nullable
+ private IBinder mPendingExitTransition;
public TvPipTransition(Context context,
@NonNull ShellInit shellInit,
@NonNull ShellTaskOrganizer shellTaskOrganizer,
@NonNull Transitions transitions,
TvPipBoundsState tvPipBoundsState,
- PipDisplayLayoutState pipDisplayLayoutState,
- PipTransitionState pipTransitionState,
TvPipMenuController tvPipMenuController,
TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
+ PipTransitionState pipTransitionState,
PipAnimationController pipAnimationController,
PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
- Optional<SplitScreenController> splitScreenOptional) {
- super(context, shellInit, shellTaskOrganizer, transitions, tvPipBoundsState,
- pipDisplayLayoutState, pipTransitionState, tvPipMenuController,
- tvPipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
- splitScreenOptional);
+ PipDisplayLayoutState pipDisplayLayoutState) {
+ super(shellInit, shellTaskOrganizer, transitions, tvPipBoundsState, tvPipMenuController,
+ tvPipBoundsAlgorithm);
+ mPipTransitionState = pipTransitionState;
+ mPipAnimationController = pipAnimationController;
+ mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
+ mTvPipMenuController = tvPipMenuController;
+ mPipDisplayLayoutState = pipDisplayLayoutState;
+ mTransactionFactory =
+ new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
+
+ mEnterFadeOutDuration = context.getResources().getInteger(
+ R.integer.config_tvPipEnterFadeOutDuration);
+ mEnterFadeInDuration = context.getResources().getInteger(
+ R.integer.config_tvPipEnterFadeInDuration);
+ mExitFadeOutDuration = context.getResources().getInteger(
+ R.integer.config_tvPipExitFadeOutDuration);
+ mExitFadeInDuration = context.getResources().getInteger(
+ R.integer.config_tvPipExitFadeInDuration);
}
+ @Override
+ public void startExitTransition(int type, WindowContainerTransaction out,
+ @Nullable Rect destinationBounds) {
+ cancelAnimations();
+ mPendingExitTransition = mTransitions.startTransition(type, out, this);
+ }
+
+ @Override
+ public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+
+ if (isCloseTransition(info)) {
+ // PiP is closing (without reentering fullscreen activity)
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Starting close animation", TAG);
+ cancelAnimations();
+ startCloseAnimation(info, startTransaction, finishTransaction, finishCallback);
+ mCurrentPipTaskToken = null;
+ return true;
+
+ } else if (transition.equals(mPendingExitTransition)) {
+ // PiP is exiting (reentering fullscreen activity)
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Starting exit animation", TAG);
+
+ final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
+ mPendingExitTransition = null;
+ // PipTaskChange can be null if the PIP task has been detached, for example, when the
+ // task contains multiple activities, the PIP will be moved to a new PIP task when
+ // entering, and be moved back when exiting. In that case, the PIP task will be removed
+ // immediately.
+ final TaskInfo pipTaskInfo = currentPipTaskChange != null
+ ? currentPipTaskChange.getTaskInfo()
+ : mPipOrganizer.getTaskInfo();
+ if (pipTaskInfo == null) {
+ throw new RuntimeException("Cannot find the pip task for exit-pip transition.");
+ }
+
+ final int type = info.getType();
+ switch (type) {
+ case TRANSIT_EXIT_PIP -> {
+ TransitionInfo.Change pipChange = currentPipTaskChange;
+ SurfaceControl activitySc = null;
+ if (mCurrentPipTaskToken == null) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: There is no existing PiP Task for TRANSIT_EXIT_PIP", TAG);
+ } else if (pipChange == null) {
+ // The pipTaskChange is null, this can happen if we are reparenting the
+ // PIP activity back to its original Task. In that case, we should animate
+ // the activity leash instead, which should be the change whose last parent
+ // is the recorded PiP Task.
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (mCurrentPipTaskToken.equals(change.getLastParent())) {
+ // Find the activity that is exiting PiP.
+ pipChange = change;
+ activitySc = change.getLeash();
+ break;
+ }
+ }
+ }
+ if (pipChange == null) {
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: No window of exiting PIP is found. Can't play expand "
+ + "animation",
+ TAG);
+ removePipImmediately(info, pipTaskInfo, startTransaction, finishTransaction,
+ finishCallback);
+ return true;
+ }
+ final TransitionInfo.Root root = TransitionUtil.getRootFor(pipChange, info);
+ final SurfaceControl pipLeash;
+ if (activitySc != null) {
+ // Use a local leash to animate activity in case the activity has
+ // letterbox which may be broken by PiP animation, e.g. always end at 0,0
+ // in parent and unable to include letterbox area in crop bounds.
+ final SurfaceControl activitySurface = pipChange.getLeash();
+ pipLeash = new SurfaceControl.Builder()
+ .setName(activitySc + "_pip-leash")
+ .setContainerLayer()
+ .setHidden(false)
+ .setParent(root.getLeash())
+ .build();
+ startTransaction.reparent(activitySurface, pipLeash);
+ // Put the activity at local position with offset in case it is letterboxed.
+ final Point activityOffset = pipChange.getEndRelOffset();
+ startTransaction.setPosition(activitySc, activityOffset.x,
+ activityOffset.y);
+ } else {
+ pipLeash = pipChange.getLeash();
+ startTransaction.reparent(pipLeash, root.getLeash());
+ }
+ startTransaction.setLayer(pipLeash, Integer.MAX_VALUE);
+ final Rect currentBounds = mPipBoundsState.getBounds();
+ final Rect destinationBounds = new Rect(pipChange.getEndAbsBounds());
+ cancelAnimations();
+ startExitAnimation(pipTaskInfo, pipLeash, currentBounds, destinationBounds,
+ startTransaction,
+ finishTransaction, finishCallback);
+ }
+ // pass through here is intended
+ case TRANSIT_TO_BACK, TRANSIT_REMOVE_PIP -> removePipImmediately(info, pipTaskInfo,
+ startTransaction, finishTransaction,
+ finishCallback
+ );
+ default -> {
+ return false;
+ }
+ }
+ mCurrentPipTaskToken = null;
+ return true;
+
+ } else if (isEnteringPip(info)) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Starting enter animation", TAG);
+
+ // Search for an Enter PiP transition
+ TransitionInfo.Change enterPip = null;
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) {
+ enterPip = change;
+ }
+ }
+ if (enterPip == null) {
+ throw new IllegalStateException("Trying to start PiP animation without a pip"
+ + "participant");
+ }
+
+ // Make sure other open changes are visible as entering PIP. Some may be hidden in
+ // Transitions#setupStartState because the transition type is OPEN (such as auto-enter).
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (change == enterPip) continue;
+ if (TransitionUtil.isOpeningType(change.getMode())) {
+ final SurfaceControl leash = change.getLeash();
+ startTransaction.show(leash).setAlpha(leash, 1.f);
+ }
+ }
+
+ cancelAnimations();
+ startEnterAnimation(enterPip, startTransaction, finishTransaction, finishCallback);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * For {@link Transitions#TRANSIT_REMOVE_PIP}, we just immediately remove the PIP Task.
+ */
+ private void removePipImmediately(@NonNull TransitionInfo info,
+ @NonNull TaskInfo taskInfo, @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: removePipImmediately", TAG);
+ cancelAnimations();
+ startTransaction.apply();
+ finishTransaction.setWindowCrop(info.getChanges().get(0).getLeash(),
+ mPipDisplayLayoutState.getDisplayBounds());
+ mTvPipMenuController.detach();
+ mPipOrganizer.onExitPipFinished(taskInfo);
+ finishCallback.onTransitionFinished(/* wct= */ null);
+
+ mPipTransitionState.setTransitionState(UNDEFINED);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
+ }
+
+ private void startCloseAnimation(@NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final TransitionInfo.Change pipTaskChange = findCurrentPipTaskChange(info);
+ final SurfaceControl pipLeash = pipTaskChange.getLeash();
+
+ final List<SurfaceControl> closeLeashes = new ArrayList<>();
+ for (TransitionInfo.Change change : info.getChanges()) {
+ if (TransitionUtil.isClosingType(change.getMode()) && change != pipTaskChange) {
+ closeLeashes.add(change.getLeash());
+ }
+ }
+
+ final Rect pipBounds = mPipBoundsState.getBounds();
+ mSurfaceTransactionHelper
+ .resetScale(startTransaction, pipLeash, pipBounds)
+ .crop(startTransaction, pipLeash, pipBounds)
+ .shadow(startTransaction, pipLeash, false);
+
+ final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction();
+ for (SurfaceControl leash : closeLeashes) {
+ startTransaction.setShadowRadius(leash, 0f);
+ }
+
+ ValueAnimator closeFadeOutAnimator = createAnimator();
+ closeFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+ closeFadeOutAnimator.setDuration(mExitFadeOutDuration);
+ closeFadeOutAnimator.addUpdateListener(
+ animationUpdateListener(pipLeash).fadingOut().withMenu());
+ for (SurfaceControl leash : closeLeashes) {
+ closeFadeOutAnimator.addUpdateListener(animationUpdateListener(leash).fadingOut());
+ }
+
+ closeFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(@NonNull Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close animation: start", TAG);
+ for (SurfaceControl leash : closeLeashes) {
+ startTransaction.setShadowRadius(leash, 0f);
+ }
+ startTransaction.apply();
+
+ mPipTransitionState.setTransitionState(EXITING_PIP);
+ sendOnPipTransitionStarted(TRANSITION_DIRECTION_REMOVE_STACK);
+ }
+
+ @Override
+ public void onAnimationCancel(@NonNull Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close animation: cancel", TAG);
+ sendOnPipTransitionCancelled(TRANSITION_DIRECTION_REMOVE_STACK);
+ }
+
+ @Override
+ public void onAnimationEnd(@NonNull Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: close animation: end", TAG);
+ mTvPipMenuController.detach();
+ finishCallback.onTransitionFinished(null /* wct */);
+ transaction.close();
+ mPipTransitionState.setTransitionState(UNDEFINED);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_REMOVE_STACK);
+
+ mCurrentAnimator = null;
+ }
+ });
+
+ closeFadeOutAnimator.start();
+ mCurrentAnimator = closeFadeOutAnimator;
+ }
+
+ @Override
+ public void startEnterAnimation(@NonNull TransitionInfo.Change pipChange,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ // Keep track of the PIP task
+ mCurrentPipTaskToken = pipChange.getContainer();
+ final ActivityManager.RunningTaskInfo taskInfo = pipChange.getTaskInfo();
+ final SurfaceControl leash = pipChange.getLeash();
+
+ mTvPipMenuController.attach(leash);
+ setBoundsStateForEntry(taskInfo.topActivity, taskInfo.pictureInPictureParams,
+ taskInfo.topActivityInfo);
+
+ final Rect pipBounds =
+ mPipBoundsAlgorithm.getEntryDestinationBoundsIgnoringKeepClearAreas();
+ mPipBoundsState.setBounds(pipBounds);
+ mTvPipMenuController.movePipMenu(null, pipBounds, 0f);
+
+ final WindowContainerTransaction resizePipWct = new WindowContainerTransaction();
+ resizePipWct.setWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
+ resizePipWct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_PINNED);
+ resizePipWct.setBounds(taskInfo.token, pipBounds);
+
+ mSurfaceTransactionHelper
+ .resetScale(finishTransaction, leash, pipBounds)
+ .crop(finishTransaction, leash, pipBounds)
+ .shadow(finishTransaction, leash, false);
+
+ final Rect currentBounds = pipChange.getStartAbsBounds();
+ final Rect fadeOutCurrentBounds = scaledRect(currentBounds, ZOOM_ANIMATION_SCALE_FACTOR);
+
+ final ValueAnimator enterFadeOutAnimator = createAnimator();
+ enterFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+ enterFadeOutAnimator.setDuration(mEnterFadeOutDuration);
+ enterFadeOutAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingOut()
+ .animateBounds(currentBounds, fadeOutCurrentBounds, currentBounds));
+
+ enterFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @SuppressLint("MissingPermission")
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter fade out animation: end", TAG);
+ SurfaceControl.Transaction tx = mTransactionFactory.getTransaction();
+ mSurfaceTransactionHelper
+ .resetScale(tx, leash, pipBounds)
+ .crop(tx, leash, pipBounds)
+ .shadow(tx, leash, false);
+ mShellTaskOrganizer.applyTransaction(resizePipWct);
+ tx.apply();
+ }
+ });
+
+ final ValueAnimator enterFadeInAnimator = createAnimator();
+ enterFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
+ enterFadeInAnimator.setDuration(mEnterFadeInDuration);
+ enterFadeInAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingIn()
+ .withMenu()
+ .atBounds(pipBounds));
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet
+ .play(enterFadeInAnimator)
+ .after(500)
+ .after(enterFadeOutAnimator);
+
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter animation: start", TAG);
+ startTransaction.apply();
+ mPipTransitionState.setTransitionState(ENTERING_PIP);
+ sendOnPipTransitionStarted(TRANSITION_DIRECTION_TO_PIP);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter animation: cancel", TAG);
+ enterFadeInAnimator.setCurrentFraction(1f);
+ sendOnPipTransitionCancelled(TRANSITION_DIRECTION_TO_PIP);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: enter animation: end", TAG);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ wct.setBounds(taskInfo.token, pipBounds);
+ finishCallback.onTransitionFinished(wct);
+
+ mPipTransitionState.setTransitionState(ENTERED_PIP);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP);
+ mCurrentAnimator = null;
+ }
+ });
+
+ animatorSet.start();
+ mCurrentAnimator = animatorSet;
+ }
+
+ private void startExitAnimation(@NonNull TaskInfo taskInfo, SurfaceControl leash,
+ Rect currentBounds, Rect destinationBounds,
+ @NonNull SurfaceControl.Transaction startTransaction,
+ @NonNull SurfaceControl.Transaction finishTransaction,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ final Rect fadeInStartBounds = scaledRect(destinationBounds, ZOOM_ANIMATION_SCALE_FACTOR);
+
+ final ValueAnimator exitFadeOutAnimator = createAnimator();
+ exitFadeOutAnimator.setInterpolator(TvPipInterpolators.EXIT);
+ exitFadeOutAnimator.setDuration(mExitFadeOutDuration);
+ exitFadeOutAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingOut()
+ .withMenu()
+ .atBounds(currentBounds));
+ exitFadeOutAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit fade out animation: end", TAG);
+ startTransaction.apply();
+ mPipMenuController.detach();
+ }
+ });
+
+ final ValueAnimator exitFadeInAnimator = createAnimator();
+ exitFadeInAnimator.setInterpolator(TvPipInterpolators.ENTER);
+ exitFadeInAnimator.setDuration(mExitFadeInDuration);
+ exitFadeInAnimator.addUpdateListener(
+ animationUpdateListener(leash)
+ .fadingIn()
+ .animateBounds(fadeInStartBounds, destinationBounds, destinationBounds));
+
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playSequentially(
+ exitFadeOutAnimator,
+ exitFadeInAnimator
+ );
+
+ animatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit animation: start", TAG);
+ mPipTransitionState.setTransitionState(EXITING_PIP);
+ sendOnPipTransitionStarted(TRANSITION_DIRECTION_LEAVE_PIP);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit animation: cancel", TAG);
+ sendOnPipTransitionCancelled(TRANSITION_DIRECTION_LEAVE_PIP);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: exit animation: end", TAG);
+ mPipOrganizer.onExitPipFinished(taskInfo);
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ wct.setActivityWindowingMode(taskInfo.token, WINDOWING_MODE_UNDEFINED);
+ wct.setBounds(taskInfo.token, destinationBounds);
+ finishCallback.onTransitionFinished(wct);
+
+ mPipTransitionState.setTransitionState(UNDEFINED);
+ sendOnPipTransitionFinished(TRANSITION_DIRECTION_LEAVE_PIP);
+
+ mCurrentAnimator = null;
+ }
+ });
+
+ animatorSet.start();
+ mCurrentAnimator = animatorSet;
+ }
+
+ @NonNull
+ private ValueAnimator createAnimator() {
+ final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
+ animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get());
+ return animator;
+ }
+
+ @NonNull
+ private TvPipTransitionAnimatorUpdateListener animationUpdateListener(
+ @NonNull SurfaceControl leash) {
+ return new TvPipTransitionAnimatorUpdateListener(leash, mTvPipMenuController,
+ mTransactionFactory.getTransaction(), mSurfaceTransactionHelper);
+ }
+
+ @NonNull
+ private static Rect scaledRect(@NonNull Rect rect, float scale) {
+ final Rect out = new Rect(rect);
+ out.inset((int) (rect.width() * (1 - scale) / 2), (int) (rect.height() * (1 - scale) / 2));
+ return out;
+ }
+
+ private boolean isCloseTransition(TransitionInfo info) {
+ final TransitionInfo.Change currentPipTaskChange = findCurrentPipTaskChange(info);
+ return currentPipTaskChange != null && info.getType() == TRANSIT_CLOSE;
+ }
+
+ @Nullable
+ private TransitionInfo.Change findCurrentPipTaskChange(@NonNull TransitionInfo info) {
+ if (mCurrentPipTaskToken == null) {
+ return null;
+ }
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (mCurrentPipTaskToken.equals(change.getContainer())) {
+ return change;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Whether we should handle the given {@link TransitionInfo} animation as entering PIP.
+ */
+ private boolean isEnteringPip(@NonNull TransitionInfo info) {
+ for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+ final TransitionInfo.Change change = info.getChanges().get(i);
+ if (isEnteringPip(change, info.getType())) return true;
+ }
+ return false;
+ }
+
+ /**
+ * Whether a particular change is a window that is entering pip.
+ */
+ @Override
+ public boolean isEnteringPip(@NonNull TransitionInfo.Change change,
+ @WindowManager.TransitionType int transitType) {
+ if (change.getTaskInfo() != null
+ && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED
+ && !Objects.equals(change.getContainer(), mCurrentPipTaskToken)) {
+ if (transitType == TRANSIT_PIP || transitType == TRANSIT_OPEN
+ || transitType == TRANSIT_CHANGE) {
+ return true;
+ }
+ // Please file a bug to handle the unexpected transition type.
+ android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type="
+ + transitTypeToString(transitType), new Throwable());
+ }
+ return false;
+ }
+
+ @Override
+ public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
+ @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+ @NonNull Transitions.TransitionFinishCallback finishCallback) {
+ ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG);
+ if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) {
+ mCurrentAnimator.end();
+ }
+ }
+
+ @Nullable
+ @Override
+ public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
+ @NonNull TransitionRequestInfo request) {
+ if (requestHasPipEnter(request)) {
+ ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: handle PiP enter request", TAG);
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ augmentRequest(transition, request, wct);
+ return wct;
+ } else if (request.getType() == TRANSIT_TO_BACK && request.getTriggerTask() != null
+ && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_PINNED) {
+ // if we receive a TRANSIT_TO_BACK type of request while in PiP
+ mPendingExitTransition = transition;
+
+ // update the transition state to avoid {@link PipTaskOrganizer#onTaskVanished()} calls
+ mPipTransitionState.setTransitionState(EXITING_PIP);
+
+ // return an empty WindowContainerTransaction so that we don't check other handlers
+ return new WindowContainerTransaction();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void augmentRequest(@NonNull IBinder transition, @NonNull TransitionRequestInfo request,
+ @NonNull WindowContainerTransaction outWCT) {
+ if (!requestHasPipEnter(request)) {
+ throw new IllegalStateException("Called PiP augmentRequest when request has no PiP");
+ }
+ outWCT.setActivityWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_UNDEFINED);
+ }
+
+ /**
+ * Cancel any ongoing PiP transitions/animations.
+ */
+ public void cancelAnimations() {
+ if (mPipAnimationController.isAnimating()) {
+ mPipAnimationController.getCurrentAnimator().cancel();
+ mPipAnimationController.resetAnimatorState();
+ }
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.cancel();
+ }
+ }
+
+ @Override
+ public void end() {
+ if (mCurrentAnimator != null) {
+ mCurrentAnimator.end();
+ }
+ }
+
+ private static class TvPipTransitionAnimatorUpdateListener implements
+ ValueAnimator.AnimatorUpdateListener {
+ private final SurfaceControl mLeash;
+ private final TvPipMenuController mTvPipMenuController;
+ private final SurfaceControl.Transaction mTransaction;
+ private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
+ private final RectF mTmpRectF = new RectF();
+ private final Rect mTmpRect = new Rect();
+
+ private float mStartAlpha = ALPHA_NO_CHANGE;
+ private float mEndAlpha = ALPHA_NO_CHANGE;
+
+ @Nullable
+ private Rect mStartBounds;
+ @Nullable
+ private Rect mEndBounds;
+ private Rect mWindowContainerBounds;
+ private boolean mShowMenu;
+
+ TvPipTransitionAnimatorUpdateListener(@NonNull SurfaceControl leash,
+ @NonNull TvPipMenuController tvPipMenuController,
+ @NonNull SurfaceControl.Transaction transaction,
+ @NonNull PipSurfaceTransactionHelper pipSurfaceTransactionHelper) {
+ mLeash = leash;
+ mTvPipMenuController = tvPipMenuController;
+ mTransaction = transaction;
+ mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
+ }
+
+ public TvPipTransitionAnimatorUpdateListener animateAlpha(
+ @FloatRange(from = 0.0, to = 1.0) float startAlpha,
+ @FloatRange(from = 0.0, to = 1.0) float endAlpha) {
+ mStartAlpha = startAlpha;
+ mEndAlpha = endAlpha;
+ return this;
+ }
+
+ public TvPipTransitionAnimatorUpdateListener animateBounds(@NonNull Rect startBounds,
+ @NonNull Rect endBounds, @NonNull Rect windowContainerBounds) {
+ mStartBounds = startBounds;
+ mEndBounds = endBounds;
+ mWindowContainerBounds = windowContainerBounds;
+ return this;
+ }
+
+ public TvPipTransitionAnimatorUpdateListener atBounds(@NonNull Rect bounds) {
+ return animateBounds(bounds, bounds, bounds);
+ }
+
+ public TvPipTransitionAnimatorUpdateListener fadingOut() {
+ return animateAlpha(1f, 0f);
+ }
+
+ public TvPipTransitionAnimatorUpdateListener fadingIn() {
+ return animateAlpha(0f, 1f);
+ }
+
+ public TvPipTransitionAnimatorUpdateListener withMenu() {
+ mShowMenu = true;
+ return this;
+ }
+
+ @Override
+ public void onAnimationUpdate(@NonNull ValueAnimator animation) {
+ final float fraction = animation.getAnimatedFraction();
+ final float alpha = lerp(mStartAlpha, mEndAlpha, fraction);
+ if (mStartBounds != null && mEndBounds != null) {
+ lerp(mStartBounds, mEndBounds, fraction, mTmpRectF);
+ applyAnimatedValue(alpha, mTmpRectF);
+ } else {
+ applyAnimatedValue(alpha, null);
+ }
+ }
+
+ private void applyAnimatedValue(float alpha, @Nullable RectF bounds) {
+ Trace.beginSection("applyAnimatedValue");
+ final SurfaceControl.Transaction tx = mTransaction;
+
+ Trace.beginSection("leash scale and alpha");
+ if (alpha != ALPHA_NO_CHANGE) {
+ mSurfaceTransactionHelper.alpha(tx, mLeash, alpha);
+ }
+ if (bounds != null) {
+ mSurfaceTransactionHelper.scale(tx, mLeash, mWindowContainerBounds, bounds);
+ }
+ mSurfaceTransactionHelper.shadow(tx, mLeash, false);
+ tx.show(mLeash);
+ Trace.endSection();
+
+ if (mShowMenu) {
+ Trace.beginSection("movePipMenu");
+ if (bounds != null) {
+ mTmpRect.set((int) bounds.left, (int) bounds.top, (int) bounds.right,
+ (int) bounds.bottom);
+ mTvPipMenuController.movePipMenu(tx, mTmpRect, alpha);
+ } else {
+ mTvPipMenuController.movePipMenu(tx, null, alpha);
+ }
+ Trace.endSection();
+ } else {
+ mTvPipMenuController.movePipMenu(tx, null, 0f);
+ }
+
+ tx.apply();
+ Trace.endSection();
+ }
+
+ private float lerp(float start, float end, float fraction) {
+ return start * (1 - fraction) + end * fraction;
+ }
+
+ private void lerp(@NonNull Rect start, @NonNull Rect end, float fraction,
+ @NonNull RectF out) {
+ out.set(
+ start.left * (1 - fraction) + end.left * fraction,
+ start.top * (1 - fraction) + end.top * fraction,
+ start.right * (1 - fraction) + end.right * fraction,
+ start.bottom * (1 - fraction) + end.bottom * fraction);
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 41ec33c..b98762d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -654,12 +654,27 @@
info.setUnreleasedWarningCallSiteForAllSurfaces("Transitions.onTransitionReady");
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady (#%d) %s: %s",
info.getDebugId(), transitionToken, info);
- final int activeIdx = findByToken(mPendingTransitions, transitionToken);
+ int activeIdx = findByToken(mPendingTransitions, transitionToken);
if (activeIdx < 0) {
- throw new IllegalStateException("Got transitionReady for non-pending transition "
+ final ActiveTransition existing = getKnownTransition(transitionToken);
+ if (existing != null) {
+ Log.e(TAG, "Got duplicate transitionReady for " + transitionToken);
+ // The transition is already somewhere else in the pipeline, so just return here.
+ t.apply();
+ existing.mFinishT.merge(finishT);
+ return;
+ }
+ // This usually means the system is in a bad state and may not recover; however,
+ // there's an incentive to propagate bad states rather than crash, so we're kinda
+ // required to do the same thing I guess.
+ Log.wtf(TAG, "Got transitionReady for non-pending transition "
+ transitionToken + ". expecting one of "
+ Arrays.toString(mPendingTransitions.stream().map(
activeTransition -> activeTransition.mToken).toArray()));
+ final ActiveTransition fallback = new ActiveTransition();
+ fallback.mToken = transitionToken;
+ mPendingTransitions.add(fallback);
+ activeIdx = mPendingTransitions.size() - 1;
}
// Move from pending to ready
final ActiveTransition active = mPendingTransitions.remove(activeIdx);
@@ -1050,34 +1065,43 @@
processReadyQueue(track);
}
- private boolean isTransitionKnown(IBinder token) {
+ /**
+ * Checks to see if the transition specified by `token` is already known. If so, it will be
+ * returned.
+ */
+ @Nullable
+ private ActiveTransition getKnownTransition(IBinder token) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
- if (mPendingTransitions.get(i).mToken == token) return true;
+ final ActiveTransition active = mPendingTransitions.get(i);
+ if (active.mToken == token) return active;
}
for (int i = 0; i < mReadyDuringSync.size(); ++i) {
- if (mReadyDuringSync.get(i).mToken == token) return true;
+ final ActiveTransition active = mReadyDuringSync.get(i);
+ if (active.mToken == token) return active;
}
for (int t = 0; t < mTracks.size(); ++t) {
final Track tr = mTracks.get(t);
for (int i = 0; i < tr.mReadyTransitions.size(); ++i) {
- if (tr.mReadyTransitions.get(i).mToken == token) return true;
+ final ActiveTransition active = tr.mReadyTransitions.get(i);
+ if (active.mToken == token) return active;
}
final ActiveTransition active = tr.mActiveTransition;
if (active == null) continue;
- if (active.mToken == token) return true;
+ if (active.mToken == token) return active;
if (active.mMerged == null) continue;
for (int m = 0; m < active.mMerged.size(); ++m) {
- if (active.mMerged.get(m).mToken == token) return true;
+ final ActiveTransition merged = active.mMerged.get(m);
+ if (merged.mToken == token) return merged;
}
}
- return false;
+ return null;
}
void requestStartTransition(@NonNull IBinder transitionToken,
@Nullable TransitionRequestInfo request) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested (#%d): %s %s",
request.getDebugId(), transitionToken, request);
- if (isTransitionKnown(transitionToken)) {
+ if (getKnownTransition(transitionToken) != null) {
throw new RuntimeException("Transition already started " + transitionToken);
}
final ActiveTransition active = new ActiveTransition();
@@ -1161,7 +1185,7 @@
*/
private void finishForSync(ActiveTransition reason,
int trackIdx, @Nullable ActiveTransition forceFinish) {
- if (!isTransitionKnown(reason.mToken)) {
+ if (getKnownTransition(reason.mToken) == null) {
Log.d(TAG, "finishForSleep: already played sync transition " + reason);
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index 3add6f4..dd6ca8d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -536,6 +536,11 @@
if (mGestureDetector.onTouchEvent(e)) {
return true;
}
+ if (e.getActionMasked() == MotionEvent.ACTION_CANCEL) {
+ // If a motion event is cancelled, reset mShouldClick so a click is not accidentally
+ // performed.
+ mShouldClick = false;
+ }
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
mDragPointerId = e.getPointerId(0);
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto
index 406ada9..5a017ad 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/trace_config/trace_config.textproto
@@ -63,11 +63,7 @@
atrace_categories: "sched_process_exit"
atrace_apps: "com.android.server.wm.flicker.testapp"
atrace_apps: "com.android.systemui"
- atrace_apps: "com.android.wm.shell.flicker"
atrace_apps: "com.android.wm.shell.flicker.other"
- atrace_apps: "com.android.wm.shell.flicker.bubbles"
- atrace_apps: "com.android.wm.shell.flicker.pip"
- atrace_apps: "com.android.wm.shell.flicker.splitscreen"
atrace_apps: "com.google.android.apps.nexuslauncher"
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto
index 406ada9..1599831 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/trace_config/trace_config.textproto
@@ -63,11 +63,7 @@
atrace_categories: "sched_process_exit"
atrace_apps: "com.android.server.wm.flicker.testapp"
atrace_apps: "com.android.systemui"
- atrace_apps: "com.android.wm.shell.flicker"
- atrace_apps: "com.android.wm.shell.flicker.other"
atrace_apps: "com.android.wm.shell.flicker.bubbles"
- atrace_apps: "com.android.wm.shell.flicker.pip"
- atrace_apps: "com.android.wm.shell.flicker.splitscreen"
atrace_apps: "com.google.android.apps.nexuslauncher"
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto
index 406ada9..fc15ff9 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto
+++ b/libs/WindowManager/Shell/tests/flicker/pip/trace_config/trace_config.textproto
@@ -63,11 +63,7 @@
atrace_categories: "sched_process_exit"
atrace_apps: "com.android.server.wm.flicker.testapp"
atrace_apps: "com.android.systemui"
- atrace_apps: "com.android.wm.shell.flicker"
- atrace_apps: "com.android.wm.shell.flicker.other"
- atrace_apps: "com.android.wm.shell.flicker.bubbles"
atrace_apps: "com.android.wm.shell.flicker.pip"
- atrace_apps: "com.android.wm.shell.flicker.splitscreen"
atrace_apps: "com.google.android.apps.nexuslauncher"
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto
index 406ada9..9f2e497 100644
--- a/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto
+++ b/libs/WindowManager/Shell/tests/flicker/service/trace_config/trace_config.textproto
@@ -63,11 +63,7 @@
atrace_categories: "sched_process_exit"
atrace_apps: "com.android.server.wm.flicker.testapp"
atrace_apps: "com.android.systemui"
- atrace_apps: "com.android.wm.shell.flicker"
- atrace_apps: "com.android.wm.shell.flicker.other"
- atrace_apps: "com.android.wm.shell.flicker.bubbles"
- atrace_apps: "com.android.wm.shell.flicker.pip"
- atrace_apps: "com.android.wm.shell.flicker.splitscreen"
+ atrace_apps: "com.android.wm.shell.flicker.service"
atrace_apps: "com.google.android.apps.nexuslauncher"
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
index 406ada9..b55f4ec 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
@@ -63,10 +63,6 @@
atrace_categories: "sched_process_exit"
atrace_apps: "com.android.server.wm.flicker.testapp"
atrace_apps: "com.android.systemui"
- atrace_apps: "com.android.wm.shell.flicker"
- atrace_apps: "com.android.wm.shell.flicker.other"
- atrace_apps: "com.android.wm.shell.flicker.bubbles"
- atrace_apps: "com.android.wm.shell.flicker.pip"
atrace_apps: "com.android.wm.shell.flicker.splitscreen"
atrace_apps: "com.google.android.apps.nexuslauncher"
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 4e2b7f6..800f9e4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -66,6 +66,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -283,7 +284,7 @@
doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
.round(any(), any(), anyBoolean());
doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
- .scale(any(), any(), any(), any(), anyFloat());
+ .scale(any(), any(), any(), ArgumentMatchers.<Rect>any(), anyFloat());
doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper)
.alpha(any(), any(), anyFloat());
doNothing().when(mMockPipSurfaceTransactionHelper).onDensityOrFontScaleChanged(any());
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index da728f9..79a7357 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -553,6 +553,7 @@
"utils/Blur.cpp",
"utils/Color.cpp",
"utils/LinearAllocator.cpp",
+ "utils/TypefaceUtils.cpp",
"utils/VectorDrawableUtils.cpp",
"AnimationContext.cpp",
"Animator.cpp",
diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp
index 34cb4ae..f4ee36ec 100644
--- a/libs/hwui/hwui/MinikinSkia.cpp
+++ b/libs/hwui/hwui/MinikinSkia.cpp
@@ -30,6 +30,7 @@
#include <minikin/MinikinExtent.h>
#include <minikin/MinikinPaint.h>
#include <minikin/MinikinRect.h>
+#include <utils/TypefaceUtils.h>
namespace android {
@@ -142,7 +143,7 @@
skVariation[i].value = SkFloatToScalar(variations[i].value);
}
args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())});
- sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
+ sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr();
sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), args));
return std::make_shared<MinikinFontSkia>(std::move(face), mSourceId, mFontData, mFontSize,
diff --git a/libs/hwui/jni/FontFamily.cpp b/libs/hwui/jni/FontFamily.cpp
index 0c3af61..e6d790f 100644
--- a/libs/hwui/jni/FontFamily.cpp
+++ b/libs/hwui/jni/FontFamily.cpp
@@ -31,6 +31,7 @@
#include <minikin/FontFamily.h>
#include <minikin/LocaleList.h>
#include <ui/FatVector.h>
+#include <utils/TypefaceUtils.h>
#include <memory>
@@ -125,7 +126,7 @@
args.setCollectionIndex(ttcIndex);
args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())});
- sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
+ sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr();
sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args));
if (face == NULL) {
ALOGE("addFont failed to create font, invalid request");
diff --git a/libs/hwui/jni/fonts/Font.cpp b/libs/hwui/jni/fonts/Font.cpp
index 2ec94c9..f405aba 100644
--- a/libs/hwui/jni/fonts/Font.cpp
+++ b/libs/hwui/jni/fonts/Font.cpp
@@ -37,6 +37,7 @@
#include <minikin/LocaleList.h>
#include <minikin/SystemFonts.h>
#include <ui/FatVector.h>
+#include <utils/TypefaceUtils.h>
#include <memory>
@@ -459,7 +460,7 @@
args.setCollectionIndex(ttcIndex);
args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())});
- sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
+ sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr();
sk_sp<SkTypeface> face(fm->makeFromStream(std::move(fontData), args));
if (face == nullptr) {
return nullptr;
diff --git a/libs/hwui/tests/unit/TypefaceTests.cpp b/libs/hwui/tests/unit/TypefaceTests.cpp
index 499afa0..c71c4d2 100644
--- a/libs/hwui/tests/unit/TypefaceTests.cpp
+++ b/libs/hwui/tests/unit/TypefaceTests.cpp
@@ -29,6 +29,7 @@
#include "hwui/MinikinSkia.h"
#include "hwui/Typeface.h"
+#include "utils/TypefaceUtils.h"
using namespace android;
@@ -56,7 +57,7 @@
sk_sp<SkData> skData =
SkData::MakeWithProc(data, st.st_size, unmap, reinterpret_cast<void*>(st.st_size));
std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(skData));
- sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
+ sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr();
sk_sp<SkTypeface> typeface(fm->makeFromStream(std::move(fontData)));
LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", fileName);
std::shared_ptr<minikin::MinikinFont> font =
diff --git a/libs/hwui/tests/unit/UnderlineTest.cpp b/libs/hwui/tests/unit/UnderlineTest.cpp
index db2be20..c70a304 100644
--- a/libs/hwui/tests/unit/UnderlineTest.cpp
+++ b/libs/hwui/tests/unit/UnderlineTest.cpp
@@ -36,6 +36,7 @@
#include "hwui/MinikinUtils.h"
#include "hwui/Paint.h"
#include "hwui/Typeface.h"
+#include "utils/TypefaceUtils.h"
using namespace android;
@@ -66,7 +67,7 @@
sk_sp<SkData> skData =
SkData::MakeWithProc(data, st.st_size, unmap, reinterpret_cast<void*>(st.st_size));
std::unique_ptr<SkStreamAsset> fontData(new SkMemoryStream(skData));
- sk_sp<SkFontMgr> fm(SkFontMgr::RefDefault());
+ sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr();
sk_sp<SkTypeface> typeface(fm->makeFromStream(std::move(fontData)));
LOG_ALWAYS_FATAL_IF(typeface == nullptr, "Failed to make typeface from %s", fileName);
std::shared_ptr<minikin::MinikinFont> font =
diff --git a/libs/hwui/utils/TypefaceUtils.cpp b/libs/hwui/utils/TypefaceUtils.cpp
new file mode 100644
index 0000000..a30b925
--- /dev/null
+++ b/libs/hwui/utils/TypefaceUtils.cpp
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+#include <utils/TypefaceUtils.h>
+
+#include "include/ports/SkFontMgr_empty.h"
+
+namespace android {
+
+sk_sp<SkFontMgr> FreeTypeFontMgr() {
+ static sk_sp<SkFontMgr> mgr = SkFontMgr_New_Custom_Empty();
+ return mgr;
+}
+
+} // namespace android
diff --git a/libs/hwui/utils/TypefaceUtils.h b/libs/hwui/utils/TypefaceUtils.h
new file mode 100644
index 0000000..c0adeae
--- /dev/null
+++ b/libs/hwui/utils/TypefaceUtils.h
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include "SkFontMgr.h"
+#include "SkRefCnt.h"
+
+namespace android {
+
+// Return an SkFontMgr which is capable of turning bytes into a SkTypeface using Freetype.
+// There are no other fonts inside this SkFontMgr (e.g. no system fonts).
+sk_sp<SkFontMgr> FreeTypeFontMgr();
+
+} // namespace android
\ No newline at end of file
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 5d211f4..de842e6 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,8 +19,8 @@
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.content.Context.DEVICE_ID_DEFAULT;
-
import static android.media.audio.Flags.autoPublicVolumeApiHardening;
+import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
import android.Manifest;
@@ -2942,6 +2942,33 @@
}
//====================================================================
+ // Loudness management
+ private final Object mLoudnessCodecLock = new Object();
+
+ @GuardedBy("mLoudnessCodecLock")
+ private LoudnessCodecDispatcher mLoudnessCodecDispatcher = null;
+
+ /**
+ * Creates a new instance of {@link LoudnessCodecConfigurator}.
+ * @return the {@link LoudnessCodecConfigurator} instance
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public @NonNull LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
+ LoudnessCodecConfigurator configurator;
+ synchronized (mLoudnessCodecLock) {
+ // initialize lazily
+ if (mLoudnessCodecDispatcher == null) {
+ mLoudnessCodecDispatcher = new LoudnessCodecDispatcher(this);
+ }
+ configurator = mLoudnessCodecDispatcher.createLoudnessCodecConfigurator();
+ }
+ return configurator;
+ }
+
+ //====================================================================
// Bluetooth SCO control
/**
* Sticky broadcast intent action indicating that the Bluetooth SCO audio
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 12ddf9b..11e3a08 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -37,6 +37,7 @@
import android.media.ICommunicationDeviceDispatcher;
import android.media.IDeviceVolumeBehaviorDispatcher;
import android.media.IDevicesForAttributesCallback;
+import android.media.ILoudnessCodecUpdatesDispatcher;
import android.media.IMuteAwaitConnectionCallback;
import android.media.IPlaybackConfigDispatcher;
import android.media.IPreferredMixerAttributesDispatcher;
@@ -51,6 +52,7 @@
import android.media.ISpatializerOutputCallback;
import android.media.IStreamAliasingDispatcher;
import android.media.IVolumeController;
+import android.media.LoudnessCodecFormat;
import android.media.PlayerBase;
import android.media.VolumeInfo;
import android.media.VolumePolicy;
@@ -728,4 +730,20 @@
@EnforcePermission("MODIFY_AUDIO_ROUTING")
@JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
boolean isBluetoothVariableLatencyEnabled();
+
+ void registerLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
+
+ void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
+
+ oneway void startLoudnessCodecUpdates(in int piid);
+
+ oneway void stopLoudnessCodecUpdates(in int piid);
+
+ oneway void addLoudnesssCodecFormat(in int piid, in LoudnessCodecFormat format);
+
+ oneway void addLoudnesssCodecFormatList(in int piid, in List<LoudnessCodecFormat> format);
+
+ oneway void removeLoudnessCodecFormat(in int piid, in LoudnessCodecFormat format);
+
+ PersistableBundle getLoudnessParams(in int piid, in LoudnessCodecFormat format);
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt b/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
copy to media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl
index 039078e..16eaaea 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
+++ b/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl
@@ -12,20 +12,20 @@
* 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.communal.ui.view
+package android.media;
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.LinearLayout
-import com.android.systemui.res.R
+import android.os.PersistableBundle;
-/** Wraps around a widget rendered in communal mode. */
-class CommunalWidgetWrapper(context: Context, attrs: AttributeSet? = null) :
- LinearLayout(context, attrs) {
- init {
- id = R.id.communal_widget_wrapper
- }
-}
+/**
+ * Interface which provides updates for the clients about MediaCodec loudness
+ * parameter changes.
+ *
+ * {@hide}
+ */
+oneway interface ILoudnessCodecUpdatesDispatcher {
+
+ void dispatchLoudnessCodecParameterChange(int piid, in PersistableBundle params);
+
+}
\ No newline at end of file
diff --git a/media/java/android/media/LoudnessCodecConfigurator.java b/media/java/android/media/LoudnessCodecConfigurator.java
new file mode 100644
index 0000000..409abc2
--- /dev/null
+++ b/media/java/android/media/LoudnessCodecConfigurator.java
@@ -0,0 +1,225 @@
+/*
+ * 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 android.media;
+
+import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.FlaggedApi;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+/**
+ * Class for getting recommended loudness parameter updates for audio decoders, according to the
+ * encoded format and current audio routing. Those updates can be automatically applied to the
+ * {@link MediaCodec} instance(s), or be provided to the user. The codec loudness management
+ * updates are defined by the CTA-2075 standard.
+ * <p>A new object should be instantiated for each {@link AudioTrack} with the help
+ * of {@link AudioManager#createLoudnessCodecConfigurator()}.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+public class LoudnessCodecConfigurator {
+ private static final String TAG = "LoudnessCodecConfigurator";
+
+ /**
+ * Listener used for receiving asynchronous loudness metadata updates.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public interface OnLoudnessCodecUpdateListener {
+ /**
+ * Contains the MediaCodec key/values that can be set directly to
+ * configure the loudness of the handle's corresponding decoder (see
+ * {@link MediaCodec#setParameters(Bundle)}).
+ *
+ * @param mediaCodec the mediaCodec that will receive the new parameters
+ * @param codecValues contains loudness key/value pairs that can be set
+ * directly on the mediaCodec. The listener can modify
+ * these values with their own edits which will be
+ * returned for the mediaCodec configuration
+ * @return a Bundle which contains the original computed codecValues
+ * aggregated with user edits. The platform will configure the associated
+ * MediaCodecs with the returned Bundle params.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ @NonNull
+ default Bundle onLoudnessCodecUpdate(@NonNull MediaCodec mediaCodec,
+ @NonNull Bundle codecValues) {
+ return codecValues;
+ }
+ }
+
+ @NonNull private final LoudnessCodecDispatcher mLcDispatcher;
+
+ private AudioTrack mAudioTrack;
+
+ private final List<MediaCodec> mMediaCodecs = new ArrayList<>();
+
+ /** @hide */
+ protected LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher) {
+ mLcDispatcher = Objects.requireNonNull(lcDispatcher);
+ }
+
+
+ /**
+ * Starts receiving asynchronous loudness updates and registers the listener for
+ * receiving {@link MediaCodec} loudness parameter updates.
+ * <p>This method should be called before {@link #startLoudnessCodecUpdates()} or
+ * after {@link #stopLoudnessCodecUpdates()}.
+ *
+ * @param executor {@link Executor} to handle the callbacks
+ * @param listener used to receive updates
+ *
+ * @return {@code true} if there is at least one {@link MediaCodec} and
+ * {@link AudioTrack} set and the user can expect receiving updates.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public boolean startLoudnessCodecUpdates(@NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(executor,
+ "Executor must not be null");
+ Objects.requireNonNull(listener,
+ "OnLoudnessCodecUpdateListener must not be null");
+ mLcDispatcher.addLoudnessCodecListener(this, executor, listener);
+
+ return checkStartLoudnessConfigurator();
+ }
+
+ /**
+ * Starts receiving asynchronous loudness updates.
+ * <p>The registered MediaCodecs will be updated automatically without any client
+ * callbacks.
+ *
+ * @return {@code true} if there is at least one MediaCodec and AudioTrack set
+ * (see {@link #setAudioTrack(AudioTrack)}, {@link #addMediaCodec(MediaCodec)})
+ * and the user can expect receiving updates.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public boolean startLoudnessCodecUpdates() {
+ mLcDispatcher.addLoudnessCodecListener(this,
+ Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
+ return checkStartLoudnessConfigurator();
+ }
+
+ /**
+ * Stops receiving asynchronous loudness updates.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void stopLoudnessCodecUpdates() {
+ mLcDispatcher.removeLoudnessCodecListener(this);
+ }
+
+ /**
+ * Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack}
+ * which is registered through {@link #setAudioTrack(AudioTrack)}.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void addMediaCodec(@NonNull MediaCodec mediaCodec) {
+ mMediaCodecs.add(Objects.requireNonNull(mediaCodec,
+ "MediaCodec for addMediaCodec must not be null"));
+ }
+
+ /**
+ * Removes the {@link MediaCodec} from receiving loudness updates.
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
+ mMediaCodecs.remove(Objects.requireNonNull(mediaCodec,
+ "MediaCodec for removeMediaCodec must not be null"));
+ }
+
+ /**
+ * Sets the {@link AudioTrack} that can receive audio data from the added
+ * {@link MediaCodec}'s. The {@link AudioTrack} is used to determine the devices
+ * on which the streaming will take place and hence will directly influence the
+ * loudness params.
+ * <p>Should be called before starting the loudness updates
+ * (see {@link #startLoudnessCodecUpdates()},
+ * {@link #startLoudnessCodecUpdates(Executor, OnLoudnessCodecUpdateListener)})
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setAudioTrack(@NonNull AudioTrack audioTrack) {
+ mAudioTrack = Objects.requireNonNull(audioTrack,
+ "AudioTrack for setAudioTrack must not be null");
+ }
+
+ /**
+ * Gets synchronous loudness updates when no listener is required and at least one
+ * {@link MediaCodec} which streams to a registered {@link AudioTrack} is set.
+ * Otherwise, an empty {@link Bundle} will be returned.
+ *
+ * @return the {@link Bundle} containing the current loudness parameters. Caller is
+ * responsible to update the {@link MediaCodec}
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ @NonNull
+ public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
+ // TODO: implement synchronous loudness params updates
+ return new Bundle();
+ }
+
+ private boolean checkStartLoudnessConfigurator() {
+ if (mAudioTrack == null) {
+ Log.w(TAG, "Cannot start loudness configurator without an AudioTrack");
+ return false;
+ }
+
+ if (mMediaCodecs.isEmpty()) {
+ Log.w(TAG, "Cannot start loudness configurator without at least one MediaCodec");
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java
new file mode 100644
index 0000000..fc5c354
--- /dev/null
+++ b/media/java/android/media/LoudnessCodecDispatcher.java
@@ -0,0 +1,109 @@
+/*
+ * 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 android.media;
+
+import android.annotation.CallbackExecutor;
+import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.os.PersistableBundle;
+import android.os.RemoteException;
+
+import androidx.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Class used to handle the loudness related communication with the audio service.
+ * @hide
+ */
+public class LoudnessCodecDispatcher {
+ private final class LoudnessCodecUpdatesDispatcherStub
+ extends ILoudnessCodecUpdatesDispatcher.Stub
+ implements CallbackUtil.DispatcherStub {
+ @Override
+ public void dispatchLoudnessCodecParameterChange(int piid, PersistableBundle params) {
+ mLoudnessListenerMgr.callListeners(listener ->
+ mConfiguratorListener.computeIfPresent(listener, (l, c) -> {
+ // TODO: send the bundle for the user to update
+ return c;
+ }));
+ }
+
+ @Override
+ public void register(boolean register) {
+ try {
+ if (register) {
+ mAm.getService().registerLoudnessCodecUpdatesDispatcher(this);
+ } else {
+ mAm.getService().unregisterLoudnessCodecUpdatesDispatcher(this);
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
+ mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
+
+ @NonNull private final LoudnessCodecUpdatesDispatcherStub mLoudnessCodecStub;
+
+ private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
+ mConfiguratorListener = new HashMap<>();
+
+ @NonNull private final AudioManager mAm;
+
+ protected LoudnessCodecDispatcher(@NonNull AudioManager am) {
+ mAm = Objects.requireNonNull(am);
+ mLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
+ }
+
+ /** @hide */
+ public LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
+ return new LoudnessCodecConfigurator(this);
+ }
+
+ /** @hide */
+ public void addLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(configurator);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+
+ mConfiguratorListener.put(listener, configurator);
+ mLoudnessListenerMgr.addListener(
+ executor, listener, "addLoudnessCodecListener", () -> mLoudnessCodecStub);
+ }
+
+ /** @hide */
+ public void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
+ Objects.requireNonNull(configurator);
+
+ for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
+ mConfiguratorListener.entrySet()) {
+ if (e.getValue() == configurator) {
+ final OnLoudnessCodecUpdateListener listener = e.getKey();
+ mConfiguratorListener.remove(listener);
+ mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
+ break;
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt b/media/java/android/media/LoudnessCodecFormat.aidl
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
copy to media/java/android/media/LoudnessCodecFormat.aidl
index 039078e..75c9060 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
+++ b/media/java/android/media/LoudnessCodecFormat.aidl
@@ -12,20 +12,19 @@
* 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.communal.ui.view
+package android.media;
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.LinearLayout
-import com.android.systemui.res.R
-/** Wraps around a widget rendered in communal mode. */
-class CommunalWidgetWrapper(context: Context, attrs: AttributeSet? = null) :
- LinearLayout(context, attrs) {
- init {
- id = R.id.communal_widget_wrapper
- }
-}
+/**
+ * Loudness format which specifies the input attributes used for measuring
+ * the parameters required to perform loudness alignment as specified by the
+ * CTA2075 standard.
+ *
+ * {@hide}
+ */
+parcelable LoudnessCodecFormat {
+ String metadataType;
+ boolean isDownmixing;
+}
\ No newline at end of file
diff --git a/nfc/Android.bp b/nfc/Android.bp
new file mode 100644
index 0000000..bf9f47c
--- /dev/null
+++ b/nfc/Android.bp
@@ -0,0 +1,51 @@
+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"],
+}
+
+filegroup {
+ name: "framework-nfc-non-updatable-sources",
+ path: "java",
+ srcs: [],
+}
+
+filegroup {
+ name: "framework-nfc-updatable-sources",
+ path: "java",
+ srcs: [
+ "java/**/*.java",
+ "java/**/*.aidl",
+ ],
+ exclude_srcs: [
+ ":framework-nfc-non-updatable-sources",
+ ],
+}
+
+java_sdk_library {
+ name: "framework-nfc",
+ libs: [
+ "unsupportedappusage", // for android.compat.annotation.UnsupportedAppUsage
+ ],
+ srcs: [
+ ":framework-nfc-updatable-sources",
+ ],
+ defaults: ["framework-non-updatable-unbundled-defaults"],
+ permitted_packages: [
+ "android.nfc",
+ "com.android.nfc",
+ ],
+ hidden_api_packages: [
+ "com.android.nfc",
+ ],
+ aidl: {
+ include_dirs: [
+ // TODO (b/303286040): Remove these when we change to |framework-module-defaults|
+ "frameworks/base/nfc/java",
+ "frameworks/base/core/java",
+ ],
+ },
+}
diff --git a/nfc/OWNERS b/nfc/OWNERS
new file mode 100644
index 0000000..35e9713
--- /dev/null
+++ b/nfc/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 48448
+include platform/packages/apps/Nfc:/OWNERS
diff --git a/nfc/TEST_MAPPING b/nfc/TEST_MAPPING
new file mode 100644
index 0000000..5b5ea37
--- /dev/null
+++ b/nfc/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+ "presubmit": [
+ {
+ "name": "NfcManagerTests"
+ },
+ {
+ "name": "CtsNfcTestCases"
+ }
+ ]
+}
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/nfc/api/module-lib-current.txt b/nfc/api/module-lib-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/module-lib-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/nfc/api/module-lib-removed.txt b/nfc/api/module-lib-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/module-lib-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/nfc/api/removed.txt b/nfc/api/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/system-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/nfc/api/system-removed.txt b/nfc/api/system-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/system-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/nfc/api/test-current.txt b/nfc/api/test-current.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/test-current.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/nfc/api/test-removed.txt b/nfc/api/test-removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/nfc/api/test-removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt b/nfc/java/android/nfc/Placeholder.java
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
copy to nfc/java/android/nfc/Placeholder.java
index 039078e..3509644 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
+++ b/nfc/java/android/nfc/Placeholder.java
@@ -12,20 +12,16 @@
* 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.communal.ui.view
+package android.nfc;
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.LinearLayout
-import com.android.systemui.res.R
-
-/** Wraps around a widget rendered in communal mode. */
-class CommunalWidgetWrapper(context: Context, attrs: AttributeSet? = null) :
- LinearLayout(context, attrs) {
- init {
- id = R.id.communal_widget_wrapper
- }
+/**
+ * Placeholder class so new framework-nfc module isn't empty, will be removed once module is
+ * populated.
+ *
+ * @hide
+ *
+ */
+public class Placeholder {
}
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index b845c2b..6e47689 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -43,6 +43,14 @@
</intent-filter>
</receiver>
+ <receiver android:name="v2.model.TemporaryFileManager"
+ android:exported="false"
+ android:enabled="false">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
<activity android:name=".v2.ui.InstallLaunch"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/Theme.AlertDialogActivity"
@@ -101,6 +109,15 @@
</intent-filter>
</receiver>
+ <receiver android:name=".v2.model.InstallEventReceiver"
+ android:permission="android.permission.INSTALL_PACKAGES"
+ android:exported="false"
+ android:enabled="false">
+ <intent-filter android:priority="1">
+ <action android:name="com.android.packageinstaller.ACTION_INSTALL_COMMIT" />
+ </intent-filter>
+ </receiver>
+
<activity android:name=".InstallSuccess"
android:theme="@style/Theme.AlertDialogActivity.NoAnimation"
android:exported="false" />
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/EventResultPersister.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/EventResultPersister.java
new file mode 100644
index 0000000..4d2d911
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/EventResultPersister.java
@@ -0,0 +1,378 @@
+/*
+ * 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.packageinstaller.v2.model;
+
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInstaller;
+import android.os.AsyncTask;
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.Xml;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * Persists results of events and calls back observers when a matching result arrives.
+ */
+public class EventResultPersister {
+
+ /**
+ * Id passed to {@link #addObserver(int, EventResultObserver)} to generate new id
+ */
+ public static final int GENERATE_NEW_ID = Integer.MIN_VALUE;
+ /**
+ * The extra with the id to set in the intent delivered to
+ * {@link #onEventReceived(Context, Intent)}
+ */
+ public static final String EXTRA_ID = "EventResultPersister.EXTRA_ID";
+ public static final String EXTRA_SERVICE_ID = "EventResultPersister.EXTRA_SERVICE_ID";
+ private static final String TAG = EventResultPersister.class.getSimpleName();
+ /**
+ * Persisted state of this object
+ */
+ private final AtomicFile mResultsFile;
+
+ private final Object mLock = new Object();
+
+ /**
+ * Currently stored but not yet called back results (install id -> status, status message)
+ */
+ private final SparseArray<EventResult> mResults = new SparseArray<>();
+
+ /**
+ * Currently registered, not called back observers (install id -> observer)
+ */
+ private final SparseArray<EventResultObserver> mObservers = new SparseArray<>();
+
+ /**
+ * Always increasing counter for install event ids
+ */
+ private int mCounter;
+
+ /**
+ * If a write that will persist the state is scheduled
+ */
+ private boolean mIsPersistScheduled;
+
+ /**
+ * If the state was changed while the data was being persisted
+ */
+ private boolean mIsPersistingStateValid;
+
+ /**
+ * Read persisted state.
+ *
+ * @param resultFile The file the results are persisted in
+ */
+ EventResultPersister(@NonNull File resultFile) {
+ mResultsFile = new AtomicFile(resultFile);
+ mCounter = GENERATE_NEW_ID + 1;
+
+ try (FileInputStream stream = mResultsFile.openRead()) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(stream, StandardCharsets.UTF_8.name());
+
+ nextElement(parser);
+ while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
+ String tagName = parser.getName();
+ if ("results".equals(tagName)) {
+ mCounter = readIntAttribute(parser, "counter");
+ } else if ("result".equals(tagName)) {
+ int id = readIntAttribute(parser, "id");
+ int status = readIntAttribute(parser, "status");
+ int legacyStatus = readIntAttribute(parser, "legacyStatus");
+ String statusMessage = readStringAttribute(parser, "statusMessage");
+ int serviceId = readIntAttribute(parser, "serviceId");
+
+ if (mResults.get(id) != null) {
+ throw new Exception("id " + id + " has two results");
+ }
+
+ mResults.put(id, new EventResult(status, legacyStatus, statusMessage,
+ serviceId));
+ } else {
+ throw new Exception("unexpected tag");
+ }
+
+ nextElement(parser);
+ }
+ } catch (Exception e) {
+ mResults.clear();
+ writeState();
+ }
+ }
+
+ /**
+ * Progress parser to the next element.
+ *
+ * @param parser The parser to progress
+ */
+ private static void nextElement(@NonNull XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ int type;
+ do {
+ type = parser.next();
+ } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
+ }
+
+ /**
+ * Read an int attribute from the current element
+ *
+ * @param parser The parser to read from
+ * @param name The attribute name to read
+ * @return The value of the attribute
+ */
+ private static int readIntAttribute(@NonNull XmlPullParser parser, @NonNull String name) {
+ return Integer.parseInt(parser.getAttributeValue(null, name));
+ }
+
+ /**
+ * Read an String attribute from the current element
+ *
+ * @param parser The parser to read from
+ * @param name The attribute name to read
+ * @return The value of the attribute or null if the attribute is not set
+ */
+ private static String readStringAttribute(@NonNull XmlPullParser parser, @NonNull String name) {
+ return parser.getAttributeValue(null, name);
+ }
+
+ /**
+ * @return a new event id.
+ */
+ public int getNewId() throws OutOfIdsException {
+ synchronized (mLock) {
+ if (mCounter == Integer.MAX_VALUE) {
+ throw new OutOfIdsException();
+ }
+
+ mCounter++;
+ writeState();
+
+ return mCounter - 1;
+ }
+ }
+
+ /**
+ * Add a result. If the result is a pending user action, execute the pending user action
+ * directly and do not queue a result.
+ *
+ * @param context The context the event was received in
+ * @param intent The intent the activity received
+ */
+ void onEventReceived(@NonNull Context context, @NonNull Intent intent) {
+ int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 0);
+
+ if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
+ Intent intentToStart = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class);
+ intentToStart.addFlags(FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(intentToStart);
+
+ return;
+ }
+
+ int id = intent.getIntExtra(EXTRA_ID, 0);
+ String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
+ int legacyStatus = intent.getIntExtra(PackageInstaller.EXTRA_LEGACY_STATUS, 0);
+ int serviceId = intent.getIntExtra(EXTRA_SERVICE_ID, 0);
+
+ EventResultObserver observerToCall = null;
+ synchronized (mLock) {
+ int numObservers = mObservers.size();
+ for (int i = 0; i < numObservers; i++) {
+ if (mObservers.keyAt(i) == id) {
+ observerToCall = mObservers.valueAt(i);
+ mObservers.removeAt(i);
+
+ break;
+ }
+ }
+
+ if (observerToCall != null) {
+ observerToCall.onResult(status, legacyStatus, statusMessage, serviceId);
+ } else {
+ mResults.put(id, new EventResult(status, legacyStatus, statusMessage, serviceId));
+ writeState();
+ }
+ }
+ }
+
+ /**
+ * Persist current state. The persistence might be delayed.
+ */
+ private void writeState() {
+ synchronized (mLock) {
+ mIsPersistingStateValid = false;
+
+ if (!mIsPersistScheduled) {
+ mIsPersistScheduled = true;
+
+ AsyncTask.execute(() -> {
+ int counter;
+ SparseArray<EventResult> results;
+
+ while (true) {
+ // Take snapshot of state
+ synchronized (mLock) {
+ counter = mCounter;
+ results = mResults.clone();
+ mIsPersistingStateValid = true;
+ }
+
+ try (FileOutputStream stream = mResultsFile.startWrite()) {
+ try {
+ XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(stream, StandardCharsets.UTF_8.name());
+ serializer.startDocument(null, true);
+ serializer.setFeature(
+ "http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ serializer.startTag(null, "results");
+ serializer.attribute(null, "counter", Integer.toString(counter));
+
+ int numResults = results.size();
+ for (int i = 0; i < numResults; i++) {
+ serializer.startTag(null, "result");
+ serializer.attribute(null, "id",
+ Integer.toString(results.keyAt(i)));
+ serializer.attribute(null, "status",
+ Integer.toString(results.valueAt(i).status));
+ serializer.attribute(null, "legacyStatus",
+ Integer.toString(results.valueAt(i).legacyStatus));
+ if (results.valueAt(i).message != null) {
+ serializer.attribute(null, "statusMessage",
+ results.valueAt(i).message);
+ }
+ serializer.attribute(null, "serviceId",
+ Integer.toString(results.valueAt(i).serviceId));
+ serializer.endTag(null, "result");
+ }
+
+ serializer.endTag(null, "results");
+ serializer.endDocument();
+
+ mResultsFile.finishWrite(stream);
+ } catch (IOException e) {
+ Log.e(TAG, "error writing results", e);
+ mResultsFile.failWrite(stream);
+ mResultsFile.delete();
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "error writing results", e);
+ mResultsFile.delete();
+ }
+
+ // Check if there was changed state since we persisted. If so, we need to
+ // persist again.
+ synchronized (mLock) {
+ if (mIsPersistingStateValid) {
+ mIsPersistScheduled = false;
+ break;
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Add an observer. If there is already an event for this id, call back inside of this call.
+ *
+ * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one.
+ * @param observer The observer to call back.
+ * @return The id for this event
+ */
+ int addObserver(int id, @NonNull EventResultObserver observer)
+ throws OutOfIdsException {
+ synchronized (mLock) {
+ int resultIndex = -1;
+
+ if (id == GENERATE_NEW_ID) {
+ id = getNewId();
+ } else {
+ resultIndex = mResults.indexOfKey(id);
+ }
+
+ // Check if we can instantly call back
+ if (resultIndex >= 0) {
+ EventResult result = mResults.valueAt(resultIndex);
+
+ observer.onResult(result.status, result.legacyStatus, result.message,
+ result.serviceId);
+ mResults.removeAt(resultIndex);
+ writeState();
+ } else {
+ mObservers.put(id, observer);
+ }
+ }
+
+ return id;
+ }
+
+ /**
+ * Remove a observer.
+ *
+ * @param id The id the observer was added for
+ */
+ void removeObserver(int id) {
+ synchronized (mLock) {
+ mObservers.delete(id);
+ }
+ }
+
+ /**
+ * Call back when a result is received. Observer is removed when onResult it called.
+ */
+ public interface EventResultObserver {
+
+ void onResult(int status, int legacyStatus, @Nullable String message, int serviceId);
+ }
+
+ /**
+ * The status from an event.
+ */
+ private static class EventResult {
+
+ public final int status;
+ public final int legacyStatus;
+ @Nullable
+ public final String message;
+ public final int serviceId;
+
+ private EventResult(int status, int legacyStatus, @Nullable String message, int serviceId) {
+ this.status = status;
+ this.legacyStatus = legacyStatus;
+ this.message = message;
+ this.serviceId = serviceId;
+ }
+ }
+
+ public static class OutOfIdsException extends Exception {
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallEventReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallEventReceiver.java
new file mode 100644
index 0000000..bcb11c8
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallEventReceiver.java
@@ -0,0 +1,77 @@
+/*
+ * 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.packageinstaller.v2.model;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+
+/**
+ * Receives install events and perists them using a {@link EventResultPersister}.
+ */
+public class InstallEventReceiver extends BroadcastReceiver {
+
+ private static final Object sLock = new Object();
+ private static EventResultPersister sReceiver;
+
+ /**
+ * Get the event receiver persisting the results
+ *
+ * @return The event receiver.
+ */
+ @NonNull
+ private static EventResultPersister getReceiver(@NonNull Context context) {
+ synchronized (sLock) {
+ if (sReceiver == null) {
+ sReceiver = new EventResultPersister(
+ TemporaryFileManager.getInstallStateFile(context));
+ }
+ }
+
+ return sReceiver;
+ }
+
+ /**
+ * Add an observer. If there is already an event for this id, call back inside of this call.
+ *
+ * @param context A context of the current app
+ * @param id The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one.
+ * @param observer The observer to call back.
+ * @return The id for this event
+ */
+ static int addObserver(@NonNull Context context, int id,
+ @NonNull EventResultPersister.EventResultObserver observer)
+ throws EventResultPersister.OutOfIdsException {
+ return getReceiver(context).addObserver(id, observer);
+ }
+
+ /**
+ * Remove a observer.
+ *
+ * @param context A context of the current app
+ * @param id The id the observer was added for
+ */
+ static void removeObserver(@NonNull Context context, int id) {
+ getReceiver(context).removeObserver(id);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ getReceiver(context).onEventReceived(context, intent);
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
index 03af951..7e7071f 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.java
@@ -17,26 +17,42 @@
package com.android.packageinstaller.v2.model;
import static com.android.packageinstaller.v2.model.PackageUtil.canPackageQuery;
+import static com.android.packageinstaller.v2.model.PackageUtil.generateStubPackageInfo;
+import static com.android.packageinstaller.v2.model.PackageUtil.getAppSnippet;
+import static com.android.packageinstaller.v2.model.PackageUtil.getPackageInfo;
+import static com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid;
import static com.android.packageinstaller.v2.model.PackageUtil.isCallerSessionOwner;
import static com.android.packageinstaller.v2.model.PackageUtil.isInstallPermissionGrantedOrRequested;
import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_DONE;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.DLG_PACKAGE_ERROR;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION;
+import static com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE;
import android.Manifest;
import android.app.Activity;
+import android.app.AppOpsManager;
+import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
+import android.content.pm.InstallSourceInfo;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.Process;
+import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.EventLog;
@@ -44,22 +60,34 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.EventResultPersister.OutOfIdsException;
+import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
+import com.android.packageinstaller.v2.model.installstagedata.InstallFailed;
+import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling;
import com.android.packageinstaller.v2.model.installstagedata.InstallReady;
import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
import com.android.packageinstaller.v2.model.installstagedata.InstallStaging;
+import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess;
+import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import java.io.File;
import java.io.IOException;
public class InstallRepository {
private static final String SCHEME_PACKAGE = "package";
+ private static final String BROADCAST_ACTION =
+ "com.android.packageinstaller.ACTION_INSTALL_COMMIT";
private static final String TAG = InstallRepository.class.getSimpleName();
private final Context mContext;
private final PackageManager mPackageManager;
private final PackageInstaller mPackageInstaller;
private final UserManager mUserManager;
private final DevicePolicyManager mDevicePolicyManager;
+ private final AppOpsManager mAppOpsManager;
private final MutableLiveData<InstallStage> mStagingResult = new MutableLiveData<>();
+ private final MutableLiveData<InstallStage> mInstallResult = new MutableLiveData<>();
private final boolean mLocalLOGV = false;
private Intent mIntent;
private boolean mIsSessionInstall;
@@ -75,6 +103,12 @@
private int mCallingUid;
private String mCallingPackage;
private SessionStager mSessionStager;
+ private AppOpRequestInfo mAppOpRequestInfo;
+ private AppSnippet mAppSnippet;
+ /**
+ * PackageInfo of the app being installed on device.
+ */
+ private PackageInfo mNewPackageInfo;
public InstallRepository(Context context) {
mContext = context;
@@ -82,6 +116,7 @@
mPackageInstaller = mPackageManager.getPackageInstaller();
mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
mUserManager = context.getSystemService(UserManager.class);
+ mAppOpsManager = context.getSystemService(AppOpsManager.class);
}
/**
@@ -124,6 +159,9 @@
final ApplicationInfo sourceInfo = getSourceInfo(mCallingPackage);
// Uid of the source package, with a preference to uid from ApplicationInfo
final int originatingUid = sourceInfo != null ? sourceInfo.uid : mCallingUid;
+ mAppOpRequestInfo = new AppOpRequestInfo(
+ getPackageNameForUid(mContext, originatingUid, mCallingPackage),
+ originatingUid, callingAttributionTag);
if (mCallingUid == Process.INVALID_UID && sourceInfo == null) {
// Caller's identity could not be determined. Abort the install
@@ -337,6 +375,466 @@
return params;
}
+ /**
+ * Processes Install session, file:// or package:// URI to generate data pertaining to user
+ * confirmation for an install. This method also checks if the source app has the AppOp granted
+ * to install unknown apps. If an AppOp is to be requested, cache the user action prompt data to
+ * be reused once appOp has been granted
+ *
+ * @return <ul>
+ * <li>InstallAborted </li>
+ * <ul>
+ * <li> If install session is invalid (not sealed or resolvedBaseApk path
+ * is invalid) </li>
+ * <li> Source app doesn't have visibility to target app </li>
+ * <li> The APK is invalid </li>
+ * <li> URI is invalid </li>
+ * <li> Can't get ApplicationInfo for source app, to request AppOp </li>
+ * </ul>
+ * <li> InstallUserActionRequired</li>
+ * <ul>
+ * <li> If AppOP is granted and user action is required to proceed
+ * with install </li>
+ * <li> If AppOp grant is to be requested from the user</li>
+ * </ul>
+ * </ul>
+ */
+ public InstallStage requestUserConfirmation() {
+ if (mIsTrustedSource) {
+ if (mLocalLOGV) {
+ Log.i(TAG, "install allowed");
+ }
+ // Returns InstallUserActionRequired stage if install details could be successfully
+ // computed, else it returns InstallAborted.
+ return generateConfirmationSnippet();
+ } else {
+ InstallStage unknownSourceStage = handleUnknownSources(mAppOpRequestInfo);
+ if (unknownSourceStage.getStageCode() == InstallStage.STAGE_READY) {
+ // Source app already has appOp granted.
+ return generateConfirmationSnippet();
+ } else {
+ return unknownSourceStage;
+ }
+ }
+ }
+
+
+ private InstallStage generateConfirmationSnippet() {
+ final Object packageSource;
+ int pendingUserActionReason = -1;
+ if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(mIntent.getAction())) {
+ final SessionInfo info = mPackageInstaller.getSessionInfo(mSessionId);
+ String resolvedPath = info != null ? info.getResolvedBaseApkPath() : null;
+
+ if (info == null || !info.isSealed() || resolvedPath == null) {
+ Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+ packageSource = Uri.fromFile(new File(resolvedPath));
+ // TODO: Not sure where is this used yet. PIA.java passes it to
+ // InstallInstalling if not null
+ // mOriginatingURI = null;
+ // mReferrerURI = null;
+ pendingUserActionReason = info.getPendingUserActionReason();
+ } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(mIntent.getAction())) {
+ final SessionInfo info = mPackageInstaller.getSessionInfo(mSessionId);
+
+ if (info == null || !info.isPreApprovalRequested()) {
+ Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+ packageSource = info;
+ // mOriginatingURI = null;
+ // mReferrerURI = null;
+ pendingUserActionReason = info.getPendingUserActionReason();
+ } else {
+ // Two possible origins:
+ // 1. Installation with SCHEME_PACKAGE.
+ // 2. Installation with "file://" for session created by this app
+ if (mIntent.getData() != null && mIntent.getData().getScheme().equals(SCHEME_PACKAGE)) {
+ packageSource = mIntent.getData();
+ } else {
+ SessionInfo stagedSessionInfo = mPackageInstaller.getSessionInfo(mStagedSessionId);
+ packageSource = Uri.fromFile(new File(stagedSessionInfo.getResolvedBaseApkPath()));
+ }
+ // mOriginatingURI = mIntent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
+ // mReferrerURI = mIntent.getParcelableExtra(Intent.EXTRA_REFERRER);
+ pendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE;
+ }
+
+ // if there's nothing to do, quietly slip into the ether
+ if (packageSource == null) {
+ Log.w(TAG, "Unspecified source");
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+ .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_URI))
+ .setActivityResultCode(Activity.RESULT_FIRST_USER)
+ .build();
+ }
+
+ return processAppSnippet(packageSource, pendingUserActionReason);
+ }
+
+ /**
+ * Parse the Uri (post-commit install session) or use the SessionInfo (pre-commit install
+ * session) to set up the installer for this install.
+ *
+ * @param source The source of package URI or SessionInfo
+ * @return {@code true} iff the installer could be set up
+ */
+ private InstallStage processAppSnippet(Object source, int userActionReason) {
+ if (source instanceof Uri) {
+ return processPackageUri((Uri) source, userActionReason);
+ } else if (source instanceof SessionInfo) {
+ return processSessionInfo((SessionInfo) source, userActionReason);
+ }
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+
+ /**
+ * Parse the Uri and set up the installer for this package.
+ *
+ * @param packageUri The URI to parse
+ * @return {@code true} iff the installer could be set up
+ */
+ private InstallStage processPackageUri(final Uri packageUri, int userActionReason) {
+ final String scheme = packageUri.getScheme();
+ final String packageName = packageUri.getSchemeSpecificPart();
+
+ if (scheme == null) {
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+
+ if (mLocalLOGV) {
+ Log.i(TAG, "processPackageUri(): uri = " + packageUri + ", scheme = " + scheme);
+ }
+
+ switch (scheme) {
+ case SCHEME_PACKAGE -> {
+ for (UserHandle handle : mUserManager.getUserHandles(true)) {
+ PackageManager pmForUser = mContext.createContextAsUser(handle, 0)
+ .getPackageManager();
+ try {
+ if (pmForUser.canPackageQuery(mCallingPackage, packageName)) {
+ mNewPackageInfo = pmForUser.getPackageInfo(packageName,
+ PackageManager.GET_PERMISSIONS
+ | PackageManager.MATCH_UNINSTALLED_PACKAGES);
+ }
+ } catch (NameNotFoundException ignored) {
+ }
+ }
+ if (mNewPackageInfo == null) {
+ Log.w(TAG, "Requested package " + packageUri.getSchemeSpecificPart()
+ + " not available. Discontinuing installation");
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+ .setErrorDialogType(DLG_PACKAGE_ERROR)
+ .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_APK))
+ .setActivityResultCode(Activity.RESULT_FIRST_USER)
+ .build();
+ }
+ mAppSnippet = getAppSnippet(mContext, mNewPackageInfo);
+ if (mLocalLOGV) {
+ Log.i(TAG, "Created snippet for " + mAppSnippet.getLabel());
+ }
+ }
+ case ContentResolver.SCHEME_FILE -> {
+ File sourceFile = new File(packageUri.getPath());
+ mNewPackageInfo = getPackageInfo(mContext, sourceFile,
+ PackageManager.GET_PERMISSIONS);
+
+ // Check for parse errors
+ if (mNewPackageInfo == null) {
+ Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+ .setErrorDialogType(DLG_PACKAGE_ERROR)
+ .setResultIntent(new Intent().putExtra(Intent.EXTRA_INSTALL_RESULT,
+ PackageManager.INSTALL_FAILED_INVALID_APK))
+ .setActivityResultCode(Activity.RESULT_FIRST_USER)
+ .build();
+ }
+ if (mLocalLOGV) {
+ Log.i(TAG, "Creating snippet for local file " + sourceFile);
+ }
+ mAppSnippet = getAppSnippet(mContext, mNewPackageInfo.applicationInfo, sourceFile);
+ }
+ default -> {
+ Log.e(TAG, "Unexpected URI scheme " + packageUri);
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+ }
+
+ return new InstallUserActionRequired.Builder(
+ USER_ACTION_REASON_INSTALL_CONFIRMATION, mAppSnippet)
+ .setDialogMessage(getUpdateMessage(mNewPackageInfo, userActionReason))
+ .setAppUpdating(isAppUpdating(mNewPackageInfo))
+ .build();
+ }
+
+ /**
+ * Use the SessionInfo and set up the installer for pre-commit install session.
+ *
+ * @param sessionInfo The SessionInfo to compose
+ * @return {@code true} iff the installer could be set up
+ */
+ private InstallStage processSessionInfo(@NonNull SessionInfo sessionInfo,
+ int userActionReason) {
+ mNewPackageInfo = generateStubPackageInfo(sessionInfo.getAppPackageName());
+
+ mAppSnippet = getAppSnippet(mContext, sessionInfo);
+ return new InstallUserActionRequired.Builder(
+ USER_ACTION_REASON_INSTALL_CONFIRMATION, mAppSnippet)
+ .setAppUpdating(isAppUpdating(mNewPackageInfo))
+ .setDialogMessage(getUpdateMessage(mNewPackageInfo, userActionReason))
+ .build();
+ }
+
+ private String getUpdateMessage(PackageInfo pkgInfo, int userActionReason) {
+ if (isAppUpdating(pkgInfo)) {
+ final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(pkgInfo);
+ final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mCallingPackage);
+
+ if (!TextUtils.isEmpty(existingUpdateOwnerLabel)
+ && userActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP) {
+ return mContext.getString(R.string.install_confirm_question_update_owner_reminder,
+ requestedUpdateOwnerLabel, existingUpdateOwnerLabel);
+ }
+ }
+ return null;
+ }
+
+ private CharSequence getExistingUpdateOwnerLabel(PackageInfo pkgInfo) {
+ try {
+ final String packageName = pkgInfo.packageName;
+ final InstallSourceInfo sourceInfo = mPackageManager.getInstallSourceInfo(packageName);
+ final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName();
+ return getApplicationLabel(existingUpdateOwner);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private CharSequence getApplicationLabel(String packageName) {
+ try {
+ final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName,
+ ApplicationInfoFlags.of(0));
+ return mPackageManager.getApplicationLabel(appInfo);
+ } catch (NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ private boolean isAppUpdating(PackageInfo newPkgInfo) {
+ String pkgName = newPkgInfo.packageName;
+ // Check if there is already a package on the device with this name
+ // but it has been renamed to something else.
+ String[] oldName = mPackageManager.canonicalToCurrentPackageNames(new String[]{pkgName});
+ if (oldName != null && oldName.length > 0 && oldName[0] != null) {
+ pkgName = oldName[0];
+ newPkgInfo.packageName = pkgName;
+ newPkgInfo.applicationInfo.packageName = pkgName;
+ }
+ // Check if package is already installed. display confirmation dialog if replacing pkg
+ try {
+ // This is a little convoluted because we want to get all uninstalled
+ // apps, but this may include apps with just data, and if it is just
+ // data we still want to count it as "installed".
+ ApplicationInfo appInfo = mPackageManager.getApplicationInfo(pkgName,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES);
+ if ((appInfo.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
+ return false;
+ }
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Once the user returns from Settings related to installing from unknown sources, reattempt
+ * the installation if the source app is granted permission to install other apps. Abort the
+ * installation if the source app is still not granted installing permission.
+ * @return {@link InstallUserActionRequired} containing data required to ask user confirmation
+ * to proceed with the install.
+ * {@link InstallAborted} if there was an error while recomputing, or the source still
+ * doesn't have install permission.
+ */
+ public InstallStage reattemptInstall() {
+ InstallStage unknownSourceStage = handleUnknownSources(mAppOpRequestInfo);
+ if (unknownSourceStage.getStageCode() == InstallStage.STAGE_READY) {
+ // Source app now has appOp granted.
+ return generateConfirmationSnippet();
+ } else if (unknownSourceStage.getStageCode() == InstallStage.STAGE_ABORTED) {
+ // There was some error in determining the AppOp code for the source app.
+ // Abort installation
+ return unknownSourceStage;
+ } else {
+ // AppOpsManager again returned a MODE_ERRORED or MODE_DEFAULT op code. This was
+ // unexpected while reattempting the install. Let's abort it.
+ Log.e(TAG, "AppOp still not granted.");
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+ }
+
+ private InstallStage handleUnknownSources(AppOpRequestInfo requestInfo) {
+ if (requestInfo.getCallingPackage() == null) {
+ Log.i(TAG, "No source found for package " + mNewPackageInfo.packageName);
+ return new InstallUserActionRequired.Builder(
+ USER_ACTION_REASON_ANONYMOUS_SOURCE, null)
+ .build();
+ }
+ // Shouldn't use static constant directly, see b/65534401.
+ final String appOpStr =
+ AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES);
+ final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr,
+ requestInfo.getOriginatingUid(),
+ requestInfo.getCallingPackage(), requestInfo.getAttributionTag(),
+ "Started package installation activity");
+
+ if (mLocalLOGV) {
+ Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode);
+ }
+ switch (appOpMode) {
+ case AppOpsManager.MODE_DEFAULT:
+ mAppOpsManager.setMode(appOpStr, requestInfo.getOriginatingUid(),
+ requestInfo.getCallingPackage(), AppOpsManager.MODE_ERRORED);
+ // fall through
+ case AppOpsManager.MODE_ERRORED:
+ try {
+ ApplicationInfo sourceInfo =
+ mPackageManager.getApplicationInfo(requestInfo.getCallingPackage(), 0);
+ AppSnippet sourceAppSnippet = getAppSnippet(mContext, sourceInfo);
+ return new InstallUserActionRequired.Builder(
+ USER_ACTION_REASON_UNKNOWN_SOURCE, sourceAppSnippet)
+ .setDialogMessage(requestInfo.getCallingPackage())
+ .build();
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Did not find appInfo for " + requestInfo.getCallingPackage());
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+ case AppOpsManager.MODE_ALLOWED:
+ return new InstallReady();
+ default:
+ Log.e(TAG, "Invalid app op mode " + appOpMode
+ + " for OP_REQUEST_INSTALL_PACKAGES found for uid "
+ + requestInfo.getOriginatingUid());
+ return new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR).build();
+ }
+ }
+
+
+ /**
+ * Kick off the installation. Register a broadcast listener to get the result of the
+ * installation and commit the staged session here. If the installation was session based,
+ * signal the PackageInstaller that the user has granted permission to proceed with the install
+ */
+ public void initiateInstall() {
+ if (mSessionId > 0) {
+ mPackageInstaller.setPermissionsResult(mSessionId, true);
+ mInstallResult.setValue(new InstallAborted.Builder(ABORT_REASON_DONE)
+ .setActivityResultCode(Activity.RESULT_OK).build());
+ return;
+ }
+
+ Uri uri = mIntent.getData();
+ if (uri != null && SCHEME_PACKAGE.equals(uri.getScheme())) {
+ try {
+ mPackageManager.installExistingPackage(mNewPackageInfo.packageName);
+ setStageBasedOnResult(PackageInstaller.STATUS_SUCCESS, -1, null, -1);
+ } catch (PackageManager.NameNotFoundException e) {
+ setStageBasedOnResult(PackageInstaller.STATUS_FAILURE,
+ PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1);
+ }
+ return;
+ }
+
+ if (mStagedSessionId <= 0) {
+ // How did we even land here?
+ Log.e(TAG, "Invalid local session and caller initiated session");
+ mInstallResult.setValue(new InstallAborted.Builder(ABORT_REASON_INTERNAL_ERROR)
+ .build());
+ return;
+ }
+
+ int installId;
+ try {
+ mInstallResult.setValue(new InstallInstalling(mAppSnippet));
+ installId = InstallEventReceiver.addObserver(mContext,
+ EventResultPersister.GENERATE_NEW_ID, this::setStageBasedOnResult);
+ } catch (OutOfIdsException e) {
+ setStageBasedOnResult(PackageInstaller.STATUS_FAILURE,
+ PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1);
+ return;
+ }
+
+ Intent broadcastIntent = new Intent(BROADCAST_ACTION);
+ broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ broadcastIntent.setPackage(mContext.getPackageName());
+ broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, installId);
+
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ mContext, installId, broadcastIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
+
+ try {
+ PackageInstaller.Session session = mPackageInstaller.openSession(mStagedSessionId);
+ session.commit(pendingIntent.getIntentSender());
+ } catch (Exception e) {
+ Log.e(TAG, "Session " + mStagedSessionId + " could not be opened.", e);
+ mPackageInstaller.abandonSession(mStagedSessionId);
+ setStageBasedOnResult(PackageInstaller.STATUS_FAILURE,
+ PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null, -1);
+ }
+ }
+
+ private void setStageBasedOnResult(int statusCode, int legacyStatus, String message,
+ int serviceId) {
+ if (statusCode == PackageInstaller.STATUS_SUCCESS) {
+ boolean shouldReturnResult = mIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
+
+ InstallSuccess.Builder successBuilder = new InstallSuccess.Builder(mAppSnippet)
+ .setShouldReturnResult(shouldReturnResult);
+ Intent resultIntent;
+ if (shouldReturnResult) {
+ resultIntent = new Intent()
+ .putExtra(Intent.EXTRA_INSTALL_RESULT, PackageManager.INSTALL_SUCCEEDED);
+ } else {
+ resultIntent = mPackageManager
+ .getLaunchIntentForPackage(mNewPackageInfo.packageName);
+ }
+ successBuilder.setResultIntent(resultIntent);
+
+ mInstallResult.setValue(successBuilder.build());
+ } else {
+ mInstallResult.setValue(
+ new InstallFailed(mAppSnippet, statusCode, legacyStatus, message));
+ }
+ }
+
+ public MutableLiveData<InstallStage> getInstallResult() {
+ return mInstallResult;
+ }
+
+ /**
+ * Cleanup the staged session. Also signal the packageinstaller that an install session is to
+ * be aborted
+ */
+ public void cleanupInstall() {
+ if (mSessionId > 0) {
+ mPackageInstaller.setPermissionsResult(mSessionId, false);
+ } else if (mStagedSessionId > 0) {
+ cleanupStagingSession();
+ }
+ }
+
+ /**
+ * When the identity of the install source could not be determined, user can skip checking the
+ * source and directly proceed with the install.
+ */
+ public InstallStage forcedSkipSourceCheck() {
+ return generateConfirmationSnippet();
+ }
+
public MutableLiveData<Integer> getStagingProgress() {
if (mSessionStager != null) {
return mSessionStager.getProgress();
@@ -373,4 +871,29 @@
return mUid;
}
}
+
+ public static class AppOpRequestInfo {
+
+ private String mCallingPackage;
+ private String mAttributionTag;
+ private int mOrginatingUid;
+
+ public AppOpRequestInfo(String callingPackage, int orginatingUid, String attributionTag) {
+ mCallingPackage = callingPackage;
+ mOrginatingUid = orginatingUid;
+ mAttributionTag = attributionTag;
+ }
+
+ public String getCallingPackage() {
+ return mCallingPackage;
+ }
+
+ public String getAttributionTag() {
+ return mAttributionTag;
+ }
+
+ public int getOriginatingUid() {
+ return mOrginatingUid;
+ }
+ }
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
index 82a8c95..9c15fd5 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
@@ -24,17 +24,23 @@
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Process;
import android.util.Log;
import androidx.annotation.NonNull;
+import java.io.File;
import java.util.Arrays;
+import java.util.Objects;
public class PackageUtil {
private static final String TAG = InstallRepository.class.getSimpleName();
private static final String DOWNLOADS_AUTHORITY = "downloads";
+ private static final String SPLIT_BASE_APK_END_WITH = "base.apk";
/**
* Determines if the UID belongs to the system downloads provider and returns the
@@ -212,4 +218,228 @@
int installerUid = sessionInfo.getInstallerUid();
return originatingUid == installerUid;
}
+
+ /**
+ * Generates a stub {@link PackageInfo} object for the given packageName
+ */
+ public static PackageInfo generateStubPackageInfo(String packageName) {
+ final PackageInfo info = new PackageInfo();
+ final ApplicationInfo aInfo = new ApplicationInfo();
+ info.applicationInfo = aInfo;
+ info.packageName = info.applicationInfo.packageName = packageName;
+ return info;
+ }
+
+ /**
+ * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
+ * {@link SessionInfo} object
+ */
+ public static AppSnippet getAppSnippet(Context context, SessionInfo info) {
+ PackageManager pm = context.getPackageManager();
+ CharSequence label = info.getAppLabel();
+ Drawable icon = info.getAppIcon() != null ?
+ new BitmapDrawable(context.getResources(), info.getAppIcon())
+ : pm.getDefaultActivityIcon();
+ return new AppSnippet(label, icon);
+ }
+
+ /**
+ * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
+ * {@link PackageInfo} object
+ */
+ public static AppSnippet getAppSnippet(Context context, PackageInfo pkgInfo) {
+ return getAppSnippet(context, pkgInfo.applicationInfo);
+ }
+
+ /**
+ * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
+ * {@link ApplicationInfo} object
+ */
+ public static AppSnippet getAppSnippet(Context context, ApplicationInfo appInfo) {
+ PackageManager pm = context.getPackageManager();
+ CharSequence label = pm.getApplicationLabel(appInfo);
+ Drawable icon = pm.getApplicationIcon(appInfo);
+ return new AppSnippet(label, icon);
+ }
+
+ /**
+ * Generates an {@link AppSnippet} containing an appIcon and appLabel from the
+ * supplied APK file
+ */
+ public static AppSnippet getAppSnippet(Context context, ApplicationInfo appInfo,
+ File sourceFile) {
+ ApplicationInfo appInfoFromFile = processAppInfoForFile(appInfo, sourceFile);
+ CharSequence label = getAppLabelFromFile(context, appInfoFromFile);
+ Drawable icon = getAppIconFromFile(context, appInfoFromFile);
+ return new AppSnippet(label, icon);
+ }
+
+ /**
+ * Utility method to load application label
+ *
+ * @param context context of package that can load the resources
+ * @param appInfo ApplicationInfo object of package whose resources are to be loaded
+ */
+ public static CharSequence getAppLabelFromFile(Context context, ApplicationInfo appInfo) {
+ PackageManager pm = context.getPackageManager();
+ CharSequence label = null;
+ // Try to load the label from the package's resources. If an app has not explicitly
+ // specified any label, just use the package name.
+ if (appInfo.labelRes != 0) {
+ try {
+ label = appInfo.loadLabel(pm);
+ } catch (Resources.NotFoundException e) {
+ }
+ }
+ if (label == null) {
+ label = (appInfo.nonLocalizedLabel != null) ?
+ appInfo.nonLocalizedLabel : appInfo.packageName;
+ }
+ return label;
+ }
+
+ /**
+ * Utility method to load application icon
+ *
+ * @param context context of package that can load the resources
+ * @param appInfo ApplicationInfo object of package whose resources are to be loaded
+ */
+ public static Drawable getAppIconFromFile(Context context, ApplicationInfo appInfo) {
+ PackageManager pm = context.getPackageManager();
+ Drawable icon = null;
+ // Try to load the icon from the package's resources. If an app has not explicitly
+ // specified any resource, just use the default icon for now.
+ try {
+ if (appInfo.icon != 0) {
+ try {
+ icon = appInfo.loadIcon(pm);
+ } catch (Resources.NotFoundException e) {
+ }
+ }
+ if (icon == null) {
+ icon = context.getPackageManager().getDefaultActivityIcon();
+ }
+ } catch (OutOfMemoryError e) {
+ Log.i(TAG, "Could not load app icon", e);
+ }
+ return icon;
+ }
+
+ private static ApplicationInfo processAppInfoForFile(ApplicationInfo appInfo, File sourceFile) {
+ final String archiveFilePath = sourceFile.getAbsolutePath();
+ appInfo.publicSourceDir = archiveFilePath;
+
+ if (appInfo.splitNames != null && appInfo.splitSourceDirs == null) {
+ final File[] files = sourceFile.getParentFile().listFiles();
+ final String[] splits = Arrays.stream(appInfo.splitNames)
+ .map(i -> findFilePath(files, i + ".apk"))
+ .filter(Objects::nonNull)
+ .toArray(String[]::new);
+
+ appInfo.splitSourceDirs = splits;
+ appInfo.splitPublicSourceDirs = splits;
+ }
+ return appInfo;
+ }
+
+ private static String findFilePath(File[] files, String postfix) {
+ for (File file : files) {
+ final String path = file.getAbsolutePath();
+ if (path.endsWith(postfix)) {
+ return path;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the packageName corresponding to a UID.
+ */
+ public static String getPackageNameForUid(Context context, int sourceUid,
+ String callingPackage) {
+ if (sourceUid == Process.INVALID_UID) {
+ return null;
+ }
+ // If the sourceUid belongs to the system downloads provider, we explicitly return the
+ // name of the Download Manager package. This is because its UID is shared with multiple
+ // packages, resulting in uncertainty about which package will end up first in the list
+ // of packages associated with this UID
+ PackageManager pm = context.getPackageManager();
+ ApplicationInfo systemDownloadProviderInfo = getSystemDownloadsProviderInfo(
+ pm, sourceUid);
+ if (systemDownloadProviderInfo != null) {
+ return systemDownloadProviderInfo.packageName;
+ }
+ String[] packagesForUid = pm.getPackagesForUid(sourceUid);
+ if (packagesForUid == null) {
+ return null;
+ }
+ if (packagesForUid.length > 1) {
+ if (callingPackage != null) {
+ for (String packageName : packagesForUid) {
+ if (packageName.equals(callingPackage)) {
+ return packageName;
+ }
+ }
+ }
+ Log.i(TAG, "Multiple packages found for source uid " + sourceUid);
+ }
+ return packagesForUid[0];
+ }
+
+ /**
+ * Utility method to get package information for a given {@link File}
+ */
+ public static PackageInfo getPackageInfo(Context context, File sourceFile, int flags) {
+ String filePath = sourceFile.getAbsolutePath();
+ if (filePath.endsWith(SPLIT_BASE_APK_END_WITH)) {
+ File dir = sourceFile.getParentFile();
+ if (dir.listFiles().length > 1) {
+ // split apks, use file directory to get archive info
+ filePath = dir.getPath();
+ }
+ }
+ try {
+ return context.getPackageManager().getPackageArchiveInfo(filePath, flags);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * The class to hold an incoming package's icon and label.
+ * See {@link #getAppSnippet(Context, SessionInfo)},
+ * {@link #getAppSnippet(Context, PackageInfo)},
+ * {@link #getAppSnippet(Context, ApplicationInfo)},
+ * {@link #getAppSnippet(Context, ApplicationInfo, File)}
+ */
+ public static class AppSnippet {
+
+ private CharSequence mLabel;
+ private Drawable mIcon;
+
+ public AppSnippet(CharSequence label, Drawable icon) {
+ mLabel = label;
+ mIcon = icon;
+ }
+
+ public AppSnippet() {
+ }
+
+ public CharSequence getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(CharSequence mLabel) {
+ this.mLabel = mLabel;
+ }
+
+ public Drawable getIcon() {
+ return mIcon;
+ }
+
+ public void setIcon(Drawable mIcon) {
+ this.mIcon = mIcon;
+ }
+ }
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/TemporaryFileManager.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/TemporaryFileManager.java
new file mode 100644
index 0000000..3a1c3973
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/TemporaryFileManager.java
@@ -0,0 +1,92 @@
+/*
+ * 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.packageinstaller.v2.model;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Manages files of the package installer and resets state during boot.
+ */
+public class TemporaryFileManager extends BroadcastReceiver {
+
+ private static final String LOG_TAG = TemporaryFileManager.class.getSimpleName();
+
+ /**
+ * Create a new file to hold a staged file.
+ *
+ * @param context The context of the caller
+ * @return A new file
+ */
+ @NonNull
+ public static File getStagedFile(@NonNull Context context) throws IOException {
+ return File.createTempFile("package", ".apk", context.getNoBackupFilesDir());
+ }
+
+ /**
+ * Get the file used to store the results of installs.
+ *
+ * @param context The context of the caller
+ * @return the file used to store the results of installs
+ */
+ @NonNull
+ public static File getInstallStateFile(@NonNull Context context) {
+ return new File(context.getNoBackupFilesDir(), "install_results.xml");
+ }
+
+ /**
+ * Get the file used to store the results of uninstalls.
+ *
+ * @param context The context of the caller
+ * @return the file used to store the results of uninstalls
+ */
+ @NonNull
+ public static File getUninstallStateFile(@NonNull Context context) {
+ return new File(context.getNoBackupFilesDir(), "uninstall_results.xml");
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ long systemBootTime = System.currentTimeMillis() - SystemClock.elapsedRealtime();
+
+ File[] filesOnBoot = context.getNoBackupFilesDir().listFiles();
+
+ if (filesOnBoot == null) {
+ return;
+ }
+
+ for (int i = 0; i < filesOnBoot.length; i++) {
+ File fileOnBoot = filesOnBoot[i];
+
+ if (systemBootTime > fileOnBoot.lastModified()) {
+ boolean wasDeleted = fileOnBoot.delete();
+ if (!wasDeleted) {
+ Log.w(LOG_TAG, "Could not delete " + fileOnBoot.getName() + " onBoot");
+ }
+ } else {
+ Log.w(LOG_TAG, fileOnBoot.getName() + " was created before onBoot broadcast was "
+ + "received");
+ }
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
index cc9857d..520b6c5 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallAborted.java
@@ -26,6 +26,8 @@
public static final int ABORT_REASON_INTERNAL_ERROR = 0;
public static final int ABORT_REASON_POLICY = 1;
+ public static final int ABORT_REASON_DONE = 2;
+ public static final int DLG_PACKAGE_ERROR = 1;
private final int mStage = InstallStage.STAGE_ABORTED;
private final int mAbortReason;
@@ -46,13 +48,15 @@
*/
@Nullable
private final Intent mIntent;
+ private final int mErrorDialogType;
private final int mActivityResultCode;
private InstallAborted(int reason, @NonNull String message, @Nullable Intent intent,
- int activityResultCode) {
+ int activityResultCode, int errorDialogType) {
mAbortReason = reason;
mMessage = message;
mIntent = intent;
+ mErrorDialogType = errorDialogType;
mActivityResultCode = activityResultCode;
}
@@ -70,6 +74,10 @@
return mIntent;
}
+ public int getErrorDialogType() {
+ return mErrorDialogType;
+ }
+
public int getActivityResultCode() {
return mActivityResultCode;
}
@@ -85,6 +93,7 @@
private String mMessage = "";
private Intent mIntent = null;
private int mActivityResultCode = Activity.RESULT_CANCELED;
+ private int mErrorDialogType;
public Builder(int reason) {
mAbortReason = reason;
@@ -100,13 +109,19 @@
return this;
}
+ public Builder setErrorDialogType(int dialogType) {
+ mErrorDialogType = dialogType;
+ return this;
+ }
+
public Builder setActivityResultCode(int resultCode) {
mActivityResultCode = resultCode;
return this;
}
public InstallAborted build() {
- return new InstallAborted(mAbortReason, mMessage, mIntent, mActivityResultCode);
+ return new InstallAborted(mAbortReason, mMessage, mIntent, mActivityResultCode,
+ mErrorDialogType);
}
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java
new file mode 100644
index 0000000..67e1690
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallFailed.java
@@ -0,0 +1,69 @@
+/*
+ * 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.packageinstaller.v2.model.installstagedata;
+
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
+
+public class InstallFailed extends InstallStage {
+
+ private final int mStage = InstallStage.STAGE_FAILED;
+ @NonNull
+ private final AppSnippet mAppSnippet;
+ private final int mStatusCode;
+ private final int mLegacyCode;
+ @Nullable
+ private final String mMessage;
+
+ public InstallFailed(@NonNull AppSnippet appSnippet, int statusCode, int legacyCode,
+ @Nullable String message) {
+ mAppSnippet = appSnippet;
+ mLegacyCode = statusCode;
+ mStatusCode = legacyCode;
+ mMessage = message;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ @NonNull
+ public Drawable getAppIcon() {
+ return mAppSnippet.getIcon();
+ }
+
+ @NonNull
+ public String getAppLabel() {
+ return (String) mAppSnippet.getLabel();
+ }
+
+ public int getStatusCode() {
+ return mStatusCode;
+ }
+
+ public int getLegacyCode() {
+ return mLegacyCode;
+ }
+
+ @Nullable
+ public String getMessage() {
+ return mMessage;
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java
new file mode 100644
index 0000000..efd4947
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallInstalling.java
@@ -0,0 +1,47 @@
+/*
+ * 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.packageinstaller.v2.model.installstagedata;
+
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
+
+public class InstallInstalling extends InstallStage {
+
+ private final int mStage = InstallStage.STAGE_INSTALLING;
+ @NonNull
+ private final AppSnippet mAppSnippet;
+
+ public InstallInstalling(@NonNull AppSnippet appSnippet) {
+ mAppSnippet = appSnippet;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ @NonNull
+ public Drawable getAppIcon() {
+ return mAppSnippet.getIcon();
+ }
+
+ @NonNull
+ public String getAppLabel() {
+ return (String) mAppSnippet.getLabel();
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java
new file mode 100644
index 0000000..da48256
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallSuccess.java
@@ -0,0 +1,95 @@
+/*
+ * 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.packageinstaller.v2.model.installstagedata;
+
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
+
+public class InstallSuccess extends InstallStage {
+
+ private final int mStage = InstallStage.STAGE_SUCCESS;
+
+ @NonNull
+ private final AppSnippet mAppSnippet;
+ private final boolean mShouldReturnResult;
+ /**
+ * <p>If the caller is requesting a result back, this will hold the Intent with
+ * EXTRA_INSTALL_RESULT set to INSTALL_SUCCEEDED which is sent back to the caller.</p>
+ * <p>If the caller doesn't want the result back, this will hold the Intent that launches
+ * the newly installed / updated app.</p>
+ */
+ @NonNull
+ private final Intent mResultIntent;
+
+ public InstallSuccess(@NonNull AppSnippet appSnippet, boolean shouldReturnResult,
+ @NonNull Intent launcherIntent) {
+ mAppSnippet = appSnippet;
+ mShouldReturnResult = shouldReturnResult;
+ mResultIntent = launcherIntent;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ @NonNull
+ public Drawable getAppIcon() {
+ return mAppSnippet.getIcon();
+ }
+
+ @NonNull
+ public String getAppLabel() {
+ return (String) mAppSnippet.getLabel();
+ }
+
+ public boolean shouldReturnResult() {
+ return mShouldReturnResult;
+ }
+
+ @NonNull
+ public Intent getResultIntent() {
+ return mResultIntent;
+ }
+
+ public static class Builder {
+
+ private final AppSnippet mAppSnippet;
+ private boolean mShouldReturnResult;
+ private Intent mLauncherIntent;
+
+ public Builder(@NonNull AppSnippet appSnippet) {
+ mAppSnippet = appSnippet;
+ }
+
+ public Builder setShouldReturnResult(boolean returnResult) {
+ mShouldReturnResult = returnResult;
+ return this;
+ }
+
+ public Builder setResultIntent(@NonNull Intent intent) {
+ mLauncherIntent = intent;
+ return this;
+ }
+
+ public InstallSuccess build() {
+ return new InstallSuccess(mAppSnippet, mShouldReturnResult, mLauncherIntent);
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java
new file mode 100644
index 0000000..08a7487
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/installstagedata/InstallUserActionRequired.java
@@ -0,0 +1,99 @@
+/*
+ * 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.packageinstaller.v2.model.installstagedata;
+
+import android.graphics.drawable.Drawable;
+import androidx.annotation.Nullable;
+import com.android.packageinstaller.v2.model.PackageUtil.AppSnippet;
+
+public class InstallUserActionRequired extends InstallStage {
+
+ public static final int USER_ACTION_REASON_UNKNOWN_SOURCE = 0;
+ public static final int USER_ACTION_REASON_ANONYMOUS_SOURCE = 1;
+ public static final int USER_ACTION_REASON_INSTALL_CONFIRMATION = 2;
+ private final int mStage = InstallStage.STAGE_USER_ACTION_REQUIRED;
+ private final int mActionReason;
+ @Nullable
+ private final AppSnippet mAppSnippet;
+ private final boolean mIsAppUpdating;
+ @Nullable
+ private final String mDialogMessage;
+
+ public InstallUserActionRequired(int actionReason, @Nullable AppSnippet appSnippet,
+ boolean isUpdating, @Nullable String dialogMessage) {
+ mActionReason = actionReason;
+ mAppSnippet = appSnippet;
+ mIsAppUpdating = isUpdating;
+ mDialogMessage = dialogMessage;
+ }
+
+ @Override
+ public int getStageCode() {
+ return mStage;
+ }
+
+ @Nullable
+ public Drawable getAppIcon() {
+ return mAppSnippet != null ? mAppSnippet.getIcon() : null;
+ }
+
+ @Nullable
+ public String getAppLabel() {
+ return mAppSnippet != null ? (String) mAppSnippet.getLabel() : null;
+ }
+
+ public boolean isAppUpdating() {
+ return mIsAppUpdating;
+ }
+
+ @Nullable
+ public String getDialogMessage() {
+ return mDialogMessage;
+ }
+
+ public int getActionReason() {
+ return mActionReason;
+ }
+
+ public static class Builder {
+
+ private final int mActionReason;
+ private final AppSnippet mAppSnippet;
+ private boolean mIsAppUpdating;
+ private String mDialogMessage;
+
+ public Builder(int actionReason, @Nullable AppSnippet appSnippet) {
+ mActionReason = actionReason;
+ mAppSnippet = appSnippet;
+ }
+
+ public Builder setAppUpdating(boolean isUpdating) {
+ mIsAppUpdating = isUpdating;
+ return this;
+ }
+
+ public Builder setDialogMessage(@Nullable String message) {
+ mDialogMessage = message;
+ return this;
+ }
+
+ public InstallUserActionRequired build() {
+ return new InstallUserActionRequired(mActionReason, mAppSnippet, mIsAppUpdating,
+ mDialogMessage);
+ }
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java
new file mode 100644
index 0000000..fdb024f
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallActionListener.java
@@ -0,0 +1,38 @@
+/*
+ * 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.packageinstaller.v2.ui;
+
+import android.content.Intent;
+
+public interface InstallActionListener {
+
+ /**
+ * Method to handle a positive response from the user
+ */
+ void onPositiveResponse(int stageCode);
+
+ /**
+ * Method to dispatch intent for toggling "install from unknown sources" setting for a package
+ */
+ void sendUnknownAppsIntent(String packageName);
+
+ /**
+ * Method to handle a negative response from the user
+ */
+ void onNegativeResponse(int stageCode);
+ void openInstalledApp(Intent intent);
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
index ba5a0cd..9493555 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.java
@@ -16,13 +16,22 @@
package com.android.packageinstaller.v2.ui;
+import static android.content.Intent.CATEGORY_LAUNCHER;
+import static android.content.Intent.FLAG_ACTIVITY_NO_HISTORY;
import static android.os.Process.INVALID_UID;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_INTERNAL_ERROR;
import static com.android.packageinstaller.v2.model.installstagedata.InstallAborted.ABORT_REASON_POLICY;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.content.ActivityNotFoundException;
import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
import android.os.UserManager;
+import android.provider.Settings;
import android.util.Log;
import android.view.Window;
import androidx.annotation.Nullable;
@@ -34,13 +43,25 @@
import com.android.packageinstaller.v2.model.InstallRepository;
import com.android.packageinstaller.v2.model.InstallRepository.CallerInfo;
import com.android.packageinstaller.v2.model.installstagedata.InstallAborted;
+import com.android.packageinstaller.v2.model.installstagedata.InstallFailed;
+import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling;
import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess;
+import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.fragments.AnonymousSourceFragment;
+import com.android.packageinstaller.v2.ui.fragments.ExternalSourcesBlockedFragment;
+import com.android.packageinstaller.v2.ui.fragments.InstallConfirmationFragment;
+import com.android.packageinstaller.v2.ui.fragments.InstallFailedFragment;
+import com.android.packageinstaller.v2.ui.fragments.InstallInstallingFragment;
import com.android.packageinstaller.v2.ui.fragments.InstallStagingFragment;
+import com.android.packageinstaller.v2.ui.fragments.InstallSuccessFragment;
import com.android.packageinstaller.v2.ui.fragments.SimpleErrorFragment;
import com.android.packageinstaller.v2.viewmodel.InstallViewModel;
import com.android.packageinstaller.v2.viewmodel.InstallViewModelFactory;
+import java.util.ArrayList;
+import java.util.List;
-public class InstallLaunch extends FragmentActivity {
+public class InstallLaunch extends FragmentActivity implements InstallActionListener {
public static final String EXTRA_CALLING_PKG_UID =
InstallLaunch.class.getPackageName() + ".callingPkgUid";
@@ -48,11 +69,17 @@
InstallLaunch.class.getPackageName() + ".callingPkgName";
private static final String TAG = InstallLaunch.class.getSimpleName();
private static final String TAG_DIALOG = "dialog";
+ private final int REQUEST_TRUST_EXTERNAL_SOURCE = 1;
private final boolean mLocalLOGV = false;
+ /**
+ * A collection of unknown sources listeners that are actively listening for app ops mode
+ * changes
+ */
+ private final List<UnknownSourcesListener> mActiveUnknownSourcesListeners = new ArrayList<>(1);
private InstallViewModel mInstallViewModel;
private InstallRepository mInstallRepository;
-
private FragmentManager mFragmentManager;
+ private AppOpsManager mAppOpsManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -61,6 +88,8 @@
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
mFragmentManager = getSupportFragmentManager();
+ mAppOpsManager = getSystemService(AppOpsManager.class);
+
mInstallRepository = new InstallRepository(getApplicationContext());
mInstallViewModel = new ViewModelProvider(this,
new InstallViewModelFactory(this.getApplication(), mInstallRepository)).get(
@@ -87,10 +116,44 @@
InstallAborted aborted = (InstallAborted) installStage;
switch (aborted.getAbortReason()) {
// TODO: check if any dialog is to be shown for ABORT_REASON_INTERNAL_ERROR
- case ABORT_REASON_INTERNAL_ERROR -> setResult(RESULT_CANCELED, true);
- case ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted);
- default -> setResult(RESULT_CANCELED, true);
+ case InstallAborted.ABORT_REASON_DONE, InstallAborted.ABORT_REASON_INTERNAL_ERROR ->
+ setResult(aborted.getActivityResultCode(), aborted.getResultIntent(), true);
+ case InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted);
+ default -> setResult(RESULT_CANCELED, null, true);
}
+ } else if (installStage.getStageCode() == InstallStage.STAGE_USER_ACTION_REQUIRED) {
+ InstallUserActionRequired uar = (InstallUserActionRequired) installStage;
+ switch (uar.getActionReason()) {
+ case InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION:
+ InstallConfirmationFragment actionDialog = new InstallConfirmationFragment(uar);
+ showDialogInner(actionDialog);
+ break;
+ case InstallUserActionRequired.USER_ACTION_REASON_UNKNOWN_SOURCE:
+ ExternalSourcesBlockedFragment externalSourceDialog =
+ new ExternalSourcesBlockedFragment(uar);
+ showDialogInner(externalSourceDialog);
+ break;
+ case InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE:
+ AnonymousSourceFragment anonymousSourceDialog = new AnonymousSourceFragment();
+ showDialogInner(anonymousSourceDialog);
+ }
+ } else if (installStage.getStageCode() == InstallStage.STAGE_INSTALLING) {
+ InstallInstalling installing = (InstallInstalling) installStage;
+ InstallInstallingFragment installingDialog = new InstallInstallingFragment(installing);
+ showDialogInner(installingDialog);
+ } else if (installStage.getStageCode() == InstallStage.STAGE_SUCCESS) {
+ InstallSuccess success = (InstallSuccess) installStage;
+ if (success.shouldReturnResult()) {
+ Intent successIntent = success.getResultIntent();
+ setResult(Activity.RESULT_OK, successIntent, true);
+ } else {
+ InstallSuccessFragment successFragment = new InstallSuccessFragment(success);
+ showDialogInner(successFragment);
+ }
+ } else if (installStage.getStageCode() == InstallStage.STAGE_FAILED) {
+ InstallFailed failed = (InstallFailed) installStage;
+ InstallFailedFragment failedDialog = new InstallFailedFragment(failed);
+ showDialogInner(failedDialog);
} else {
Log.d(TAG, "Unimplemented stage: " + installStage.getStageCode());
showDialogInner(null);
@@ -122,7 +185,7 @@
shouldFinish = false;
showDialogInner(blockedByPolicyDialog);
}
- setResult(RESULT_CANCELED, shouldFinish);
+ setResult(RESULT_CANCELED, null, shouldFinish);
}
/**
@@ -161,12 +224,113 @@
}
}
- public void setResult(int resultCode, boolean shouldFinish) {
- // TODO: This is incomplete. We need to send RESULT_FIRST_USER, RESULT_OK etc
- // for relevant use cases. Investigate when to send what result.
- super.setResult(resultCode);
+ public void setResult(int resultCode, Intent data, boolean shouldFinish) {
+ super.setResult(resultCode, data);
if (shouldFinish) {
finish();
}
}
+
+ @Override
+ public void onPositiveResponse(int reasonCode) {
+ switch (reasonCode) {
+ case InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE ->
+ mInstallViewModel.forcedSkipSourceCheck();
+ case InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION ->
+ mInstallViewModel.initiateInstall();
+ }
+ }
+
+ @Override
+ public void onNegativeResponse(int stageCode) {
+ if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) {
+ mInstallViewModel.cleanupInstall();
+ }
+ setResult(Activity.RESULT_CANCELED, null, true);
+ }
+
+ @Override
+ public void sendUnknownAppsIntent(String sourcePackageName) {
+ Intent settingsIntent = new Intent();
+ settingsIntent.setAction(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
+ final Uri packageUri = Uri.parse("package:" + sourcePackageName);
+ settingsIntent.setData(packageUri);
+ settingsIntent.setFlags(FLAG_ACTIVITY_NO_HISTORY);
+
+ try {
+ registerAppOpChangeListener(new UnknownSourcesListener(sourcePackageName),
+ sourcePackageName);
+ startActivityForResult(settingsIntent, REQUEST_TRUST_EXTERNAL_SOURCE);
+ } catch (ActivityNotFoundException exc) {
+ Log.e(TAG, "Settings activity not found for action: "
+ + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
+ }
+ }
+
+ @Override
+ public void openInstalledApp(Intent intent) {
+ setResult(RESULT_OK, intent, true);
+ if (intent != null && intent.hasCategory(CATEGORY_LAUNCHER)) {
+ startActivity(intent);
+ }
+ }
+
+ private void registerAppOpChangeListener(UnknownSourcesListener listener, String packageName) {
+ mAppOpsManager.startWatchingMode(
+ AppOpsManager.OPSTR_REQUEST_INSTALL_PACKAGES, packageName,
+ listener);
+ mActiveUnknownSourcesListeners.add(listener);
+ }
+
+ private void unregisterAppOpChangeListener(UnknownSourcesListener listener) {
+ mActiveUnknownSourcesListeners.remove(listener);
+ mAppOpsManager.stopWatchingMode(listener);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == REQUEST_TRUST_EXTERNAL_SOURCE) {
+ mInstallViewModel.reattemptInstall();
+ } else {
+ setResult(Activity.RESULT_CANCELED, null, true);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ while (!mActiveUnknownSourcesListeners.isEmpty()) {
+ unregisterAppOpChangeListener(mActiveUnknownSourcesListeners.get(0));
+ }
+ }
+
+ private class UnknownSourcesListener implements AppOpsManager.OnOpChangedListener {
+
+ private final String mOriginatingPackage;
+
+ public UnknownSourcesListener(String originatingPackage) {
+ mOriginatingPackage = originatingPackage;
+ }
+
+ @Override
+ public void onOpChanged(String op, String packageName) {
+ if (!mOriginatingPackage.equals(packageName)) {
+ return;
+ }
+ unregisterAppOpChangeListener(this);
+ mActiveUnknownSourcesListeners.remove(this);
+ if (isDestroyed()) {
+ return;
+ }
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ if (!isDestroyed()) {
+ // Bring Pia to the foreground. FLAG_ACTIVITY_REORDER_TO_FRONT will reuse the
+ // paused instance, so we don't unnecessarily create a new instance of Pia.
+ startActivity(getIntent()
+ .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT));
+ }
+ }, 500);
+ }
+ }
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
new file mode 100644
index 0000000..6d6fcc9
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/AnonymousSourceFragment.java
@@ -0,0 +1,63 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.InstallActionListener;
+
+/**
+ * Dialog to show when the source of apk can not be identified.
+ */
+public class AnonymousSourceFragment extends DialogFragment {
+
+ public static String TAG = AnonymousSourceFragment.class.getSimpleName();
+ private InstallActionListener mInstallActionListener;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mInstallActionListener = (InstallActionListener) context;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity())
+ .setMessage(R.string.anonymous_source_warning)
+ .setPositiveButton(R.string.anonymous_source_continue,
+ ((dialog, which) -> mInstallActionListener.onPositiveResponse(
+ InstallUserActionRequired.USER_ACTION_REASON_ANONYMOUS_SOURCE)))
+ .setNegativeButton(R.string.cancel,
+ ((dialog, which) -> mInstallActionListener.onNegativeResponse(
+ InstallStage.STAGE_USER_ACTION_REQUIRED))).create();
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ mInstallActionListener.onNegativeResponse(InstallStage.STAGE_USER_ACTION_REQUIRED);
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
new file mode 100644
index 0000000..4cdce52
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/ExternalSourcesBlockedFragment.java
@@ -0,0 +1,72 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.InstallActionListener;
+
+/**
+ * Dialog to show when the installing app is an unknown source and needs AppOp grant to install
+ * other apps.
+ */
+public class ExternalSourcesBlockedFragment extends DialogFragment {
+
+ private final String TAG = ExternalSourcesBlockedFragment.class.getSimpleName();
+ private final InstallUserActionRequired mDialogData;
+ private InstallActionListener mInstallActionListener;
+
+ public ExternalSourcesBlockedFragment(InstallUserActionRequired dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mInstallActionListener = (InstallActionListener) context;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ return new AlertDialog.Builder(requireContext())
+ .setTitle(mDialogData.getAppLabel())
+ .setIcon(mDialogData.getAppIcon())
+ .setMessage(R.string.untrusted_external_source_warning)
+ .setPositiveButton(R.string.external_sources_settings,
+ (dialog, which) -> mInstallActionListener.sendUnknownAppsIntent(
+ mDialogData.getDialogMessage()))
+ .setNegativeButton(R.string.cancel,
+ (dialog, which) -> mInstallActionListener.onNegativeResponse(
+ mDialogData.getStageCode()))
+ .create();
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
new file mode 100644
index 0000000..6398aef
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallConfirmationFragment.java
@@ -0,0 +1,95 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.Html;
+import android.view.View;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.installstagedata.InstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.InstallActionListener;
+
+/**
+ * Dialog to show when the requesting user confirmation for installing an app.
+ */
+public class InstallConfirmationFragment extends DialogFragment {
+
+ public static String TAG = InstallConfirmationFragment.class.getSimpleName();
+
+ @NonNull
+ private final InstallUserActionRequired mDialogData;
+ @NonNull
+ private InstallActionListener mInstallActionListener;
+
+ public InstallConfirmationFragment(@NonNull InstallUserActionRequired dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mInstallActionListener = (InstallActionListener) context;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
+
+ AlertDialog dialog = new AlertDialog.Builder(requireContext())
+ .setIcon(mDialogData.getAppIcon())
+ .setTitle(mDialogData.getAppLabel())
+ .setView(dialogView)
+ .setPositiveButton(mDialogData.isAppUpdating() ? R.string.update : R.string.install,
+ (dialogInt, which) -> mInstallActionListener.onPositiveResponse(
+ InstallUserActionRequired.USER_ACTION_REASON_INSTALL_CONFIRMATION))
+ .setNegativeButton(R.string.cancel,
+ (dialogInt, which) -> mInstallActionListener.onNegativeResponse(
+ mDialogData.getStageCode()))
+
+ .create();
+
+ // TODO: Dynamically change positive button text to update anyway
+ TextView viewToEnable;
+ if (mDialogData.isAppUpdating()) {
+ viewToEnable = dialogView.requireViewById(R.id.install_confirm_question_update);
+ String dialogMessage = mDialogData.getDialogMessage();
+ if (dialogMessage != null) {
+ viewToEnable.setText(Html.fromHtml(dialogMessage, Html.FROM_HTML_MODE_LEGACY));
+ }
+ } else {
+ viewToEnable = dialogView.requireViewById(R.id.install_confirm_question);
+ }
+ viewToEnable.setVisibility(View.VISIBLE);
+
+ return dialog;
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java
new file mode 100644
index 0000000..d45cd76
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallFailedFragment.java
@@ -0,0 +1,107 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageInstaller;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.installstagedata.InstallFailed;
+import com.android.packageinstaller.v2.ui.InstallActionListener;
+
+/**
+ * Dialog to show when the installation failed. Depending on the failure code, an appropriate
+ * message would be shown to the user. This dialog is shown only when the caller does not want the
+ * install result back.
+ */
+public class InstallFailedFragment extends DialogFragment {
+
+ private static final String TAG = InstallFailedFragment.class.getSimpleName();
+ private final InstallFailed mDialogData;
+ private InstallActionListener mInstallActionListener;
+
+ public InstallFailedFragment(InstallFailed dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mInstallActionListener = (InstallActionListener) context;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
+ AlertDialog dialog = new AlertDialog.Builder(requireContext())
+ .setTitle(mDialogData.getAppLabel())
+ .setIcon(mDialogData.getAppIcon())
+ .setView(dialogView)
+ .setPositiveButton(R.string.done,
+ (dialogInt, which) -> mInstallActionListener.onNegativeResponse(
+ mDialogData.getStageCode()))
+ .create();
+ setExplanationFromErrorCode(mDialogData.getStatusCode(), dialogView);
+
+ return dialog;
+ }
+
+ /**
+ * Unhide the appropriate label for the statusCode.
+ *
+ * @param statusCode The status code from the package installer.
+ */
+ private void setExplanationFromErrorCode(int statusCode, View dialogView) {
+ Log.d(TAG, "Installation status code: " + statusCode);
+
+ View viewToEnable;
+ switch (statusCode) {
+ case PackageInstaller.STATUS_FAILURE_BLOCKED:
+ viewToEnable = dialogView.requireViewById(R.id.install_failed_blocked);
+ break;
+ case PackageInstaller.STATUS_FAILURE_CONFLICT:
+ viewToEnable = dialogView.requireViewById(R.id.install_failed_conflict);
+ break;
+ case PackageInstaller.STATUS_FAILURE_INCOMPATIBLE:
+ viewToEnable = dialogView.requireViewById(R.id.install_failed_incompatible);
+ break;
+ case PackageInstaller.STATUS_FAILURE_INVALID:
+ viewToEnable = dialogView.requireViewById(R.id.install_failed_invalid_apk);
+ break;
+ default:
+ viewToEnable = dialogView.requireViewById(R.id.install_failed);
+ break;
+ }
+
+ viewToEnable.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java
new file mode 100644
index 0000000..9f60f96
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallInstallingFragment.java
@@ -0,0 +1,64 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.installstagedata.InstallInstalling;
+
+/**
+ * Dialog to show when an install is in progress.
+ */
+public class InstallInstallingFragment extends DialogFragment {
+
+ private final InstallInstalling mDialogData;
+ private AlertDialog mDialog;
+
+ public InstallInstallingFragment(InstallInstalling dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
+ mDialog = new AlertDialog.Builder(requireContext())
+ .setTitle(mDialogData.getAppLabel())
+ .setIcon(mDialogData.getAppIcon())
+ .setView(dialogView)
+ .setNegativeButton(R.string.cancel, null)
+ .create();
+
+ dialogView.requireViewById(R.id.installing).setVisibility(View.VISIBLE);
+ this.setCancelable(false);
+
+ return mDialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(false);
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
new file mode 100644
index 0000000..ab6a932
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/InstallSuccessFragment.java
@@ -0,0 +1,100 @@
+/*
+ * 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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.Button;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.model.installstagedata.InstallSuccess;
+import com.android.packageinstaller.v2.ui.InstallActionListener;
+import java.util.List;
+
+/**
+ * Dialog to show on a successful installation. This dialog is shown only when the caller does not
+ * want the install result back.
+ */
+public class InstallSuccessFragment extends DialogFragment {
+
+ private final InstallSuccess mDialogData;
+ private AlertDialog mDialog;
+ private InstallActionListener mInstallActionListener;
+ private PackageManager mPm;
+
+ public InstallSuccessFragment(InstallSuccess dialogData) {
+ mDialogData = dialogData;
+ }
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mInstallActionListener = (InstallActionListener) context;
+ mPm = context.getPackageManager();
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ View dialogView = getLayoutInflater().inflate(R.layout.install_content_view, null);
+ mDialog = new AlertDialog.Builder(requireContext()).setTitle(mDialogData.getAppLabel())
+ .setIcon(mDialogData.getAppIcon()).setView(dialogView).setNegativeButton(R.string.done,
+ (dialog, which) -> mInstallActionListener.onNegativeResponse(
+ InstallStage.STAGE_SUCCESS))
+ .setPositiveButton(R.string.launch, (dialog, which) -> {
+ }).create();
+
+ dialogView.requireViewById(R.id.install_success).setVisibility(View.VISIBLE);
+
+ return mDialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Button launchButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ boolean enabled = false;
+ if (mDialogData.getResultIntent() != null) {
+ List<ResolveInfo> list = mPm.queryIntentActivities(mDialogData.getResultIntent(), 0);
+ if (list.size() > 0) {
+ enabled = true;
+ }
+ }
+ if (enabled) {
+ launchButton.setOnClickListener(view -> {
+ mInstallActionListener.openInstalledApp(mDialogData.getResultIntent());
+ });
+ } else {
+ launchButton.setEnabled(false);
+ }
+ }
+
+ @Override
+ public void onCancel(@NonNull DialogInterface dialog) {
+ super.onCancel(dialog);
+ mInstallActionListener.onNegativeResponse(mDialogData.getStageCode());
+ }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
index dce0b9a..47fd67f 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/SimpleErrorFragment.java
@@ -16,36 +16,47 @@
package com.android.packageinstaller.v2.ui.fragments;
-import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
+import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.installstagedata.InstallStage;
+import com.android.packageinstaller.v2.ui.InstallActionListener;
public class SimpleErrorFragment extends DialogFragment {
private static final String TAG = SimpleErrorFragment.class.getSimpleName();
private final int mMessageResId;
+ private InstallActionListener mInstallActionListener;
public SimpleErrorFragment(int messageResId) {
mMessageResId = messageResId;
}
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ mInstallActionListener = (InstallActionListener) context;
+ }
+
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setMessage(mMessageResId)
- .setPositiveButton(R.string.ok, (dialog, which) -> getActivity().finish())
+ .setPositiveButton(R.string.ok,
+ (dialog, which) ->
+ mInstallActionListener.onNegativeResponse(InstallStage.STAGE_ABORTED))
.create();
}
@Override
public void onCancel(DialogInterface dialog) {
- getActivity().setResult(Activity.RESULT_CANCELED);
- getActivity().finish();
+ super.onCancel(dialog);
+ mInstallActionListener.onNegativeResponse(InstallStage.STAGE_ABORTED);
}
}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
index 42b3023..759f468 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/InstallViewModel.java
@@ -58,7 +58,7 @@
if (installStage.getStageCode() != InstallStage.STAGE_READY) {
mCurrentInstallStage.setValue(installStage);
} else {
- // Proceed with user confirmation here.
+ checkIfAllowedAndInitiateInstall();
}
});
}
@@ -67,4 +67,35 @@
public MutableLiveData<Integer> getStagingProgress() {
return mRepository.getStagingProgress();
}
+
+ private void checkIfAllowedAndInitiateInstall() {
+ InstallStage stage = mRepository.requestUserConfirmation();
+ mCurrentInstallStage.setValue(stage);
+ }
+
+ public void forcedSkipSourceCheck() {
+ InstallStage stage = mRepository.forcedSkipSourceCheck();
+ mCurrentInstallStage.setValue(stage);
+ }
+
+ public void cleanupInstall() {
+ mRepository.cleanupInstall();
+ }
+
+ public void reattemptInstall() {
+ InstallStage stage = mRepository.reattemptInstall();
+ mCurrentInstallStage.setValue(stage);
+ }
+
+ public void initiateInstall() {
+ // Since installing is an async operation, we will get the install result later in time.
+ // Result of the installation will be set in InstallRepository#mInstallResult.
+ // As such, mCurrentInstallStage will need to add another MutableLiveData as a data source
+ mRepository.initiateInstall();
+ mCurrentInstallStage.addSource(mRepository.getInstallResult(), installStage -> {
+ if (installStage != null) {
+ mCurrentInstallStage.setValue(installStage);
+ }
+ });
+ }
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index b1e1585..90c7d46 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -49,6 +49,7 @@
import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
+import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver
@@ -100,6 +101,7 @@
SettingsTextFieldPasswordPageProvider,
SearchScaffoldPageProvider,
CardPageProvider,
+ CopyablePageProvider,
),
rootPages = listOf(
HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index f52ceec..1d897f7 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -44,6 +44,7 @@
import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
+import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.widget.scaffold.HomeScaffold
@@ -71,6 +72,7 @@
AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
CardPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ CopyablePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
)
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
new file mode 100644
index 0000000..f897d8c
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.ui
+
+import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CopyableBody
+
+private const val TITLE = "Sample Copyable"
+
+object CopyablePageProvider : SettingsPageProvider {
+ override val name = "Copyable"
+
+ private val owner = createSettingsPage()
+
+ fun buildInjectEntry(): SettingsEntryBuilder {
+ return SettingsEntryBuilder.createInject(owner)
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
+ .setSearchDataFn { EntrySearchData(title = TITLE) }
+ }
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ RegularScaffold(title = TITLE) {
+ Box(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
+ CopyableBody(body = "Copyable body")
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index 905640f..b40e911 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
#
[versions]
-agp = "8.1.3"
+agp = "8.1.4"
compose-compiler = "1.5.1"
dexmaker-mockito = "2.28.3"
jvm = "17"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
new file mode 100644
index 0000000..930d0a1
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.DpOffset
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+fun CopyableBody(body: String) {
+ var expanded by remember { mutableStateOf(false) }
+ var dpOffset by remember { mutableStateOf(DpOffset.Unspecified) }
+
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onLongPress = {
+ dpOffset = DpOffset(it.x.toDp(), it.y.toDp())
+ expanded = true
+ },
+ )
+ }
+ ) {
+ SettingsBody(body)
+
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ offset = dpOffset,
+ ) {
+ DropdownMenuTitle(body)
+ DropdownMenuCopy(body) { expanded = false }
+ }
+ }
+}
+
+@Composable
+private fun DropdownMenuTitle(text: String) {
+ Text(
+ text = text,
+ modifier = Modifier
+ .padding(MenuDefaults.DropdownMenuItemContentPadding)
+ .padding(
+ top = SettingsDimension.itemPaddingAround,
+ bottom = SettingsDimension.buttonPaddingVertical,
+ ),
+ color = SettingsTheme.colorScheme.categoryTitle,
+ style = MaterialTheme.typography.labelMedium,
+ )
+}
+
+@Composable
+private fun DropdownMenuCopy(body: String, onCopy: () -> Unit) {
+ val clipboardManager = LocalClipboardManager.current
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = stringResource(android.R.string.copy),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ onClick = {
+ onCopy()
+ clipboardManager.setText(AnnotatedString(body))
+ }
+ )
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
new file mode 100644
index 0000000..71072a5
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.ui
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CopyableBodyTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun text_isDisplayed() {
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).assertIsDisplayed()
+ }
+
+ @Test
+ fun onLongPress_contextMenuDisplayed() {
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).performTouchInput {
+ longClick()
+ }
+
+ composeTestRule.onNodeWithText(context.getString(android.R.string.copy)).assertIsDisplayed()
+ }
+
+ @Test
+ fun onCopy_saveToClipboard() {
+ val clipboardManager = context.getSystemService(ClipboardManager::class.java)!!
+ clipboardManager.setPrimaryClip(ClipData.newPlainText("", ""))
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).performTouchInput {
+ longClick()
+ }
+ composeTestRule.onNodeWithText(context.getString(android.R.string.copy)).performClick()
+
+ assertThat(clipboardManager.primaryClip!!.getItemAt(0).text.toString()).isEqualTo(TEXT)
+ }
+
+ private companion object {
+ const val TEXT = "Text"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index fc10a27..45295b0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -36,10 +36,10 @@
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
import com.android.settingslib.development.DevelopmentSettingsEnabler
import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.CopyableBody
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SettingsTitle
import com.android.settingslib.spaprivileged.R
@@ -71,26 +71,38 @@
@Composable
private fun InstallType(app: ApplicationInfo) {
if (!app.isInstantApp) return
- Spacer(modifier = Modifier.height(4.dp))
- SettingsBody(stringResource(com.android.settingslib.widget.preference.app.R.string.install_type_instant))
+ Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
+ SettingsBody(
+ stringResource(
+ com.android.settingslib.widget.preference.app.R.string.install_type_instant
+ )
+ )
}
@Composable
private fun AppVersion() {
- if (packageInfo.versionName == null) return
- Spacer(modifier = Modifier.height(4.dp))
- SettingsBody(packageInfo.versionNameBidiWrapped)
+ val versionName = packageInfo.versionNameBidiWrapped ?: return
+ Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
+ SettingsBody(versionName)
}
@Composable
fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) {
- if (packageInfo.versionName == null) return
+ val context = LocalContext.current
+ val footer = remember(showPackageName) {
+ val list = mutableListOf<String>()
+ packageInfo.versionNameBidiWrapped?.let {
+ list += context.getString(R.string.version_text, it)
+ }
+ if (showPackageName) {
+ list += packageInfo.packageName
+ }
+ list.joinToString(separator = System.lineSeparator())
+ }
+ if (footer.isBlank()) return
HorizontalDivider()
Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
- SettingsBody(stringResource(R.string.version_text, packageInfo.versionNameBidiWrapped))
- if (showPackageName) {
- SettingsBody(packageInfo.packageName)
- }
+ CopyableBody(footer)
}
}
@@ -104,7 +116,7 @@
private companion object {
/** Wrapped the version name, so its directionality still keep same when RTL. */
- val PackageInfo.versionNameBidiWrapped: String
+ val PackageInfo.versionNameBidiWrapped: String?
get() = BidiFormatter.getInstance().unicodeWrap(versionName)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
index 785f779..36c91f4 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
@@ -45,7 +45,7 @@
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
-import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
+import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel
import kotlinx.coroutines.flow.Flow
private const val ENTRY_NAME = "AppList"
@@ -157,7 +157,7 @@
}
val restrictedMode by restrictionsProviderFactory.rememberRestrictedMode(restrictions)
val allowed = listModel.isAllowed(record)
- return RestrictedSwitchPreference.getSummary(
+ return RestrictedSwitchPreferenceModel.getSummary(
context = context,
restrictedModeSupplier = { restrictedMode },
summaryIfNoRestricted = { getSummaryIfNoRestricted(allowed()) },
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreference.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreference.kt
new file mode 100644
index 0000000..125f636
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreference.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.template.preference
+
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.Composable
+import com.android.settingslib.spa.widget.preference.MainSwitchPreference
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
+import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper
+
+@Composable
+fun RestrictedMainSwitchPreference(model: SwitchPreferenceModel, restrictions: Restrictions) {
+ RestrictedMainSwitchPreference(model, restrictions, ::RestrictionsProviderImpl)
+}
+
+@VisibleForTesting
+@Composable
+internal fun RestrictedMainSwitchPreference(
+ model: SwitchPreferenceModel,
+ restrictions: Restrictions,
+ restrictionsProviderFactory: RestrictionsProviderFactory,
+) {
+ if (restrictions.keys.isEmpty()) {
+ MainSwitchPreference(model)
+ return
+ }
+ restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
+ MainSwitchPreference(it)
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt
index e41976f..d5c5574 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreference.kt
@@ -16,29 +16,14 @@
package com.android.settingslib.spaprivileged.template.preference
-import android.content.Context
import androidx.annotation.VisibleForTesting
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.semantics.toggleableState
-import androidx.compose.ui.state.ToggleableState
import com.android.settingslib.spa.widget.preference.SwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
-import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
-import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
-import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
-import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
-import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
+import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreferenceModel.Companion.RestrictedSwitchWrapper
@Composable
fun RestrictedSwitchPreference(
@@ -59,91 +44,7 @@
SwitchPreference(model)
return
}
- val context = LocalContext.current
- val restrictedMode = restrictionsProviderFactory.rememberRestrictedMode(restrictions).value
- val restrictedSwitchModel = remember(restrictedMode) {
- RestrictedSwitchPreferenceModel(context, model, restrictedMode)
- }
- restrictedSwitchModel.RestrictionWrapper {
- SwitchPreference(restrictedSwitchModel)
- }
-}
-
-internal object RestrictedSwitchPreference {
- fun getSummary(
- context: Context,
- restrictedModeSupplier: () -> RestrictedMode?,
- summaryIfNoRestricted: () -> String,
- checked: () -> Boolean?,
- ): () -> String = {
- when (val restrictedMode = restrictedModeSupplier()) {
- is NoRestricted -> summaryIfNoRestricted()
- is BaseUserRestricted -> context.getString(com.android.settingslib.R.string.disabled)
- is BlockedByAdmin -> restrictedMode.getSummary(checked())
- null -> context.getPlaceholder()
- }
- }
-}
-
-private class RestrictedSwitchPreferenceModel(
- context: Context,
- model: SwitchPreferenceModel,
- private val restrictedMode: RestrictedMode?,
-) : SwitchPreferenceModel {
- override val title = model.title
-
- override val summary = RestrictedSwitchPreference.getSummary(
- context = context,
- restrictedModeSupplier = { restrictedMode },
- summaryIfNoRestricted = model.summary,
- checked = model.checked,
- )
-
- override val checked = when (restrictedMode) {
- null -> ({ null })
- is NoRestricted -> model.checked
- is BaseUserRestricted -> ({ false })
- is BlockedByAdmin -> model.checked
- }
-
- override val changeable = when (restrictedMode) {
- null -> ({ false })
- is NoRestricted -> model.changeable
- is BaseUserRestricted -> ({ false })
- is BlockedByAdmin -> ({ false })
- }
-
- override val onCheckedChange = when (restrictedMode) {
- null -> null
- is NoRestricted -> model.onCheckedChange
- // Need to passthrough onCheckedChange for toggleable semantics, although since changeable
- // is false so this will not be called.
- is BaseUserRestricted -> model.onCheckedChange
- // Pass null since semantics ToggleableState is provided in RestrictionWrapper.
- is BlockedByAdmin -> null
- }
-
- @Composable
- fun RestrictionWrapper(content: @Composable () -> Unit) {
- if (restrictedMode !is BlockedByAdmin) {
- content()
- return
- }
- Box(
- Modifier
- .clickable(
- role = Role.Switch,
- onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
- )
- .semantics {
- this.toggleableState = ToggleableState(checked())
- },
- ) { content() }
- }
-
- private fun ToggleableState(value: Boolean?) = when (value) {
- true -> ToggleableState.On
- false -> ToggleableState.Off
- null -> ToggleableState.Indeterminate
+ restrictionsProviderFactory.RestrictedSwitchWrapper(model, restrictions) {
+ SwitchPreference(it)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
new file mode 100644
index 0000000..fa44ecb
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/preference/RestrictedSwitchPreferenceModel.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.template.preference
+
+import android.content.Context
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.toggleableState
+import androidx.compose.ui.state.ToggleableState
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spaprivileged.framework.compose.getPlaceholder
+import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
+import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
+import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictedMode
+import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
+import com.android.settingslib.spaprivileged.model.enterprise.rememberRestrictedMode
+
+internal class RestrictedSwitchPreferenceModel(
+ context: Context,
+ model: SwitchPreferenceModel,
+ private val restrictedMode: RestrictedMode?,
+) : SwitchPreferenceModel {
+ override val title = model.title
+
+ override val summary = getSummary(
+ context = context,
+ restrictedModeSupplier = { restrictedMode },
+ summaryIfNoRestricted = model.summary,
+ checked = model.checked,
+ )
+
+ override val checked = when (restrictedMode) {
+ null -> ({ null })
+ is NoRestricted -> model.checked
+ is BaseUserRestricted -> ({ false })
+ is BlockedByAdmin -> model.checked
+ }
+
+ override val changeable = if (restrictedMode is NoRestricted) model.changeable else ({ false })
+
+ override val onCheckedChange = when (restrictedMode) {
+ null -> null
+ is NoRestricted -> model.onCheckedChange
+ // Need to passthrough onCheckedChange for toggleable semantics, although since changeable
+ // is false so this will not be called.
+ is BaseUserRestricted -> model.onCheckedChange
+ // Pass null since semantics ToggleableState is provided in RestrictionWrapper.
+ is BlockedByAdmin -> null
+ }
+
+ @Composable
+ fun RestrictionWrapper(content: @Composable () -> Unit) {
+ if (restrictedMode !is BlockedByAdmin) {
+ content()
+ return
+ }
+ Box(
+ Modifier
+ .clickable(
+ role = Role.Switch,
+ onClick = { restrictedMode.sendShowAdminSupportDetailsIntent() },
+ )
+ .semantics {
+ this.toggleableState = ToggleableState(checked())
+ },
+ ) { content() }
+ }
+
+ private fun ToggleableState(value: Boolean?) = when (value) {
+ true -> ToggleableState.On
+ false -> ToggleableState.Off
+ null -> ToggleableState.Indeterminate
+ }
+
+ companion object {
+ @Composable
+ fun RestrictionsProviderFactory.RestrictedSwitchWrapper(
+ model: SwitchPreferenceModel,
+ restrictions: Restrictions,
+ content: @Composable (SwitchPreferenceModel) -> Unit,
+ ) {
+ val context = LocalContext.current
+ val restrictedMode = rememberRestrictedMode(restrictions).value
+ val restrictedSwitchPreferenceModel = remember(restrictedMode) {
+ RestrictedSwitchPreferenceModel(context, model, restrictedMode)
+ }
+ restrictedSwitchPreferenceModel.RestrictionWrapper {
+ content(restrictedSwitchPreferenceModel)
+ }
+ }
+
+ fun getSummary(
+ context: Context,
+ restrictedModeSupplier: () -> RestrictedMode?,
+ summaryIfNoRestricted: () -> String,
+ checked: () -> Boolean?,
+ ): () -> String = {
+ when (val restrictedMode = restrictedModeSupplier()) {
+ is NoRestricted -> summaryIfNoRestricted()
+ is BaseUserRestricted ->
+ context.getString(com.android.settingslib.R.string.disabled)
+
+ is BlockedByAdmin -> restrictedMode.getSummary(checked())
+ null -> context.getPlaceholder()
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
index ab34f68..72a5bd7 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
@@ -105,7 +105,8 @@
}
}
- composeTestRule.onNodeWithText("version $VERSION_NAME").assertIsDisplayed()
+ composeTestRule.onNodeWithText(text = "version $VERSION_NAME", substring = true)
+ .assertIsDisplayed()
}
@Test
@@ -119,10 +120,10 @@
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
- appInfoProvider.FooterAppVersion(true)
+ appInfoProvider.FooterAppVersion(showPackageName = true)
}
}
- composeTestRule.onNodeWithText(PACKAGE_NAME).assertIsDisplayed()
+ composeTestRule.onNodeWithText(text = PACKAGE_NAME, substring = true).assertIsDisplayed()
}
@@ -137,10 +138,10 @@
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
- appInfoProvider.FooterAppVersion(false)
+ appInfoProvider.FooterAppVersion(showPackageName = false)
}
}
- composeTestRule.onNodeWithText(PACKAGE_NAME).assertDoesNotExist()
+ composeTestRule.onNodeWithText(text = PACKAGE_NAME, substring = true).assertDoesNotExist()
}
private companion object {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreferenceTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreferenceTest.kt
new file mode 100644
index 0000000..55c16bd
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/preference/RestrictedMainSwitchPreferenceTest.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spaprivileged.template.preference
+
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertIsNotEnabled
+import androidx.compose.ui.test.isOff
+import androidx.compose.ui.test.isOn
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
+import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
+import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.tests.testutils.FakeBlockedByAdmin
+import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class RestrictedMainSwitchPreferenceTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val fakeBlockedByAdmin = FakeBlockedByAdmin()
+
+ private val fakeRestrictionsProvider = FakeRestrictionsProvider()
+
+ private val switchPreferenceModel = object : SwitchPreferenceModel {
+ override val title = TITLE
+ private val checkedState = mutableStateOf(true)
+ override val checked = { checkedState.value }
+ override val onCheckedChange: (Boolean) -> Unit = { checkedState.value = it }
+ }
+
+ @Test
+ fun whenRestrictionsKeysIsEmpty_enabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = emptyList())
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled()
+ composeTestRule.onNode(isOn()).assertIsDisplayed()
+ }
+
+ @Test
+ fun whenRestrictionsKeysIsEmpty_toggleable() {
+ val restrictions = Restrictions(userId = USER_ID, keys = emptyList())
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ composeTestRule.onNode(isOff()).assertIsDisplayed()
+ }
+
+ @Test
+ fun whenNoRestricted_enabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = NoRestricted
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled()
+ composeTestRule.onNode(isOn()).assertIsDisplayed()
+ }
+
+ @Test
+ fun whenNoRestricted_toggleable() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = NoRestricted
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ composeTestRule.onNode(isOff()).assertIsDisplayed()
+ }
+
+ @Test
+ fun whenBaseUserRestricted_disabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = BaseUserRestricted
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsNotEnabled()
+ composeTestRule.onNode(isOff()).assertIsDisplayed()
+ }
+
+ @Test
+ fun whenBaseUserRestricted_notToggleable() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = BaseUserRestricted
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ composeTestRule.onNode(isOff()).assertIsDisplayed()
+ }
+
+ @Test
+ fun whenBlockedByAdmin_disabled() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin
+
+ setContent(restrictions)
+
+ composeTestRule.onNodeWithText(TITLE).assertIsDisplayed().assertIsEnabled()
+ composeTestRule.onNodeWithText(FakeBlockedByAdmin.SUMMARY).assertDoesNotExist()
+ composeTestRule.onNode(isOn()).assertIsDisplayed()
+ }
+
+ @Test
+ fun whenBlockedByAdmin_click() {
+ val restrictions = Restrictions(userId = USER_ID, keys = listOf(RESTRICTION_KEY))
+ fakeRestrictionsProvider.restrictedMode = fakeBlockedByAdmin
+
+ setContent(restrictions)
+ composeTestRule.onRoot().performClick()
+
+ assertThat(fakeBlockedByAdmin.sendShowAdminSupportDetailsIntentIsCalled).isTrue()
+ }
+
+ private fun setContent(restrictions: Restrictions) {
+ composeTestRule.setContent {
+ RestrictedMainSwitchPreference(switchPreferenceModel, restrictions) { _, _ ->
+ fakeRestrictionsProvider
+ }
+ }
+ }
+
+ private companion object {
+ const val TITLE = "Title"
+ const val USER_ID = 0
+ const val RESTRICTION_KEY = "restriction_key"
+ }
+}
diff --git a/packages/SettingsLib/res/drawable/ic_bt_untethered_earbuds.xml b/packages/SettingsLib/res/drawable/ic_bt_untethered_earbuds.xml
new file mode 100644
index 0000000..bcf5b91
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_bt_untethered_earbuds.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?android:attr/colorControlNormal" >
+ <path
+ android:fillColor="#4E4639"
+ android:pathData="M21.818,18.14V15.227C21.818,12.522 19.615,10.318 16.909,10.318C14.204,10.318 12,12.522 12,15.227V19.591C12,22.296 14.204,24.5 16.909,24.5C17.662,24.5 18.382,24.326 19.025,24.02C19.538,24.326 20.127,24.5 20.727,24.5C22.527,24.5 24,23.028 24,21.228C24,19.809 23.084,18.587 21.818,18.14ZM16.909,12.5C18.414,12.5 19.636,13.722 19.636,15.227C19.636,16.733 18.414,17.955 16.909,17.955C15.404,17.955 14.182,16.733 14.182,15.227C14.182,13.722 15.404,12.5 16.909,12.5ZM14.182,19.591V19.308C14.967,19.831 15.906,20.136 16.909,20.136C17.913,20.136 18.851,19.831 19.636,19.308V19.591C19.636,19.635 19.636,19.678 19.636,19.711C19.636,19.722 19.636,19.733 19.636,19.744C19.636,19.777 19.636,19.82 19.625,19.853C19.625,19.864 19.625,19.886 19.625,19.896C19.625,19.918 19.615,19.951 19.615,19.973C19.615,19.995 19.604,20.028 19.604,20.049C19.604,20.06 19.593,20.082 19.593,20.093C19.549,20.3 19.494,20.497 19.407,20.693C19.385,20.736 19.364,20.78 19.342,20.824C19.342,20.835 19.331,20.846 19.331,20.846C19.32,20.867 19.309,20.889 19.298,20.911C19.287,20.933 19.265,20.966 19.254,20.987C19.244,20.998 19.244,21.009 19.233,21.02C19.211,21.053 19.2,21.075 19.178,21.107C19.178,21.107 19.178,21.108 19.178,21.118C18.687,21.838 17.858,22.318 16.92,22.318C15.404,22.318 14.182,21.097 14.182,19.591Z" />
+ <path
+ android:fillColor="#4E4639"
+ android:pathData="M12,5.409C12,2.704 9.796,0.5 7.091,0.5C4.385,0.5 2.182,2.704 2.182,5.409V8.322C0.916,8.769 0,9.991 0,11.409C0,13.209 1.473,14.682 3.273,14.682C3.873,14.682 4.462,14.507 4.975,14.202C5.618,14.507 6.338,14.682 7.091,14.682C9.796,14.682 12,12.478 12,9.773V5.409ZM7.091,2.682C8.596,2.682 9.818,3.904 9.818,5.409C9.818,6.915 8.596,8.136 7.091,8.136C5.585,8.136 4.364,6.915 4.364,5.409C4.364,3.904 5.585,2.682 7.091,2.682ZM7.091,12.5C6.153,12.5 5.324,12.02 4.833,11.3C4.833,11.3 4.833,11.3 4.833,11.289C4.811,11.256 4.8,11.234 4.778,11.202C4.767,11.191 4.767,11.18 4.756,11.169C4.745,11.147 4.724,11.115 4.713,11.093C4.702,11.071 4.691,11.049 4.68,11.027C4.68,11.016 4.669,11.005 4.669,11.005C4.647,10.962 4.625,10.918 4.604,10.875C4.516,10.678 4.451,10.482 4.418,10.274C4.418,10.264 4.407,10.242 4.407,10.231C4.407,10.209 4.396,10.176 4.396,10.155C4.396,10.133 4.385,10.1 4.385,10.078C4.385,10.067 4.385,10.045 4.385,10.035C4.385,10.002 4.375,9.958 4.375,9.925C4.375,9.915 4.375,9.904 4.375,9.893C4.364,9.86 4.364,9.816 4.364,9.773V9.489C5.149,10.013 6.087,10.318 7.091,10.318C8.095,10.318 9.033,10.013 9.818,9.489V9.773C9.818,11.278 8.596,12.5 7.091,12.5Z" />
+</vector>
+
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java
index 21cdc49..b3d66aa 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java
@@ -259,8 +259,10 @@
}
@Test
- public void isSearchable_noMetadata_isTrue() {
- final Tile tile = new ActivityTile(null, "category");
+ public void isSearchable_nullMetadata_isTrue() {
+ mActivityInfo.metaData = null;
+
+ final Tile tile = new ActivityTile(mActivityInfo, "category");
assertThat(tile.isSearchable()).isTrue();
}
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 27a45df..3e0d05c 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -391,6 +391,7 @@
case Settings.Secure.TOUCH_EXPLORATION_ENABLED:
case Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED:
case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED:
+ case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED:
case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED:
return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) != 0;
case Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES:
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 ddd1c67..914e5f2 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
@@ -22,7 +22,7 @@
import android.view.WindowInsets
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
@@ -47,6 +47,14 @@
throwComposeUnavailableError()
}
+ override fun setCommunalEditWidgetActivityContent(
+ activity: ComponentActivity,
+ viewModel: BaseCommunalViewModel,
+ onOpenWidgetPicker: () -> Unit,
+ ) {
+ throwComposeUnavailableError()
+ }
+
override fun createFooterActionsView(
context: Context,
viewModel: FooterActionsViewModel,
@@ -67,12 +75,12 @@
override fun createCommunalView(
context: Context,
- viewModel: CommunalViewModel,
+ viewModel: BaseCommunalViewModel,
): View {
throwComposeUnavailableError()
}
- override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
+ override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): 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 eeda6c6..59bd95b 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
@@ -32,7 +32,7 @@
import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider
import com.android.systemui.communal.ui.compose.CommunalContainer
import com.android.systemui.communal.ui.compose.CommunalHub
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -62,6 +62,21 @@
activity.setContent { PlatformTheme { PeopleScreen(viewModel, onResult) } }
}
+ override fun setCommunalEditWidgetActivityContent(
+ activity: ComponentActivity,
+ viewModel: BaseCommunalViewModel,
+ onOpenWidgetPicker: () -> Unit,
+ ) {
+ activity.setContent {
+ PlatformTheme {
+ CommunalHub(
+ viewModel = viewModel,
+ onOpenWidgetPicker = onOpenWidgetPicker,
+ )
+ }
+ }
+ }
+
override fun createFooterActionsView(
context: Context,
viewModel: FooterActionsViewModel,
@@ -98,14 +113,14 @@
override fun createCommunalView(
context: Context,
- viewModel: CommunalViewModel,
+ viewModel: BaseCommunalViewModel,
): View {
return ComposeView(context).apply {
setContent { PlatformTheme { CommunalHub(viewModel = viewModel) } }
}
}
- override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View {
+ override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View {
return ComposeView(context).apply {
setContent { PlatformTheme { CommunalContainer(viewModel = viewModel) } }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 2bbe9b8..ff1cbd6 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -39,11 +39,11 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.integerResource
-import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.modifiers.thenIf
@@ -79,12 +79,6 @@
val lineColor = MaterialTheme.colorScheme.primary
val lineStrokeWidth = with(LocalDensity.current) { LINE_STROKE_WIDTH_DP.dp.toPx() }
- var containerSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) }
- val horizontalSpacing = containerSize.width / colCount
- val verticalSpacing = containerSize.height / rowCount
- val spacing = min(horizontalSpacing, verticalSpacing).toFloat()
- val verticalOffset = containerSize.height - spacing * rowCount
-
// All dots that should be rendered on the grid.
val dots: List<PatternDotViewModel> by viewModel.dots.collectAsState()
// The most recently selected dot, if the user is currently dragging.
@@ -195,13 +189,14 @@
// This is the position of the input pointer.
var inputPosition: Offset? by remember { mutableStateOf(null) }
+ var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
Canvas(
modifier
// Need to clip to bounds to make sure that the lines don't follow the input pointer
// when it leaves the bounds of the dot grid.
.clipToBounds()
- .onSizeChanged { containerSize = it }
+ .onGloballyPositioned { coordinates -> gridCoordinates = coordinates }
.thenIf(isInputEnabled) {
Modifier.pointerInput(Unit) {
awaitEachGesture {
@@ -232,63 +227,73 @@
viewModel.onDrag(
xPx = change.position.x,
yPx = change.position.y,
- containerSizePx = containerSize.width,
- verticalOffsetPx = verticalOffset,
+ containerSizePx = size.width,
)
}
}
}
) {
- if (isAnimationEnabled) {
- // Draw lines between dots.
- selectedDots.forEachIndexed { index, dot ->
- if (index > 0) {
- val previousDot = selectedDots[index - 1]
- val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value
- val startLerp = 1 - lineFadeOutAnimationProgress
- val from = pixelOffset(previousDot, spacing, verticalOffset)
- val to = pixelOffset(dot, spacing, verticalOffset)
- val lerpedFrom =
- Offset(
- x = from.x + (to.x - from.x) * startLerp,
- y = from.y + (to.y - from.y) * startLerp,
+ gridCoordinates?.let { nonNullCoordinates ->
+ val containerSize = nonNullCoordinates.size
+ val horizontalSpacing = containerSize.width.toFloat() / colCount
+ val verticalSpacing = containerSize.height.toFloat() / rowCount
+ val spacing = min(horizontalSpacing, verticalSpacing)
+ val verticalOffset = containerSize.height - spacing * rowCount
+
+ if (isAnimationEnabled) {
+ // Draw lines between dots.
+ selectedDots.forEachIndexed { index, dot ->
+ if (index > 0) {
+ val previousDot = selectedDots[index - 1]
+ val lineFadeOutAnimationProgress =
+ lineFadeOutAnimatables[previousDot]!!.value
+ val startLerp = 1 - lineFadeOutAnimationProgress
+ val from = pixelOffset(previousDot, spacing, verticalOffset)
+ val to = pixelOffset(dot, spacing, verticalOffset)
+ val lerpedFrom =
+ Offset(
+ x = from.x + (to.x - from.x) * startLerp,
+ y = from.y + (to.y - from.y) * startLerp,
+ )
+ drawLine(
+ start = lerpedFrom,
+ end = to,
+ cap = StrokeCap.Round,
+ alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
)
- drawLine(
- start = lerpedFrom,
- end = to,
- cap = StrokeCap.Round,
- alpha = lineFadeOutAnimationProgress * lineAlpha(spacing),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
+ }
+ }
+
+ // Draw the line between the most recently-selected dot and the input pointer
+ // position.
+ inputPosition?.let { lineEnd ->
+ currentDot?.let { dot ->
+ val from = pixelOffset(dot, spacing, verticalOffset)
+ val lineLength =
+ sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
+ drawLine(
+ start = from,
+ end = lineEnd,
+ cap = StrokeCap.Round,
+ alpha = lineAlpha(spacing, lineLength),
+ color = lineColor,
+ strokeWidth = lineStrokeWidth,
+ )
+ }
}
}
- // Draw the line between the most recently-selected dot and the input pointer position.
- inputPosition?.let { lineEnd ->
- currentDot?.let { dot ->
- val from = pixelOffset(dot, spacing, verticalOffset)
- val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
- drawLine(
- start = from,
- end = lineEnd,
- cap = StrokeCap.Round,
- alpha = lineAlpha(spacing, lineLength),
- color = lineColor,
- strokeWidth = lineStrokeWidth,
- )
- }
+ // Draw each dot on the grid.
+ dots.forEach { dot ->
+ drawCircle(
+ center = pixelOffset(dot, spacing, verticalOffset),
+ color = dotColor,
+ radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f),
+ )
}
}
-
- // Draw each dot on the grid.
- dots.forEach { dot ->
- drawCircle(
- center = pixelOffset(dot, spacing, verticalOffset),
- color = dotColor,
- radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f),
- )
- }
}
}
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 09706be..ce84c19 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
@@ -32,7 +32,7 @@
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.transitions
import com.android.systemui.communal.shared.model.CommunalSceneKey
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import kotlinx.coroutines.flow.transform
object Communal {
@@ -59,7 +59,7 @@
@Composable
fun CommunalContainer(
modifier: Modifier = Modifier,
- viewModel: CommunalViewModel,
+ viewModel: BaseCommunalViewModel,
) {
val currentScene: SceneKey by
viewModel.currentScene
@@ -129,7 +129,7 @@
/** Scene containing the glanceable hub UI. */
@Composable
private fun SceneScope.CommunalScene(
- viewModel: CommunalViewModel,
+ viewModel: BaseCommunalViewModel,
modifier: Modifier = Modifier,
) {
Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index d822d19..e8ecd3a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -19,6 +19,8 @@
import android.os.Bundle
import android.util.SizeF
import android.widget.FrameLayout
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -31,10 +33,13 @@
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Edit
import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
@@ -44,14 +49,14 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.media.controls.ui.MediaHierarchyManager
import com.android.systemui.media.controls.ui.MediaHostState
import com.android.systemui.res.R
@@ -59,39 +64,27 @@
@Composable
fun CommunalHub(
modifier: Modifier = Modifier,
- viewModel: CommunalViewModel,
+ viewModel: BaseCommunalViewModel,
+ onOpenWidgetPicker: (() -> Unit)? = null,
) {
val communalContent by viewModel.communalContent.collectAsState(initial = emptyList())
Box(
modifier = modifier.fillMaxSize().background(Color.White),
) {
- LazyHorizontalGrid(
- modifier = modifier.height(Dimensions.GridHeight).align(Alignment.CenterStart),
- rows = GridCells.Fixed(CommunalContentSize.FULL.span),
- contentPadding = PaddingValues(horizontal = Dimensions.Spacing),
- horizontalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
- verticalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
- ) {
- items(
- count = communalContent.size,
- key = { index -> communalContent[index].key },
- span = { index -> GridItemSpan(communalContent[index].size.span) },
- ) { index ->
- CommunalContent(
- modifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth),
- model = communalContent[index],
- viewModel = viewModel,
- deleteOnClick = viewModel::onDeleteWidget,
- size =
- SizeF(
- Dimensions.CardWidth.value,
- communalContent[index].size.dp().value,
- ),
- )
+ CommunalHubLazyGrid(
+ modifier = Modifier.height(Dimensions.GridHeight).align(Alignment.CenterStart),
+ communalContent = communalContent,
+ isEditMode = viewModel.isEditMode,
+ viewModel = viewModel,
+ )
+ if (viewModel.isEditMode && onOpenWidgetPicker != null) {
+ IconButton(onClick = onOpenWidgetPicker) {
+ Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text))
}
- }
- IconButton(onClick = viewModel::onOpenWidgetEditor) {
- Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor))
+ } else {
+ IconButton(onClick = viewModel::onOpenWidgetEditor) {
+ Icon(Icons.Default.Edit, stringResource(R.string.button_to_open_widget_editor))
+ }
}
// This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving
@@ -106,16 +99,80 @@
}
}
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun CommunalHubLazyGrid(
+ communalContent: List<CommunalContentModel>,
+ isEditMode: Boolean,
+ viewModel: BaseCommunalViewModel,
+ modifier: Modifier = Modifier,
+) {
+ var gridModifier = modifier
+ val gridState = rememberLazyGridState()
+ var list = communalContent
+ var dragDropState: GridDragDropState? = null
+ if (isEditMode && viewModel is CommunalEditModeViewModel) {
+ val contentListState = rememberContentListState(communalContent, viewModel)
+ list = contentListState.list
+ dragDropState = rememberGridDragDropState(gridState, contentListState)
+ gridModifier = gridModifier.dragContainer(dragDropState)
+ }
+ LazyHorizontalGrid(
+ modifier = gridModifier,
+ state = gridState,
+ rows = GridCells.Fixed(CommunalContentSize.FULL.span),
+ contentPadding = PaddingValues(horizontal = Dimensions.Spacing),
+ horizontalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
+ verticalArrangement = Arrangement.spacedBy(Dimensions.Spacing),
+ ) {
+ items(
+ count = list.size,
+ key = { index -> list[index].key },
+ span = { index -> GridItemSpan(list[index].size.span) },
+ ) { index ->
+ val cardModifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth)
+ val size =
+ SizeF(
+ Dimensions.CardWidth.value,
+ list[index].size.dp().value,
+ )
+ if (isEditMode && dragDropState != null) {
+ DraggableItem(dragDropState = dragDropState, enabled = true, index = index) {
+ isDragging ->
+ val elevation by animateDpAsState(if (isDragging) 4.dp else 1.dp)
+ CommunalContent(
+ modifier = cardModifier,
+ deleteOnClick = viewModel::onDeleteWidget,
+ elevation = elevation,
+ model = list[index],
+ viewModel = viewModel,
+ size = size,
+ )
+ }
+ } else {
+ CommunalContent(
+ modifier = cardModifier,
+ model = list[index],
+ viewModel = viewModel,
+ size = size,
+ )
+ }
+ }
+ }
+}
+
@Composable
private fun CommunalContent(
model: CommunalContentModel,
- viewModel: CommunalViewModel,
+ viewModel: BaseCommunalViewModel,
size: SizeF,
- deleteOnClick: (id: Int) -> Unit,
modifier: Modifier = Modifier,
+ elevation: Dp = 0.dp,
+ deleteOnClick: ((id: Int) -> Unit)? = null,
) {
when (model) {
- is CommunalContentModel.Widget -> WidgetContent(model, size, deleteOnClick, modifier)
+ is CommunalContentModel.Widget ->
+ WidgetContent(model, size, elevation, deleteOnClick, modifier)
is CommunalContentModel.Smartspace -> SmartspaceContent(model, modifier)
is CommunalContentModel.Tutorial -> TutorialContent(modifier)
is CommunalContentModel.Umo -> Umo(viewModel, modifier)
@@ -126,18 +183,19 @@
private fun WidgetContent(
model: CommunalContentModel.Widget,
size: SizeF,
- deleteOnClick: (id: Int) -> Unit,
+ elevation: Dp,
+ deleteOnClick: ((id: Int) -> Unit)?,
modifier: Modifier = Modifier,
) {
// TODO(b/309009246): update background color
- Box(
+ Card(
modifier = modifier.fillMaxSize().background(Color.White),
+ elevation = CardDefaults.cardElevation(draggedElevation = elevation),
) {
- IconButton(onClick = { deleteOnClick(model.appWidgetId) }) {
- Icon(
- Icons.Default.Close,
- LocalContext.current.getString(R.string.button_to_remove_widget)
- )
+ if (deleteOnClick != null) {
+ IconButton(onClick = { deleteOnClick(model.appWidgetId) }) {
+ Icon(Icons.Default.Close, stringResource(R.string.button_to_remove_widget))
+ }
}
AndroidView(
modifier = modifier,
@@ -171,7 +229,7 @@
}
@Composable
-private fun Umo(viewModel: CommunalViewModel, modifier: Modifier = Modifier) {
+private fun Umo(viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = {
@@ -199,7 +257,7 @@
}
}
-private object Dimensions {
+object Dimensions {
val CardWidth = 464.dp
val CardHeightFull = 630.dp
val CardHeightHalf = 307.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
new file mode 100644
index 0000000..89c5765
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.communal.ui.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
+
+@Composable
+fun rememberContentListState(
+ communalContent: List<CommunalContentModel>,
+ viewModel: CommunalEditModeViewModel,
+): ContentListState {
+ return remember(communalContent) {
+ ContentListState(
+ communalContent,
+ viewModel::onDeleteWidget,
+ viewModel::onReorderWidgets,
+ )
+ }
+}
+
+/**
+ * Keeps the current state of the [CommunalContentModel] list being edited. [GridDragDropState]
+ * interacts with this class to update the order in the list. [onSaveList] should be called on
+ * dragging ends to persist the state in db for better performance.
+ */
+class ContentListState
+internal constructor(
+ communalContent: List<CommunalContentModel>,
+ private val onDeleteWidget: (id: Int) -> Unit,
+ private val onReorderWidgets: (ids: List<Int>) -> Unit,
+) {
+ var list by mutableStateOf(communalContent)
+ private set
+
+ /** Move item to a new position in the list. */
+ fun onMove(fromIndex: Int, toIndex: Int) {
+ list = list.toMutableList().apply { add(toIndex, removeAt(fromIndex)) }
+ }
+
+ /** Remove widget from the list and the database. */
+ fun onRemove(indexToRemove: Int) {
+ if (list[indexToRemove] is CommunalContentModel.Widget) {
+ val widget = list[indexToRemove] as CommunalContentModel.Widget
+ list = list.toMutableList().apply { removeAt(indexToRemove) }
+ onDeleteWidget(widget.appWidgetId)
+ }
+ }
+
+ /** Persist the new order with all the movements happened during dragging. */
+ fun onSaveList() {
+ val widgetIds: List<Int> =
+ list.filterIsInstance<CommunalContentModel.Widget>().map { it.appWidgetId }
+ onReorderWidgets(widgetIds)
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
new file mode 100644
index 0000000..6cfa2f2
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt
@@ -0,0 +1,247 @@
+/*
+ * 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.communal.ui.compose
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
+import androidx.compose.foundation.lazy.grid.LazyGridItemScope
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.toOffset
+import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.zIndex
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.launch
+
+@Composable
+fun rememberGridDragDropState(
+ gridState: LazyGridState,
+ contentListState: ContentListState
+): GridDragDropState {
+ val scope = rememberCoroutineScope()
+ val state =
+ remember(gridState, contentListState) {
+ GridDragDropState(state = gridState, contentListState = contentListState, scope = scope)
+ }
+ LaunchedEffect(state) {
+ while (true) {
+ val diff = state.scrollChannel.receive()
+ gridState.scrollBy(diff)
+ }
+ }
+ return state
+}
+
+/**
+ * Handles drag and drop cards in the glanceable hub. While dragging to move, other items that are
+ * affected will dynamically get positioned and the state is tracked by [ContentListState]. When
+ * dragging to remove, affected cards will be moved and [ContentListState.onRemove] is called to
+ * remove the dragged item. On dragging ends, call [ContentListState.onSaveList] to persist the
+ * change.
+ */
+class GridDragDropState
+internal constructor(
+ private val state: LazyGridState,
+ private val contentListState: ContentListState,
+ private val scope: CoroutineScope,
+) {
+ var draggingItemIndex by mutableStateOf<Int?>(null)
+ private set
+
+ internal val scrollChannel = Channel<Float>()
+
+ private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
+ private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
+ internal val draggingItemOffset: Offset
+ get() =
+ draggingItemLayoutInfo?.let { item ->
+ draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset()
+ }
+ ?: Offset.Zero
+
+ private val draggingItemLayoutInfo: LazyGridItemInfo?
+ get() = state.layoutInfo.visibleItemsInfo.firstOrNull { it.index == draggingItemIndex }
+
+ internal fun onDragStart(offset: Offset) {
+ state.layoutInfo.visibleItemsInfo
+ .firstOrNull { item ->
+ item.isEditable &&
+ offset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+ offset.y.toInt() in item.offset.y..item.offsetEnd.y
+ }
+ ?.apply {
+ draggingItemIndex = index
+ draggingItemInitialOffset = this.offset.toOffset()
+ }
+ }
+
+ internal fun onDragInterrupted() {
+ if (draggingItemIndex != null) {
+ // persist list editing changes on dragging ends
+ contentListState.onSaveList()
+ draggingItemIndex = null
+ }
+ draggingItemDraggedDelta = Offset.Zero
+ draggingItemInitialOffset = Offset.Zero
+ }
+
+ internal fun onDrag(offset: Offset) {
+ draggingItemDraggedDelta += offset
+
+ val draggingItem = draggingItemLayoutInfo ?: return
+ val startOffset = draggingItem.offset.toOffset() + draggingItemOffset
+ val endOffset = startOffset + draggingItem.size.toSize()
+ val middleOffset = startOffset + (endOffset - startOffset) / 2f
+
+ val targetItem =
+ state.layoutInfo.visibleItemsInfo.find { item ->
+ item.isEditable &&
+ middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
+ middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
+ draggingItem.index != item.index
+ }
+
+ if (targetItem != null) {
+ val scrollToIndex =
+ if (targetItem.index == state.firstVisibleItemIndex) {
+ draggingItem.index
+ } else if (draggingItem.index == state.firstVisibleItemIndex) {
+ targetItem.index
+ } else {
+ null
+ }
+ if (scrollToIndex != null) {
+ scope.launch {
+ // this is needed to neutralize automatic keeping the first item first.
+ state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
+ contentListState.onMove(draggingItem.index, targetItem.index)
+ }
+ } else {
+ contentListState.onMove(draggingItem.index, targetItem.index)
+ }
+ draggingItemIndex = targetItem.index
+ } else {
+ val overscroll = checkForOverscroll(startOffset, endOffset)
+ if (overscroll != 0f) {
+ scrollChannel.trySend(overscroll)
+ }
+ val removeOffset = checkForRemove(startOffset)
+ if (removeOffset != 0f) {
+ draggingItemIndex?.let {
+ contentListState.onRemove(it)
+ draggingItemIndex = null
+ }
+ }
+ }
+ }
+
+ private val LazyGridItemInfo.offsetEnd: IntOffset
+ get() = this.offset + this.size
+
+ /** Whether the grid item can be dragged or be a drop target. Only widget card is editable. */
+ private val LazyGridItemInfo.isEditable: Boolean
+ get() = contentListState.list[this.index] is CommunalContentModel.Widget
+
+ /** Calculate the amount dragged out of bound on both sides. Returns 0f if not overscrolled */
+ private fun checkForOverscroll(startOffset: Offset, endOffset: Offset): Float {
+ return when {
+ draggingItemDraggedDelta.x > 0 ->
+ (endOffset.x - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
+ draggingItemDraggedDelta.x < 0 ->
+ (startOffset.x - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
+ else -> 0f
+ }
+ }
+
+ // TODO(b/309968801): a temporary solution to decide whether to remove card when it's dragged up
+ // and out of grid. Once we have a taskbar, calculate the intersection of the dragged item with
+ // the Remove button.
+ private fun checkForRemove(startOffset: Offset): Float {
+ return if (draggingItemDraggedDelta.y < 0)
+ (startOffset.y + Dimensions.CardHeightHalf.value - state.layoutInfo.viewportStartOffset)
+ .coerceAtMost(0f)
+ else 0f
+ }
+}
+
+private operator fun IntOffset.plus(size: IntSize): IntOffset {
+ return IntOffset(x + size.width, y + size.height)
+}
+
+private operator fun Offset.plus(size: Size): Offset {
+ return Offset(x + size.width, y + size.height)
+}
+
+fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier {
+ return pointerInput(dragDropState) {
+ detectDragGesturesAfterLongPress(
+ onDrag = { change, offset ->
+ change.consume()
+ dragDropState.onDrag(offset = offset)
+ },
+ onDragStart = { offset -> dragDropState.onDragStart(offset) },
+ onDragEnd = { dragDropState.onDragInterrupted() },
+ onDragCancel = { dragDropState.onDragInterrupted() }
+ )
+ }
+}
+
+/** Wrap LazyGrid item with additional modifier needed for drag and drop. */
+@ExperimentalFoundationApi
+@Composable
+fun LazyGridItemScope.DraggableItem(
+ dragDropState: GridDragDropState,
+ index: Int,
+ enabled: Boolean,
+ modifier: Modifier = Modifier,
+ content: @Composable (isDragging: Boolean) -> Unit
+) {
+ if (!enabled) {
+ return Box(modifier = modifier) { content(false) }
+ }
+ val dragging = index == dragDropState.draggingItemIndex
+ val draggingModifier =
+ if (dragging) {
+ Modifier.zIndex(1f).graphicsLayer {
+ translationX = dragDropState.draggingItemOffset.x
+ translationY = dragDropState.draggingItemOffset.y
+ }
+ } else {
+ Modifier.animateItemPlacement()
+ }
+ Box(modifier = modifier.then(draggingModifier), propagateMinConstraints = true) {
+ content(dragging)
+ }
+}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index c51287a..b00c886 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -27,7 +27,6 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
@@ -536,24 +535,6 @@
) : NestedScrollHandler {
override val connection: PriorityNestedScrollConnection = nestedScrollConnection()
- private fun Offset.toAmount() =
- when (gestureHandler.orientation) {
- Orientation.Horizontal -> x
- Orientation.Vertical -> y
- }
-
- private fun Velocity.toAmount() =
- when (gestureHandler.orientation) {
- Orientation.Horizontal -> x
- Orientation.Vertical -> y
- }
-
- private fun Float.toOffset() =
- when (gestureHandler.orientation) {
- Orientation.Horizontal -> Offset(x = this, y = 0f)
- Orientation.Vertical -> Offset(x = 0f, y = this)
- }
-
private fun nestedScrollConnection(): PriorityNestedScrollConnection {
// If we performed a long gesture before entering priority mode, we would have to avoid
// moving on to the next scene.
@@ -591,13 +572,12 @@
}
return PriorityNestedScrollConnection(
+ orientation = gestureHandler.orientation,
canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
- canChangeScene = offsetBeforeStart == Offset.Zero
+ canChangeScene = offsetBeforeStart == 0f
val canInterceptSwipeTransition =
- canChangeScene &&
- gestureHandler.isDrivingTransition &&
- offsetAvailable.toAmount() != 0f
+ canChangeScene && gestureHandler.isDrivingTransition && offsetAvailable != 0f
if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false
val progress = gestureHandler.swipeTransition.progress
@@ -618,15 +598,14 @@
!shouldSnapToIdle
},
canStartPostScroll = { offsetAvailable, offsetBeforeStart ->
- val amount = offsetAvailable.toAmount()
val behavior: NestedScrollBehavior =
when {
- amount > 0 -> startBehavior
- amount < 0 -> endBehavior
+ offsetAvailable > 0f -> startBehavior
+ offsetAvailable < 0f -> endBehavior
else -> return@PriorityNestedScrollConnection false
}
- val isZeroOffset = offsetBeforeStart == Offset.Zero
+ val isZeroOffset = offsetBeforeStart == 0f
when (behavior) {
NestedScrollBehavior.DuringTransitionBetweenScenes -> {
@@ -635,30 +614,29 @@
}
NestedScrollBehavior.EdgeNoOverscroll -> {
canChangeScene = isZeroOffset
- isZeroOffset && hasNextScene(amount)
+ isZeroOffset && hasNextScene(offsetAvailable)
}
NestedScrollBehavior.EdgeWithOverscroll -> {
canChangeScene = isZeroOffset
- hasNextScene(amount)
+ hasNextScene(offsetAvailable)
}
NestedScrollBehavior.Always -> {
canChangeScene = true
- hasNextScene(amount)
+ hasNextScene(offsetAvailable)
}
}
},
canStartPostFling = { velocityAvailable ->
- val amount = velocityAvailable.toAmount()
val behavior: NestedScrollBehavior =
when {
- amount > 0 -> startBehavior
- amount < 0 -> endBehavior
+ velocityAvailable > 0f -> startBehavior
+ velocityAvailable < 0f -> endBehavior
else -> return@PriorityNestedScrollConnection false
}
// We could start an overscroll animation
canChangeScene = false
- behavior.canStartOnPostFling && hasNextScene(amount)
+ behavior.canStartOnPostFling && hasNextScene(velocityAvailable)
},
canContinueScroll = { true },
onStart = {
@@ -671,24 +649,22 @@
},
onScroll = { offsetAvailable ->
if (gestureHandler.gestureWithPriority != this) {
- return@PriorityNestedScrollConnection Offset.Zero
+ return@PriorityNestedScrollConnection 0f
}
- val amount = offsetAvailable.toAmount()
-
// TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is
// initiated in a nested child.
- gestureHandler.onDrag(amount)
+ gestureHandler.onDrag(offsetAvailable)
- amount.toOffset()
+ offsetAvailable
},
onStop = { velocityAvailable ->
if (gestureHandler.gestureWithPriority != this) {
- return@PriorityNestedScrollConnection Velocity.Zero
+ return@PriorityNestedScrollConnection 0f
}
gestureHandler.onDragStopped(
- velocity = velocityAvailable.toAmount(),
+ velocity = velocityAvailable,
canChangeScene = canChangeScene
)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index 824c10b..a5fd1bf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -16,10 +16,12 @@
package com.android.compose.nestedscroll
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity
+import com.android.compose.ui.util.SpaceVectorConverter
/**
* This [NestedScrollConnection] waits for a child to scroll ([onPreScroll] or [onPostScroll]), and
@@ -147,3 +149,35 @@
return onStop(velocity)
}
}
+
+fun PriorityNestedScrollConnection(
+ orientation: Orientation,
+ canStartPreScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
+ canStartPostScroll: (offsetAvailable: Float, offsetBeforeStart: Float) -> Boolean,
+ canStartPostFling: (velocityAvailable: Float) -> Boolean,
+ canContinueScroll: () -> Boolean,
+ onStart: () -> Unit,
+ onScroll: (offsetAvailable: Float) -> Float,
+ onStop: (velocityAvailable: Float) -> Float,
+) =
+ with(SpaceVectorConverter(orientation)) {
+ PriorityNestedScrollConnection(
+ canStartPreScroll = { offsetAvailable: Offset, offsetBeforeStart: Offset ->
+ canStartPreScroll(offsetAvailable.toFloat(), offsetBeforeStart.toFloat())
+ },
+ canStartPostScroll = { offsetAvailable: Offset, offsetBeforeStart: Offset ->
+ canStartPostScroll(offsetAvailable.toFloat(), offsetBeforeStart.toFloat())
+ },
+ canStartPostFling = { velocityAvailable: Velocity ->
+ canStartPostFling(velocityAvailable.toFloat())
+ },
+ canContinueScroll = canContinueScroll,
+ onStart = onStart,
+ onScroll = { offsetAvailable: Offset ->
+ onScroll(offsetAvailable.toFloat()).toOffset()
+ },
+ onStop = { velocityAvailable: Velocity ->
+ onStop(velocityAvailable.toFloat()).toVelocity()
+ },
+ )
+ }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/SpaceVectorConverter.kt b/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/SpaceVectorConverter.kt
new file mode 100644
index 0000000..a13e944
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/SpaceVectorConverter.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.compose.ui.util
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Velocity
+
+interface SpaceVectorConverter {
+ fun Offset.toFloat(): Float
+ fun Velocity.toFloat(): Float
+ fun Float.toOffset(): Offset
+ fun Float.toVelocity(): Velocity
+}
+
+fun SpaceVectorConverter(orientation: Orientation) =
+ when (orientation) {
+ Orientation.Horizontal -> HorizontalConverter
+ Orientation.Vertical -> VerticalConverter
+ }
+
+private val HorizontalConverter =
+ object : SpaceVectorConverter {
+ override fun Offset.toFloat() = x
+ override fun Velocity.toFloat() = x
+ override fun Float.toOffset() = Offset(this, 0f)
+ override fun Float.toVelocity() = Velocity(this, 0f)
+ }
+
+private val VerticalConverter =
+ object : SpaceVectorConverter {
+ override fun Offset.toFloat() = y
+ override fun Velocity.toFloat() = y
+ override fun Float.toOffset() = Offset(0f, this)
+ override fun Float.toVelocity() = Velocity(0f, this)
+ }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
new file mode 100644
index 0000000..ce7db80
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.communal.view.viewmodel
+
+import android.app.smartspace.SmartspaceTarget
+import android.provider.Settings
+import android.widget.RemoteViews
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
+import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
+import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalEditModeViewModelTest : SysuiTestCase() {
+ @Mock private lateinit var mediaHost: MediaHost
+
+ private lateinit var testScope: TestScope
+
+ private lateinit var keyguardRepository: FakeKeyguardRepository
+ private lateinit var communalRepository: FakeCommunalRepository
+ private lateinit var tutorialRepository: FakeCommunalTutorialRepository
+ private lateinit var widgetRepository: FakeCommunalWidgetRepository
+ private lateinit var smartspaceRepository: FakeSmartspaceRepository
+ private lateinit var mediaRepository: FakeCommunalMediaRepository
+
+ private lateinit var underTest: CommunalEditModeViewModel
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ testScope = TestScope()
+
+ val withDeps = CommunalInteractorFactory.create()
+ keyguardRepository = withDeps.keyguardRepository
+ communalRepository = withDeps.communalRepository
+ tutorialRepository = withDeps.tutorialRepository
+ widgetRepository = withDeps.widgetRepository
+ smartspaceRepository = withDeps.smartspaceRepository
+ mediaRepository = withDeps.mediaRepository
+
+ underTest =
+ CommunalEditModeViewModel(
+ withDeps.communalInteractor,
+ mediaHost,
+ )
+ }
+
+ @Test
+ fun communalContent_onlyWidgetsAreShownInEditMode() =
+ testScope.runTest {
+ tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+
+ // Widgets available.
+ val widgets =
+ listOf(
+ CommunalWidgetContentModel(
+ appWidgetId = 0,
+ priority = 30,
+ providerInfo = mock(),
+ ),
+ CommunalWidgetContentModel(
+ appWidgetId = 1,
+ priority = 20,
+ providerInfo = mock(),
+ ),
+ )
+ widgetRepository.setCommunalWidgets(widgets)
+
+ // Smartspace available.
+ val target = Mockito.mock(SmartspaceTarget::class.java)
+ whenever(target.smartspaceTargetId).thenReturn("target")
+ whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+ whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
+ smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target))
+
+ // Media playing.
+ mediaRepository.mediaPlaying.value = true
+
+ val communalContent by collectLastValue(underTest.communalContent)
+
+ // Only Widgets are shown.
+ assertThat(communalContent?.size).isEqualTo(2)
+ assertThat(communalContent?.get(0))
+ .isInstanceOf(CommunalContentModel.Widget::class.java)
+ assertThat(communalContent?.get(1))
+ .isInstanceOf(CommunalContentModel.Widget::class.java)
+ }
+}
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
new file mode 100644
index 0000000..32f4d07
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -0,0 +1,148 @@
+/*
+ * 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.communal.view.viewmodel
+
+import android.app.smartspace.SmartspaceTarget
+import android.provider.Settings
+import android.widget.RemoteViews
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository
+import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository
+import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository
+import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
+import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+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
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalViewModelTest : SysuiTestCase() {
+ @Mock private lateinit var mediaHost: MediaHost
+
+ private lateinit var testScope: TestScope
+
+ private lateinit var keyguardRepository: FakeKeyguardRepository
+ private lateinit var communalRepository: FakeCommunalRepository
+ private lateinit var tutorialRepository: FakeCommunalTutorialRepository
+ private lateinit var widgetRepository: FakeCommunalWidgetRepository
+ private lateinit var smartspaceRepository: FakeSmartspaceRepository
+ private lateinit var mediaRepository: FakeCommunalMediaRepository
+
+ private lateinit var underTest: CommunalViewModel
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ testScope = TestScope()
+
+ val withDeps = CommunalInteractorFactory.create()
+ keyguardRepository = withDeps.keyguardRepository
+ communalRepository = withDeps.communalRepository
+ tutorialRepository = withDeps.tutorialRepository
+ widgetRepository = withDeps.widgetRepository
+ smartspaceRepository = withDeps.smartspaceRepository
+ mediaRepository = withDeps.mediaRepository
+
+ underTest =
+ CommunalViewModel(
+ withDeps.communalInteractor,
+ withDeps.tutorialInteractor,
+ mediaHost,
+ )
+ }
+
+ @Test
+ fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
+ testScope.runTest {
+ // Keyguard showing, and tutorial not started.
+ keyguardRepository.setKeyguardShowing(true)
+ keyguardRepository.setKeyguardOccluded(false)
+ tutorialRepository.setTutorialSettingState(
+ Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
+ )
+
+ val communalContent by collectLastValue(underTest.communalContent)
+
+ assertThat(communalContent!!).isNotEmpty()
+ communalContent!!.forEach { model ->
+ assertThat(model is CommunalContentModel.Tutorial).isTrue()
+ }
+ }
+
+ @Test
+ fun ordering_smartspaceBeforeUmoBeforeWidgets() =
+ testScope.runTest {
+ tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED)
+
+ // Widgets available.
+ val widgets =
+ listOf(
+ CommunalWidgetContentModel(
+ appWidgetId = 0,
+ priority = 30,
+ providerInfo = mock(),
+ ),
+ CommunalWidgetContentModel(
+ appWidgetId = 1,
+ priority = 20,
+ providerInfo = mock(),
+ ),
+ )
+ widgetRepository.setCommunalWidgets(widgets)
+
+ // Smartspace available.
+ val target = Mockito.mock(SmartspaceTarget::class.java)
+ whenever(target.smartspaceTargetId).thenReturn("target")
+ whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
+ whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java))
+ smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target))
+
+ // Media playing.
+ mediaRepository.mediaPlaying.value = true
+
+ val communalContent by collectLastValue(underTest.communalContent)
+
+ // Order is smart space, then UMO, then widget content.
+ assertThat(communalContent?.size).isEqualTo(4)
+ assertThat(communalContent?.get(0))
+ .isInstanceOf(CommunalContentModel.Smartspace::class.java)
+ assertThat(communalContent?.get(1)).isInstanceOf(CommunalContentModel.Umo::class.java)
+ assertThat(communalContent?.get(2))
+ .isInstanceOf(CommunalContentModel.Widget::class.java)
+ assertThat(communalContent?.get(3))
+ .isInstanceOf(CommunalContentModel.Widget::class.java)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt
new file mode 100644
index 0000000..fab290d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.impl.flashlight.domain
+
+import android.graphics.drawable.Drawable
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
+import com.android.systemui.qs.tiles.impl.flashlight.qsFlashlightTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FlashlightMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val qsTileConfig = kosmos.qsFlashlightTileConfig
+ private val mapper by lazy { FlashlightMapper(context) }
+
+ @Test
+ fun mapsDisabledDataToInactiveState() {
+ val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(false))
+
+ val actualActivationState = tileState.activationState
+
+ assertEquals(QSTileState.ActivationState.INACTIVE, actualActivationState)
+ }
+
+ @Test
+ fun mapsEnabledDataToActiveState() {
+ val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(true))
+
+ val actualActivationState = tileState.activationState
+ assertEquals(QSTileState.ActivationState.ACTIVE, actualActivationState)
+ }
+
+ @Test
+ fun mapsEnabledDataToOnIconState() {
+ val fakeDrawable = mock<Drawable>()
+ context.orCreateTestableResources.addOverride(
+ R.drawable.qs_flashlight_icon_on,
+ fakeDrawable
+ )
+ val expectedIcon = Icon.Loaded(fakeDrawable, null)
+
+ val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(true))
+
+ val actualIcon = tileState.icon()
+ assertThat(actualIcon).isEqualTo(expectedIcon)
+ }
+
+ @Test
+ fun mapsDisabledDataToOffIconState() {
+ val fakeDrawable = mock<Drawable>()
+ context.orCreateTestableResources.addOverride(
+ R.drawable.qs_flashlight_icon_off,
+ fakeDrawable
+ )
+ val expectedIcon = Icon.Loaded(fakeDrawable, null)
+
+ val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(false))
+
+ val actualIcon = tileState.icon()
+ assertThat(actualIcon).isEqualTo(expectedIcon)
+ }
+
+ @Test
+ fun supportsOnlyClickAction() {
+ val dontCare = true
+ val tileState: QSTileState = mapper.map(qsTileConfig, FlashlightTileModel(dontCare))
+
+ val supportedActions = tileState.supportedActions
+ assertThat(supportedActions).containsExactly(QSTileState.UserAction.CLICK)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt
new file mode 100644
index 0000000..00572d3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractorTest.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+import android.os.UserHandle
+import android.testing.LeakCheck
+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.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.impl.flashlight.domain.interactor.FlashlightTileDataInteractor
+import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
+import com.android.systemui.utils.leaks.FakeFlashlightController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FlashlightTileDataInteractorTest : SysuiTestCase() {
+ private lateinit var controller: FakeFlashlightController
+ private lateinit var underTest: FlashlightTileDataInteractor
+
+ @Before
+ fun setup() {
+ controller = FakeFlashlightController(LeakCheck())
+ underTest = FlashlightTileDataInteractor(controller)
+ }
+
+ @Test
+ fun availabilityOnMatchesController() = runTest {
+ controller.hasFlashlight = true
+
+ runCurrent()
+ val availability by collectLastValue(underTest.availability(TEST_USER))
+
+ assertThat(availability).isTrue()
+ }
+ @Test
+ fun availabilityOffMatchesController() = runTest {
+ controller.hasFlashlight = false
+
+ runCurrent()
+ val availability by collectLastValue(underTest.availability(TEST_USER))
+
+ assertThat(availability).isFalse()
+ }
+
+ @Test
+ fun dataMatchesController() = runTest {
+ controller.setFlashlight(false)
+ val flowValues: List<FlashlightTileModel> by
+ collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))
+
+ runCurrent()
+ controller.setFlashlight(true)
+ runCurrent()
+ controller.setFlashlight(false)
+ runCurrent()
+
+ assertThat(flowValues.size).isEqualTo(3)
+ assertThat(flowValues.map { it.isEnabled }).containsExactly(false, true, false).inOrder()
+ }
+
+ private companion object {
+ val TEST_USER = UserHandle.of(1)!!
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractorTest.kt
new file mode 100644
index 0000000..f819f53
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractorTest.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+import android.app.ActivityManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click
+import com.android.systemui.qs.tiles.impl.flashlight.domain.interactor.FlashlightTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
+import com.android.systemui.statusbar.policy.FlashlightController
+import com.android.systemui.util.mockito.mock
+import kotlinx.coroutines.test.runTest
+import org.junit.Assume.assumeFalse
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FlashlightTileUserActionInteractorTest : SysuiTestCase() {
+
+ @Mock private lateinit var controller: FlashlightController
+
+ private lateinit var underTest: FlashlightTileUserActionInteractor
+
+ @Before
+ fun setup() {
+ controller = mock<FlashlightController>()
+ underTest = FlashlightTileUserActionInteractor(controller)
+ }
+
+ @Test
+ fun handleClickToEnable() = runTest {
+ assumeFalse(ActivityManager.isUserAMonkey())
+ val stateBeforeClick = false
+
+ underTest.handleInput(click(FlashlightTileModel(stateBeforeClick)))
+
+ verify(controller).setFlashlight(!stateBeforeClick)
+ }
+
+ @Test
+ fun handleClickToDisable() = runTest {
+ assumeFalse(ActivityManager.isUserAMonkey())
+ val stateBeforeClick = true
+
+ underTest.handleInput(click(FlashlightTileModel(stateBeforeClick)))
+
+ verify(controller).setFlashlight(!stateBeforeClick)
+ }
+}
diff --git a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
deleted file mode 100644
index bc1775e..0000000
--- a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ 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.
- -->
-
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:id="@id/background"
- android:gravity="center">
- <shape android:shape="oval">
- <size
- android:height="24px"
- android:width="24px"
- />
- <solid android:color="#FFFFFFFF" />
- </shape>
- </item>
- <item android:id="@id/icon"
- android:gravity="center"
- android:width="20px"
- android:height="20px"
- android:drawable="@drawable/ic_person_outline"
- />
-</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/ic_assistant_attention_indicator.xml
new file mode 100644
index 0000000..a1e8b9d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_assistant_attention_indicator.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <group>
+ <path
+ android:pathData="M12 8.13333C12.7089 8.13333 13.2889 8.71333 13.2889 9.42221C13.2889 10.1311 12.7089 10.7111 12 10.7111C11.2911 10.7111 10.7111 10.1311 10.7111 9.42221C10.7111 8.71333 11.2911 8.13333 12 8.13333Z"
+ android:fillColor="#FFFFFF"
+ />
+ <path
+ android:pathData="M12 13.9333C13.74 13.9333 15.7378 14.7647 15.8667 15.2222V15.8667H8.13333V15.2287C8.26221 14.7647 10.26 13.9333 12 13.9333Z"
+ android:fillColor="#FFFFFF"
+ />
+ <path
+ android:pathData="M12 24C18.6274 24 24 18.6274 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24ZM9.42228 9.42224C9.42228 7.99802 10.5758 6.84447 12.0001 6.84447C13.4243 6.84447 14.5778 7.99802 14.5778 9.42224C14.5778 10.8465 13.4243 12 12.0001 12C10.5758 12 9.42228 10.8465 9.42228 9.42224ZM12 12.6445C10.2794 12.6445 6.84447 13.508 6.84447 15.2223V17.1556H17.1556V15.2223C17.1556 13.508 13.7207 12.6445 12 12.6445Z"
+ android:fillColor="#FFFFFF"
+ android:fillType="evenOdd"
+ />
+ </group>
+</vector>
+
+
diff --git a/packages/SystemUI/res/drawable/ic_person_outline.xml b/packages/SystemUI/res/drawable/ic_person_outline.xml
deleted file mode 100644
index d94714e..0000000
--- a/packages/SystemUI/res/drawable/ic_person_outline.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="24dp"
- android:height="24dp"
- android:viewportWidth="960"
- android:viewportHeight="960"
- android:tint="?attr/colorControlNormal">
- <path
- android:fillColor="@android:color/black"
- android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720L480,720Z"/>
-</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/screenshare_options_spinner_background.xml b/packages/SystemUI/res/drawable/screenshare_options_spinner_background.xml
index 34e7d0a..f7c04b5 100644
--- a/packages/SystemUI/res/drawable/screenshare_options_spinner_background.xml
+++ b/packages/SystemUI/res/drawable/screenshare_options_spinner_background.xml
@@ -26,10 +26,17 @@
</shape>
</item>
<item
- android:drawable="@drawable/ic_ksh_key_down"
- android:gravity="end|center_vertical"
- android:textColor="?androidprv:attr/textColorPrimary"
- android:width="@dimen/screenrecord_spinner_arrow_size"
- android:height="@dimen/screenrecord_spinner_arrow_size"
- android:end="20dp" />
+ android:end="20dp"
+ android:gravity="end|center_vertical">
+ <vector
+ android:width="@dimen/screenrecord_spinner_arrow_size"
+ android:height="@dimen/screenrecord_spinner_arrow_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="?androidprv:attr/colorControlNormal">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" />
+ </vector>
+ </item>
</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index ad9a775..ec2edb5 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -113,7 +113,7 @@
android:layout_width="@dimen/dream_overlay_status_bar_icon_size"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/dream_overlay_status_icon_margin"
- android:src="@drawable/dream_overlay_assistant_attention_indicator"
+ android:src="@drawable/ic_assistant_attention_indicator"
android:visibility="gone"
android:contentDescription="@string/assistant_attention_content_description" />
diff --git a/packages/SystemUI/res/layout/edit_widgets.xml b/packages/SystemUI/res/layout/edit_widgets.xml
deleted file mode 100644
index 182e651..0000000
--- a/packages/SystemUI/res/layout/edit_widgets.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/edit_widgets"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
-
- <Button
- style="@android:Widget.DeviceDefault.Button.Colored"
- android:id="@+id/add_widget"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:textSize="28sp"
- android:text="@string/hub_mode_add_widget_button_text"/>
-
-</FrameLayout>
diff --git a/packages/SystemUI/res/layout/qs_footer_impl.xml b/packages/SystemUI/res/layout/qs_footer_impl.xml
index b8f4c0f..7ab44e7 100644
--- a/packages/SystemUI/res/layout/qs_footer_impl.xml
+++ b/packages/SystemUI/res/layout/qs_footer_impl.xml
@@ -53,6 +53,8 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
+ android:focusable="true"
+ android:importantForAccessibility="no"
android:tint="?attr/shadeActive"
android:visibility="gone" />
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 74d435d..1838795 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -230,7 +230,6 @@
<!-- Communal mode -->
<item type="id" name="communal_hub" />
- <item type="id" name="communal_widget_wrapper" />
<!-- Values assigned to the views in Biometrics Prompt -->
<item type="id" name="pin_pad"/>
diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
index 9305ab6..7ccf704 100644
--- a/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
+++ b/packages/SystemUI/src/com/android/systemui/assist/AssistManager.java
@@ -7,6 +7,7 @@
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.SearchManager;
+import android.app.StatusBarManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
@@ -439,6 +440,14 @@
public void onStartPerceiving() {
mAssistUtils.enableVisualQueryDetection(
mVisualQueryDetectionAttentionListener);
+ final StatusBarManager statusBarManager =
+ mContext.getSystemService(StatusBarManager.class);
+ if (statusBarManager != null) {
+ statusBarManager.setIcon("assist_attention",
+ R.drawable.ic_assistant_attention_indicator,
+ 0, "Attention Icon for Assistant");
+ statusBarManager.setIconVisibility("assist_attention", false);
+ }
}
@Override
@@ -447,11 +456,20 @@
// accordingly).
handleVisualAttentionChanged(false);
mAssistUtils.disableVisualQueryDetection();
+ final StatusBarManager statusBarManager =
+ mContext.getSystemService(StatusBarManager.class);
+ if (statusBarManager != null) {
+ statusBarManager.removeIcon("assist_attention");
+ }
}
});
}
private void handleVisualAttentionChanged(boolean attentionGained) {
+ final StatusBarManager statusBarManager = mContext.getSystemService(StatusBarManager.class);
+ if (statusBarManager != null) {
+ statusBarManager.setIconVisibility("assist_attention", attentionGained);
+ }
mVisualQueryAttentionListeners.forEach(
attentionGained
? VisualQueryAttentionListener::onAttentionGained
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index ed6a48f..b1c5ab6 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -94,24 +94,23 @@
* @param yPx The vertical coordinate of the position of the user's pointer, in pixels.
* @param containerSizePx The size of the container of the dot grid, in pixels. It's assumed
* that the dot grid is perfectly square such that width and height are equal.
- * @param verticalOffsetPx How far down from `0` does the dot grid start on the display.
*/
- fun onDrag(xPx: Float, yPx: Float, containerSizePx: Int, verticalOffsetPx: Float) {
+ fun onDrag(xPx: Float, yPx: Float, containerSizePx: Int) {
val cellWidthPx = containerSizePx / columnCount
val cellHeightPx = containerSizePx / rowCount
- if (xPx < 0 || yPx < verticalOffsetPx) {
+ if (xPx < 0 || yPx < 0) {
return
}
val dotColumn = (xPx / cellWidthPx).toInt()
- val dotRow = ((yPx - verticalOffsetPx) / cellHeightPx).toInt()
+ val dotRow = (yPx / cellHeightPx).toInt()
if (dotColumn > columnCount - 1 || dotRow > rowCount - 1) {
return
}
val dotPixelX = dotColumn * cellWidthPx + cellWidthPx / 2
- val dotPixelY = dotRow * cellHeightPx + cellHeightPx / 2 + verticalOffsetPx
+ val dotPixelY = dotRow * cellHeightPx + cellHeightPx / 2
val distance = sqrt((xPx - dotPixelX).pow(2) + (yPx - dotPixelY).pow(2))
val hitRadius = hitFactor * min(cellWidthPx, cellHeightPx) / 2
diff --git a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
similarity index 71%
copy from packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
copy to packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
index 5e6caf0..27c9b3f 100644
--- a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/data/CommonDataLayerModule.kt
@@ -11,20 +11,17 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.common
-import com.android.systemui.common.domain.interactor.ConfigurationInteractor
-import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
+package com.android.systemui.common.data
+
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
import dagger.Binds
import dagger.Module
@Module
-abstract class CommonModule {
+abstract class CommonDataLayerModule {
@Binds abstract fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository
-
- @Binds abstract fun bindInteractor(impl: ConfigurationInteractorImpl): ConfigurationInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt b/packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
rename to packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
index 5e6caf0..7be2eaf 100644
--- a/packages/SystemUI/src/com/android/systemui/common/CommonModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/domain/CommonDomainLayerModule.kt
@@ -11,20 +11,17 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.common
+
+package com.android.systemui.common.domain
import com.android.systemui.common.domain.interactor.ConfigurationInteractor
import com.android.systemui.common.domain.interactor.ConfigurationInteractorImpl
-import com.android.systemui.common.ui.data.repository.ConfigurationRepository
-import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryImpl
import dagger.Binds
import dagger.Module
@Module
-abstract class CommonModule {
- @Binds abstract fun bindRepository(impl: ConfigurationRepositoryImpl): ConfigurationRepository
-
+abstract class CommonDomainLayerModule {
@Binds abstract fun bindInteractor(impl: ConfigurationInteractorImpl): ConfigurationInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt
new file mode 100644
index 0000000..8d04e3d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.common.ui.view
+
+import android.view.View
+
+/**
+ * Set this view's [View#importantForAccessibility] to [View#IMPORTANT_FOR_ACCESSIBILITY_YES] or
+ * [View#IMPORTANT_FOR_ACCESSIBILITY_NO] based on [value].
+ */
+fun View.setImportantForAccessibilityYesNo(value: Boolean) {
+ importantForAccessibility =
+ if (value) View.IMPORTANT_FOR_ACCESSIBILITY_YES else View.IMPORTANT_FOR_ACCESSIBILITY_NO
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
index e50850d..a12db6f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt
@@ -91,7 +91,8 @@
interface CommunalWidgetDao {
@Query(
"SELECT * FROM communal_widget_table JOIN communal_item_rank_table " +
- "ON communal_item_rank_table.uid = communal_widget_table.item_id"
+ "ON communal_item_rank_table.uid = communal_widget_table.item_id " +
+ "ORDER BY communal_item_rank_table.rank DESC"
)
fun getWidgets(): Flow<Map<CommunalItemRank, CommunalWidgetItem>>
@@ -112,6 +113,17 @@
@Query("INSERT INTO communal_item_rank_table(rank) VALUES(:rank)")
fun insertItemRank(rank: Int): Long
+ @Query("UPDATE communal_item_rank_table SET rank = :order WHERE uid = :itemUid")
+ fun updateItemRank(itemUid: Long, order: Int)
+
+ @Transaction
+ fun updateWidgetOrder(ids: List<Int>) {
+ ids.forEachIndexed { index, it ->
+ val widget = getWidgetByIdNow(it)
+ updateItemRank(widget.itemId, ids.size - index)
+ }
+ }
+
@Transaction
fun addWidget(widgetId: Int, provider: ComponentName, priority: Int): Long {
return insertWidget(
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index f7fee96..ded5581 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -61,6 +61,9 @@
/** Delete a widget by id from app widget service and the database. */
fun deleteWidget(widgetId: Int) {}
+
+ /** Update the order of widgets in the database. */
+ fun updateWidgetOrder(ids: List<Int>) {}
}
@OptIn(ExperimentalCoroutinesApi::class)
@@ -165,6 +168,15 @@
}
}
+ override fun updateWidgetOrder(ids: List<Int>) {
+ applicationScope.launch(bgDispatcher) {
+ communalWidgetDao.updateWidgetOrder(ids)
+ logger.i({ "Updated the order of widget list with ids: $str1." }) {
+ str1 = ids.toString()
+ }
+ }
+ }
+
private fun mapToContentModel(
entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
): CommunalWidgetContentModel {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 7391a5e..fd7f641 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -32,7 +32,6 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -47,7 +46,6 @@
private val widgetRepository: CommunalWidgetRepository,
mediaRepository: CommunalMediaRepository,
smartspaceRepository: SmartspaceRepository,
- tutorialInteractor: CommunalTutorialInteractor,
private val appWidgetHost: AppWidgetHost,
private val editWidgetsActivityStarter: EditWidgetsActivityStarter
) {
@@ -86,20 +84,11 @@
/** Delete a widget by id. */
fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id)
- /** A list of all the communal content to be displayed in the communal hub. */
- @OptIn(ExperimentalCoroutinesApi::class)
- val communalContent: Flow<List<CommunalContentModel>> =
- tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode ->
- if (isTutorialMode) {
- return@flatMapLatest flowOf(tutorialContent)
- }
- combine(smartspaceContent, umoContent, widgetContent) { smartspace, umo, widgets ->
- smartspace + umo + widgets
- }
- }
+ /** Reorder widgets. The order in the list will be their display order in the hub. */
+ fun updateWidgetOrder(ids: List<Int>) = widgetRepository.updateWidgetOrder(ids)
/** A list of widget content to be displayed in the communal hub. */
- private val widgetContent: Flow<List<CommunalContentModel.Widget>> =
+ val widgetContent: Flow<List<CommunalContentModel.Widget>> =
widgetRepository.communalWidgets.map { widgets ->
widgets.map Widget@{ widget ->
return@Widget CommunalContentModel.Widget(
@@ -111,7 +100,7 @@
}
/** A flow of available smartspace content. Currently only showing timer targets. */
- private val smartspaceContent: Flow<List<CommunalContentModel.Smartspace>> =
+ val smartspaceContent: Flow<List<CommunalContentModel.Smartspace>> =
if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) {
flowOf(emptyList())
} else {
@@ -133,7 +122,7 @@
}
/** A list of tutorial content to be displayed in the communal hub in tutorial mode. */
- private val tutorialContent: List<CommunalContentModel.Tutorial> =
+ val tutorialContent: List<CommunalContentModel.Tutorial> =
listOf(
CommunalContentModel.Tutorial(id = 0, CommunalContentSize.FULL),
CommunalContentModel.Tutorial(id = 1, CommunalContentSize.THIRD),
@@ -145,7 +134,7 @@
CommunalContentModel.Tutorial(id = 7, CommunalContentSize.HALF),
)
- private val umoContent: Flow<List<CommunalContentModel.Umo>> =
+ val umoContent: Flow<List<CommunalContentModel.Umo>> =
mediaRepository.mediaPlaying.flatMapLatest { mediaPlaying ->
if (mediaPlaying) {
// TODO(b/310254801): support HALF and FULL layouts
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt
deleted file mode 100644
index 0daf7b5..0000000
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/adapter/CommunalWidgetViewAdapter.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.communal.ui.adapter
-
-import android.appwidget.AppWidgetHost
-import android.appwidget.AppWidgetManager
-import android.content.Context
-import android.util.SizeF
-import com.android.systemui.communal.shared.model.CommunalAppWidgetInfo
-import com.android.systemui.communal.ui.view.CommunalWidgetWrapper
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.core.Logger
-import com.android.systemui.log.dagger.CommunalLog
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-/** Transforms a [CommunalAppWidgetInfo] to a view that renders the widget. */
-class CommunalWidgetViewAdapter
-@Inject
-constructor(
- @Application private val context: Context,
- private val appWidgetManager: AppWidgetManager,
- private val appWidgetHost: AppWidgetHost,
- @CommunalLog logBuffer: LogBuffer,
-) {
- companion object {
- private const val TAG = "CommunalWidgetViewAdapter"
- }
-
- private val logger = Logger(logBuffer, TAG)
-
- fun adapt(providerInfoFlow: Flow<CommunalAppWidgetInfo?>): Flow<CommunalWidgetWrapper?> =
- providerInfoFlow.map {
- if (it == null) {
- return@map null
- }
-
- val appWidgetId = it.appWidgetId
- val providerInfo = it.providerInfo
-
- if (appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId, providerInfo.provider)) {
- logger.d("Success binding app widget id: $appWidgetId")
- return@map CommunalWidgetWrapper(context).apply {
- addView(
- appWidgetHost.createView(context, appWidgetId, providerInfo).apply {
- // Set the widget to minimum width and height
- updateAppWidgetSize(
- appWidgetManager.getAppWidgetOptions(appWidgetId),
- listOf(
- SizeF(
- providerInfo.minResizeWidth.toFloat(),
- providerInfo.minResizeHeight.toFloat()
- )
- )
- )
- }
- )
- }
- } else {
- logger.w("Failed binding app widget id")
- return@map null
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
new file mode 100644
index 0000000..b4ab5fb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.media.controls.ui.MediaHost
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+/** The base view model for the communal hub. */
+abstract class BaseCommunalViewModel(
+ private val communalInteractor: CommunalInteractor,
+ val mediaHost: MediaHost,
+) {
+ val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene
+
+ fun onSceneChanged(scene: CommunalSceneKey) {
+ communalInteractor.onSceneChanged(scene)
+ }
+
+ /** A list of all the communal content to be displayed in the communal hub. */
+ abstract val communalContent: Flow<List<CommunalContentModel>>
+
+ /** Whether in edit mode for the communal hub. */
+ open val isEditMode = false
+
+ /** Called as the UI requests deleting a widget. */
+ open fun onDeleteWidget(id: Int) {}
+
+ /** Called as the UI requests reordering widgets. */
+ open fun onReorderWidgets(ids: List<Int>) {}
+
+ /** Called as the UI requests opening the widget editor. */
+ open fun onOpenWidgetEditor() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
new file mode 100644
index 0000000..111f8b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.communal.ui.viewmodel
+
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.model.CommunalContentModel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.media.controls.ui.MediaHost
+import com.android.systemui.media.dagger.MediaModule
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.flow.Flow
+
+/** The view model for communal hub in edit mode. */
+@SysUISingleton
+class CommunalEditModeViewModel
+@Inject
+constructor(
+ private val communalInteractor: CommunalInteractor,
+ @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
+) : BaseCommunalViewModel(communalInteractor, mediaHost) {
+
+ override val isEditMode = true
+
+ // Only widgets are editable.
+ override val communalContent: Flow<List<CommunalContentModel>> =
+ communalInteractor.widgetContent
+
+ override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
+
+ override fun onReorderWidgets(ids: List<Int>) = communalInteractor.updateWidgetOrder(ids)
+}
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 14edc8e..eaf2d57 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
@@ -17,34 +17,43 @@
package com.android.systemui.communal.ui.viewmodel
import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
-import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.media.controls.ui.MediaHost
import com.android.systemui.media.dagger.MediaModule
import javax.inject.Inject
import javax.inject.Named
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+/** The default view model used for showing the communal hub. */
@SysUISingleton
class CommunalViewModel
@Inject
constructor(
private val communalInteractor: CommunalInteractor,
- @Named(MediaModule.COMMUNAL_HUB) val mediaHost: MediaHost,
-) {
- val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene
- fun onSceneChanged(scene: CommunalSceneKey) {
- communalInteractor.onSceneChanged(scene)
- }
+ tutorialInteractor: CommunalTutorialInteractor,
+ @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
+) : BaseCommunalViewModel(communalInteractor, mediaHost) {
- /** A list of all the communal content to be displayed in the communal hub. */
- val communalContent: Flow<List<CommunalContentModel>> = communalInteractor.communalContent
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override val communalContent: Flow<List<CommunalContentModel>> =
+ tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode ->
+ if (isTutorialMode) {
+ return@flatMapLatest flowOf(communalInteractor.tutorialContent)
+ }
+ combine(
+ communalInteractor.smartspaceContent,
+ communalInteractor.umoContent,
+ communalInteractor.widgetContent,
+ ) { smartspace, umo, widgets ->
+ smartspace + umo + widgets
+ }
+ }
- /** Delete a widget by id. */
- fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id)
-
- /** Open the widget editor */
- fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor()
+ override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor()
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 78e85db..7b94fc1 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -20,17 +20,21 @@
import android.content.Intent
import android.os.Bundle
import android.util.Log
-import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.res.R
+import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
+import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent
import javax.inject.Inject
/** An Activity for editing the widgets that appear in hub mode. */
-class EditWidgetsActivity @Inject constructor(private val communalInteractor: CommunalInteractor) :
- ComponentActivity() {
+class EditWidgetsActivity
+@Inject
+constructor(
+ private val communalViewModel: CommunalEditModeViewModel,
+ private val communalInteractor: CommunalInteractor,
+) : ComponentActivity() {
companion object {
/**
* Intent extra name for the {@link AppWidgetProviderInfo} of a widget to add to hub mode.
@@ -59,20 +63,19 @@
"Failed to receive result from widget picker, code=${result.resultCode}"
)
}
- this@EditWidgetsActivity.finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- setShowWhenLocked(true)
- setContentView(R.layout.edit_widgets)
-
- val addWidgetsButton = findViewById<View>(R.id.add_widget)
- addWidgetsButton?.setOnClickListener({
- addWidgetActivityLauncher.launch(
- Intent(applicationContext, WidgetPickerActivity::class.java)
- )
- })
+ setCommunalEditWidgetActivityContent(
+ activity = this,
+ viewModel = communalViewModel,
+ onOpenWidgetPicker = {
+ addWidgetActivityLauncher.launch(
+ Intent(applicationContext, WidgetPickerActivity::class.java)
+ )
+ },
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
index 3e6dbd5..a276548 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt
@@ -43,7 +43,6 @@
super.onCreate(savedInstanceState)
setContentView(R.layout.widget_picker)
- setShowWhenLocked(true)
loadWidgets()
}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 4bdea75..65d4495 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -22,7 +22,7 @@
import android.view.WindowInsets
import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleOwner
-import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
+import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
@@ -58,6 +58,13 @@
onResult: (PeopleViewModel.Result) -> Unit,
)
+ /** Bind the content of [activity] to [viewModel]. */
+ fun setCommunalEditWidgetActivityContent(
+ activity: ComponentActivity,
+ viewModel: BaseCommunalViewModel,
+ onOpenWidgetPicker: () -> Unit,
+ )
+
/** Create a [View] to represent [viewModel] on screen. */
fun createFooterActionsView(
context: Context,
@@ -77,9 +84,9 @@
/** Create a [View] to represent [viewModel] on screen. */
fun createCommunalView(
context: Context,
- viewModel: CommunalViewModel,
+ viewModel: BaseCommunalViewModel,
): View
/** Creates a container that hosts the communal UI and handles gesture transitions. */
- fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View
+ fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View
}
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 482832b..0405ca4 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -42,7 +42,8 @@
import com.android.systemui.bouncer.ui.BouncerViewModule;
import com.android.systemui.classifier.FalsingModule;
import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
-import com.android.systemui.common.CommonModule;
+import com.android.systemui.common.data.CommonDataLayerModule;
+import com.android.systemui.common.domain.CommonDomainLayerModule;
import com.android.systemui.communal.dagger.CommunalModule;
import com.android.systemui.complication.dagger.ComplicationComponent;
import com.android.systemui.controls.dagger.ControlsModule;
@@ -176,7 +177,8 @@
ClipboardOverlayModule.class,
ClockRegistryModule.class,
CommunalModule.class,
- CommonModule.class,
+ CommonDataLayerModule.class,
+ CommonDomainLayerModule.class,
ConnectivityModule.class,
ControlsModule.class,
CoroutinesModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
index c5a8375..eee5206 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -17,13 +17,11 @@
package com.android.systemui.keyguard.ui.binder
import android.annotation.SuppressLint
-import android.content.Intent
import android.graphics.Rect
import android.graphics.drawable.Animatable2
import android.util.Size
import android.view.View
import android.view.ViewGroup
-import android.view.ViewPropertyAnimator
import android.widget.ImageView
import androidx.core.animation.CycleInterpolator
import androidx.core.animation.ObjectAnimator
@@ -34,7 +32,6 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
import com.android.settingslib.Utils
-import com.android.systemui.res.R
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.view.LaunchableLinearLayout
@@ -43,9 +40,12 @@
import com.android.systemui.common.ui.binder.TextViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils
+import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils.LAUNCH_SOURCE_KEYGUARD
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.doOnEnd
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -79,7 +79,7 @@
* Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
* it is bound.
*/
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
@Deprecated("Deprecated as part of b/278057014")
interface Binding {
/** Notifies that device configuration has changed. */
@@ -133,8 +133,7 @@
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
-
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
viewModel.startButton.collect { buttonModel ->
updateButton(
@@ -147,7 +146,7 @@
}
}
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
viewModel.endButton.collect { buttonModel ->
updateButton(
@@ -185,7 +184,7 @@
}
}
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
updateButtonAlpha(
view = startButton,
@@ -194,7 +193,7 @@
)
}
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
updateButtonAlpha(
view = endButton,
@@ -220,7 +219,7 @@
}
}
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
launch {
configurationBasedDimensions.collect { dimensions ->
startButton.updateLayoutParams<ViewGroup.LayoutParams> {
@@ -378,13 +377,14 @@
view.isClickable = viewModel.isClickable
if (viewModel.isClickable) {
if (viewModel.useLongPress) {
- val onTouchListener = KeyguardQuickAffordanceOnTouchListener(
- view,
- viewModel,
- messageDisplayer,
- vibratorHelper,
- falsingManager,
- )
+ val onTouchListener =
+ KeyguardQuickAffordanceOnTouchListener(
+ view,
+ viewModel,
+ messageDisplayer,
+ vibratorHelper,
+ falsingManager,
+ )
view.setOnTouchListener(onTouchListener)
view.setOnClickListener {
messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short)
@@ -403,9 +403,7 @@
KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds
shakeAnimator.interpolator =
CycleInterpolator(KeyguardBottomAreaVibrations.ShakeAnimationCycles)
- shakeAnimator.doOnEnd {
- view.translationX = 0f
- }
+ shakeAnimator.doOnEnd { view.translationX = 0f }
shakeAnimator.start()
vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
@@ -425,7 +423,7 @@
}
@Deprecated("Deprecated as part of b/278057014")
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private suspend fun updateButtonAlpha(
view: View,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
@@ -456,7 +454,7 @@
}
@Deprecated("Deprecated as part of b/278057014")
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private class OnLongClickListener(
private val falsingManager: FalsingManager?,
private val viewModel: KeyguardQuickAffordanceViewModel,
@@ -493,7 +491,7 @@
}
@Deprecated("Deprecated as part of b/278057014")
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private class OnClickListener(
private val viewModel: KeyguardQuickAffordanceViewModel,
private val falsingManager: FalsingManager,
@@ -535,13 +533,7 @@
view: View,
) {
activityStarter.postStartActivityDismissingKeyguard(
- Intent(Intent.ACTION_SET_WALLPAPER).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- view.context
- .getString(R.string.config_wallpaperPickerPackage)
- .takeIf { it.isNotEmpty() }
- ?.let { packageName -> setPackage(packageName) }
- },
+ WallpaperPickerIntentUtils.getIntent(view.context, LAUNCH_SOURCE_KEYGUARD),
/* delay= */ 0,
/* animationController= */ ActivityLaunchAnimator.Controller.fromView(view),
/* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls)
@@ -549,7 +541,7 @@
}
@Deprecated("Deprecated as part of b/278057014")
- //If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
+ // If updated, be sure to update [KeyguardQuickAffordanceViewBinder.kt]
private data class ConfigurationBasedDimensions(
val defaultBurnInPreventionYOffsetPx: Int,
val buttonSizePx: Size,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
index 6beef8e..8514225 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSettingsViewBinder.kt
@@ -17,19 +17,20 @@
package com.android.systemui.keyguard.ui.binder
-import android.content.Intent
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.res.R
import com.android.systemui.animation.ActivityLaunchAnimator
import com.android.systemui.animation.view.LaunchableLinearLayout
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.common.ui.binder.TextViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSettingsMenuViewModel
+import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils
+import com.android.systemui.keyguard.util.WallpaperPickerIntentUtils.LAUNCH_SOURCE_KEYGUARD
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -98,13 +99,7 @@
view: View,
) {
activityStarter.postStartActivityDismissingKeyguard(
- Intent(Intent.ACTION_SET_WALLPAPER).apply {
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- view.context
- .getString(R.string.config_wallpaperPickerPackage)
- .takeIf { it.isNotEmpty() }
- ?.let { packageName -> setPackage(packageName) }
- },
+ WallpaperPickerIntentUtils.getIntent(view.context, LAUNCH_SOURCE_KEYGUARD),
/* delay= */ 0,
/* animationController= */ ActivityLaunchAnimator.Controller.fromView(view),
/* customMessage= */ view.context.getString(R.string.keyguard_unlock_to_customize_ls)
@@ -127,5 +122,4 @@
}
.start()
}
-
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/util/WallpaperPickerIntentUtils.kt b/packages/SystemUI/src/com/android/systemui/keyguard/util/WallpaperPickerIntentUtils.kt
new file mode 100644
index 0000000..84e0566
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/util/WallpaperPickerIntentUtils.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.keyguard.util
+
+import android.content.Context
+import android.content.Intent
+import com.android.systemui.res.R
+
+/** Provides function(s) to get intent for launching the Wallpaper Picker app. */
+object WallpaperPickerIntentUtils {
+
+ fun getIntent(context: Context, launchSource: String): Intent {
+ return Intent(Intent.ACTION_SET_WALLPAPER).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ context
+ .getString(R.string.config_wallpaperPickerPackage)
+ .takeIf { it.isNotEmpty() }
+ ?.let { packageName -> setPackage(packageName) }
+ putExtra(WALLPAPER_LAUNCH_SOURCE, launchSource)
+ }
+ }
+
+ private const val WALLPAPER_LAUNCH_SOURCE = "com.android.wallpaper.LAUNCH_SOURCE"
+ const val LAUNCH_SOURCE_KEYGUARD = "app_launched_keyguard"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
index 9f5e1b7..0320dec 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarControllerImpl.java
@@ -278,6 +278,7 @@
@Override
public void onDisplayRemoved(int displayId) {
removeNavigationBar(displayId);
+ mHasNavBar.delete(displayId);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index fa18b35b..052c0da 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -12,6 +12,7 @@
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.AttributeSet;
+import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -22,6 +23,7 @@
import android.widget.Scroller;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
@@ -43,6 +45,7 @@
private static final int NO_PAGE = -1;
private static final int REVEAL_SCROLL_DURATION_MILLIS = 750;
+ private static final int SINGLE_PAGE_SCROLL_DURATION_MILLIS = 300;
private static final float BOUNCE_ANIMATION_TENSION = 1.3f;
private static final long BOUNCE_ANIMATION_DURATION = 450L;
private static final int TILE_ANIMATION_STAGGER_DELAY = 85;
@@ -63,8 +66,9 @@
private PageListener mPageListener;
private boolean mListening;
- private Scroller mScroller;
+ @VisibleForTesting Scroller mScroller;
+ /* set of animations used to indicate which tiles were just revealed */
@Nullable
private AnimatorSet mBounceAnimatorSet;
private float mLastExpansion;
@@ -306,6 +310,38 @@
mPageIndicator = indicator;
mPageIndicator.setNumPages(mPages.size());
mPageIndicator.setLocation(mPageIndicatorPosition);
+ mPageIndicator.setOnKeyListener((view, keyCode, keyEvent) -> {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need
+ // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we
+ // have a chance to intercept ACTION_UP.
+ if (keyEvent.getAction() == KeyEvent.ACTION_UP && mScroller.isFinished()) {
+ scrollByX(getDeltaXForKeyboardScrolling(keyCode),
+ SINGLE_PAGE_SCROLL_DURATION_MILLIS);
+ }
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private int getDeltaXForKeyboardScrolling(int keyCode) {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && getCurrentItem() != 0) {
+ return -getWidth();
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT
+ && getCurrentItem() != mPages.size() - 1) {
+ return getWidth();
+ }
+ return 0;
+ }
+
+ private void scrollByX(int x, int durationMillis) {
+ if (x != 0) {
+ mScroller.startScroll(/* startX= */ getScrollX(), /* startY= */ getScrollY(),
+ /* dx= */ x, /* dy= */ 0, /* duration= */ durationMillis);
+ // scroller just sets its state, we need to invalidate view to actually start scrolling
+ postInvalidateOnAnimation();
+ }
}
@Override
@@ -596,9 +632,7 @@
});
setOffscreenPageLimit(lastPageNumber); // Ensure the page to reveal has been inflated.
int dx = getWidth() * lastPageNumber;
- mScroller.startScroll(getScrollX(), getScrollY(), isLayoutRtl() ? -dx : dx, 0,
- REVEAL_SCROLL_DURATION_MILLIS);
- postInvalidateOnAnimation();
+ scrollByX(isLayoutRtl() ? -dx : dx, REVEAL_SCROLL_DURATION_MILLIS);
}
private boolean shouldNotRunAnimation(Set<String> tilesToReveal) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
new file mode 100644
index 0000000..b2b22646
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.impl.flashlight.domain
+
+import android.content.Context
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
+import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/** Maps [FlashlightTileModel] to [QSTileState]. */
+class FlashlightMapper @Inject constructor(private val context: Context) :
+ QSTileDataToStateMapper<FlashlightTileModel> {
+
+ override fun map(config: QSTileConfig, data: FlashlightTileModel): QSTileState =
+ QSTileState.build(context, config.uiConfig) {
+ val icon =
+ Icon.Loaded(
+ context.resources.getDrawable(
+ if (data.isEnabled) {
+ R.drawable.qs_flashlight_icon_on
+ } else {
+ R.drawable.qs_flashlight_icon_off
+ }
+ ),
+ contentDescription = null
+ )
+ this.icon = { icon }
+
+ if (data.isEnabled) {
+ activationState = QSTileState.ActivationState.ACTIVE
+ secondaryLabel = context.resources.getStringArray(R.array.tile_states_flashlight)[2]
+ } else {
+ activationState = QSTileState.ActivationState.INACTIVE
+ secondaryLabel = context.resources.getStringArray(R.array.tile_states_flashlight)[1]
+ }
+ contentDescription = label
+ supportedActions =
+ setOf(
+ QSTileState.UserAction.CLICK,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
new file mode 100644
index 0000000..53d4cf9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.impl.flashlight.domain.interactor
+
+import android.os.UserHandle
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
+import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
+import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
+import com.android.systemui.statusbar.policy.FlashlightController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Observes flashlight state changes providing the [FlashlightTileModel]. */
+class FlashlightTileDataInteractor
+@Inject
+constructor(
+ private val flashlightController: FlashlightController,
+) : QSTileDataInteractor<FlashlightTileModel> {
+
+ override fun tileData(
+ user: UserHandle,
+ triggers: Flow<DataUpdateTrigger>
+ ): Flow<FlashlightTileModel> = conflatedCallbackFlow {
+ val initialValue = flashlightController.isEnabled
+ trySend(FlashlightTileModel(initialValue))
+
+ val callback =
+ object : FlashlightController.FlashlightListener {
+ override fun onFlashlightChanged(enabled: Boolean) {
+ trySend(FlashlightTileModel(enabled))
+ }
+ override fun onFlashlightError() {
+ trySend(FlashlightTileModel(false))
+ }
+ override fun onFlashlightAvailabilityChanged(available: Boolean) {
+ trySend(FlashlightTileModel(flashlightController.isEnabled))
+ }
+ }
+ flashlightController.addCallback(callback)
+ awaitClose { flashlightController.removeCallback(callback) }
+ }
+
+ override fun availability(user: UserHandle): Flow<Boolean> =
+ flowOf(flashlightController.hasFlashlight())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
new file mode 100644
index 0000000..9180e30
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileUserActionInteractor.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.impl.flashlight.domain.interactor
+
+import android.app.ActivityManager
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import com.android.systemui.statusbar.policy.FlashlightController
+import javax.inject.Inject
+
+/** Handles flashlight tile clicks. */
+class FlashlightTileUserActionInteractor
+@Inject
+constructor(
+ private val flashlightController: FlashlightController,
+) : QSTileUserActionInteractor<FlashlightTileModel> {
+
+ override suspend fun handleInput(input: QSTileInput<FlashlightTileModel>) =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ if (!ActivityManager.isUserAMonkey()) {
+ flashlightController.setFlashlight(!input.data.isEnabled)
+ }
+ }
+ else -> {}
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/model/FlashlightTileModel.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
rename to packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/model/FlashlightTileModel.kt
index 039078e..ef6b2be 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/CommunalWidgetWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/model/FlashlightTileModel.kt
@@ -12,20 +12,13 @@
* 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.communal.ui.view
+package com.android.systemui.qs.tiles.impl.flashlight.domain.model
-import android.content.Context
-import android.util.AttributeSet
-import android.widget.LinearLayout
-import com.android.systemui.res.R
-
-/** Wraps around a widget rendered in communal mode. */
-class CommunalWidgetWrapper(context: Context, attrs: AttributeSet? = null) :
- LinearLayout(context, attrs) {
- init {
- id = R.id.communal_widget_wrapper
- }
-}
+/**
+ * Flashlight tile model.
+ *
+ * @param isEnabled is true when the falshlight is enabled;
+ */
+@JvmInline value class FlashlightTileModel(val isEnabled: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index a44b4b4..c810786 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -184,6 +184,8 @@
import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -604,6 +606,7 @@
private final LockscreenToOccludedTransitionViewModel mLockscreenToOccludedTransitionViewModel;
private final PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel;
private final SharedNotificationContainerInteractor mSharedNotificationContainerInteractor;
+ private final ActiveNotificationsInteractor mActiveNotificationsInteractor;
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
private final KeyguardInteractor mKeyguardInteractor;
private final PowerInteractor mPowerInteractor;
@@ -774,6 +777,7 @@
KeyguardInteractor keyguardInteractor,
ActivityStarter activityStarter,
SharedNotificationContainerInteractor sharedNotificationContainerInteractor,
+ ActiveNotificationsInteractor activeNotificationsInteractor,
KeyguardViewConfigurator keyguardViewConfigurator,
KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
SplitShadeStateController splitShadeStateController,
@@ -804,6 +808,7 @@
mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mSharedNotificationContainerInteractor = sharedNotificationContainerInteractor;
+ mActiveNotificationsInteractor = activeNotificationsInteractor;
mKeyguardInteractor = keyguardInteractor;
mPowerInteractor = powerInteractor;
mKeyguardViewConfigurator = keyguardViewConfigurator;
@@ -1795,9 +1800,14 @@
}
private boolean hasVisibleNotifications() {
- return mNotificationStackScrollLayoutController
- .getVisibleNotificationCount() != 0
- || mMediaDataManager.hasActiveMediaOrRecommendation();
+ if (FooterViewRefactor.isEnabled()) {
+ return mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()
+ || mMediaDataManager.hasActiveMediaOrRecommendation();
+ } else {
+ return mNotificationStackScrollLayoutController
+ .getVisibleNotificationCount() != 0
+ || mMediaDataManager.hasActiveMediaOrRecommendation();
+ }
}
/** Returns space between top of lock icon and bottom of NotificationStackScrollLayout. */
@@ -3004,7 +3014,9 @@
@Override
public void setBouncerShowing(boolean bouncerShowing) {
mBouncerShowing = bouncerShowing;
- mNotificationStackScrollLayoutController.updateShowEmptyShadeView();
+ if (!FooterViewRefactor.isEnabled()) {
+ mNotificationStackScrollLayoutController.updateShowEmptyShadeView();
+ }
updateVisibility();
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
index 868fbce..e84bfc5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java
@@ -87,6 +87,8 @@
import com.android.systemui.statusbar.PulseExpansionHandler;
import com.android.systemui.statusbar.QsFrameTranslateController;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
@@ -106,12 +108,12 @@
import dalvik.annotation.optimization.NeverCompile;
+import dagger.Lazy;
+
import java.io.PrintWriter;
import javax.inject.Inject;
-import dagger.Lazy;
-
/** Handles QuickSettings touch handling, expansion and animation state
* TODO (b/264460656) make this dumpable
*/
@@ -157,6 +159,7 @@
private final InteractionJankMonitor mInteractionJankMonitor;
private final ShadeRepository mShadeRepository;
private final ShadeInteractor mShadeInteractor;
+ private final ActiveNotificationsInteractor mActiveNotificationsInteractor;
private final JavaAdapter mJavaAdapter;
private final FalsingManager mFalsingManager;
private final AccessibilityManager mAccessibilityManager;
@@ -339,6 +342,7 @@
KeyguardFaceAuthInteractor keyguardFaceAuthInteractor,
ShadeRepository shadeRepository,
ShadeInteractor shadeInteractor,
+ ActiveNotificationsInteractor activeNotificationsInteractor,
JavaAdapter javaAdapter,
CastController castController,
SplitShadeStateController splitShadeStateController
@@ -386,6 +390,7 @@
mInteractionJankMonitor = interactionJankMonitor;
mShadeRepository = shadeRepository;
mShadeInteractor = shadeInteractor;
+ mActiveNotificationsInteractor = activeNotificationsInteractor;
mJavaAdapter = javaAdapter;
mLockscreenShadeTransitionController.addCallback(new LockscreenShadeTransitionCallback());
@@ -983,7 +988,9 @@
void updateQsState() {
boolean qsFullScreen = getExpanded() && !mSplitShadeEnabled;
mShadeRepository.setLegacyQsFullscreen(qsFullScreen);
- mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
+ if (!FooterViewRefactor.isEnabled()) {
+ mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
+ }
mNotificationStackScrollLayoutController.setScrollingEnabled(
mBarState != KEYGUARD && (!qsFullScreen || mExpansionFromOverscroll));
@@ -2230,8 +2237,12 @@
mLockscreenShadeTransitionController.getQSDragProgress());
setExpansionHeight(qsHeight);
}
- if (mNotificationStackScrollLayoutController.getVisibleNotificationCount() == 0
- && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
+
+ boolean hasNotifications = FooterViewRefactor.isEnabled()
+ ? mActiveNotificationsInteractor.getAreAnyNotificationsPresentValue()
+ : mNotificationStackScrollLayoutController.getVisibleNotificationCount()
+ != 0;
+ if (!hasNotifications && !mMediaDataManager.hasActiveMediaOrRecommendation()) {
// No notifications are visible, let's animate to the height of qs instead
if (isQsFragmentCreated()) {
// Let's interpolate to the header height instead of the top padding,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
index de334bb..2338be2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java
@@ -85,7 +85,9 @@
public void setFooterVisibility(@Visibility int visibility) {
mFooterVisibility = visibility;
- setSecondaryVisible(visibility == View.VISIBLE, false);
+ setSecondaryVisible(/* visible = */ visibility == View.VISIBLE,
+ /* animate = */false,
+ /* onAnimationEnded = */ null);
}
public void setFooterText(@StringRes int text) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
index 64970e4..fa2748c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt
@@ -16,17 +16,19 @@
package com.android.systemui.statusbar.notification.collection.coordinator
+import com.android.app.tracing.traceSection
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
import com.android.systemui.statusbar.notification.stack.BUCKET_SILENT
import com.android.systemui.statusbar.phone.NotificationIconAreaController
-import com.android.app.tracing.traceSection
import javax.inject.Inject
/**
@@ -40,6 +42,7 @@
private val groupExpansionManagerImpl: GroupExpansionManagerImpl,
private val notificationIconAreaController: NotificationIconAreaController,
private val renderListInteractor: RenderNotificationListInteractor,
+ private val activeNotificationsInteractor: ActiveNotificationsInteractor,
) : Coordinator {
override fun attach(pipeline: NotifPipeline) {
@@ -49,8 +52,14 @@
fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) =
traceSection("StackCoordinator.onAfterRenderList") {
- controller.setNotifStats(calculateNotifStats(entries))
- if (NotificationIconContainerRefactor.isEnabled) {
+ val notifStats = calculateNotifStats(entries)
+ if (FooterViewRefactor.isEnabled) {
+ activeNotificationsInteractor.setNotifStats(notifStats)
+ }
+ // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the footer
+ // visibility is handled in the new stack.
+ controller.setNotifStats(notifStats)
+ if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) {
renderListInteractor.setRenderedList(entries)
} else {
notificationIconAreaController.updateNotificationIcons(entries)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
index fde4ecb..a37937a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifStackController.kt
@@ -26,6 +26,7 @@
/** Data provided to the NotificationRootController whenever the pipeline runs */
data class NotifStats(
+ // TODO(b/293167744): The count can be removed from here when we remove the FooterView flag.
val numActiveNotifs: Int,
val hasNonClearableAlertingNotifs: Boolean,
val hasClearableAlertingNotifs: Boolean,
@@ -33,17 +34,16 @@
val hasClearableSilentNotifs: Boolean
) {
companion object {
- @JvmStatic
- val empty = NotifStats(0, false, false, false, false)
+ @JvmStatic val empty = NotifStats(0, false, false, false, false)
}
}
/**
* An implementation of NotifStackController which provides default, no-op implementations of each
- * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding
- * methods, rather than forcing us to add no-op implementations in their implementation every time
- * a method is added.
+ * method. This is used by ArcSystemUI so that that implementation can opt-in to overriding methods,
+ * rather than forcing us to add no-op implementations in their implementation every time a method
+ * is added.
*/
open class DefaultNotifStackController @Inject constructor() : NotifStackController {
override fun setNotifStats(stats: NotifStats) {}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
index 12ee54d..5ed82cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.data.repository
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key
import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
@@ -37,6 +38,9 @@
/** Are any already-seen notifications currently filtered out of the active list? */
val hasFilteredOutSeenNotifications = MutableStateFlow(false)
+
+ /** Stats about the list of notifications attached to the shade */
+ val notifStats = MutableStateFlow(NotifStats.empty)
}
/** Represents the notification list, comprised of groups and individual notifications. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 85ba205..31893b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -15,17 +15,19 @@
package com.android.systemui.statusbar.notification.domain.interactor
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel
import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
class ActiveNotificationsInteractor
@Inject
constructor(
- repository: ActiveNotificationListRepository,
+ private val repository: ActiveNotificationListRepository,
) {
/** Notifications actively presented to the user in the notification stack, in order. */
val topLevelRepresentativeNotifications: Flow<List<ActiveNotificationModel>> =
@@ -40,4 +42,25 @@
}
}
}
+
+ /** Are any notifications being actively presented in the notification stack? */
+ val areAnyNotificationsPresent: Flow<Boolean> =
+ repository.activeNotifications.map { it.renderList.isNotEmpty() }.distinctUntilChanged()
+
+ /**
+ * The same as [areAnyNotificationsPresent], but without flows, for easy access in synchronous
+ * code.
+ */
+ val areAnyNotificationsPresentValue: Boolean
+ get() = repository.activeNotifications.value.renderList.isNotEmpty()
+
+ /** Are there are any notifications that can be cleared by the "Clear all" button? */
+ val hasClearableNotifications: Flow<Boolean> =
+ repository.notifStats
+ .map { it.hasClearableAlertingNotifs || it.hasClearableSilentNotifs }
+ .distinctUntilChanged()
+
+ fun setNotifStats(notifStats: NotifStats) {
+ repository.notifStats.value = notifStats
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
index 10a43d5..3184d5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java
@@ -46,6 +46,7 @@
import com.android.systemui.util.DumpUtilsKt;
import java.io.PrintWriter;
+import java.util.function.Consumer;
public class FooterView extends StackScrollerDecorView {
private static final String TAG = "FooterView";
@@ -63,9 +64,13 @@
private String mSeenNotifsFilteredText;
private Drawable mSeenNotifsFilteredIcon;
+ private @StringRes int mClearAllButtonTextId;
+ private @StringRes int mClearAllButtonDescriptionId;
private @StringRes int mMessageStringId;
private @DrawableRes int mMessageIconId;
+ private OnClickListener mClearAllButtonClickListener;
+
public FooterView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -84,12 +89,18 @@
return isSecondaryVisible();
}
+ /** See {@link this#setClearAllButtonVisible(boolean, boolean, Consumer)}. */
+ public void setClearAllButtonVisible(boolean visible, boolean animate) {
+ setClearAllButtonVisible(visible, animate, /* onAnimationEnded = */ null);
+ }
+
/**
* Set the visibility of the "Clear all" button to {@code visible}. Animate the change if
* {@code animate} is true.
*/
- public void setClearAllButtonVisible(boolean visible, boolean animate) {
- setSecondaryVisible(visible, animate);
+ public void setClearAllButtonVisible(boolean visible, boolean animate,
+ Consumer<Boolean> onAnimationEnded) {
+ setSecondaryVisible(visible, animate, onAnimationEnded);
}
@Override
@@ -106,6 +117,42 @@
});
}
+ /** Set the text label for the "Clear all" button. */
+ public void setClearAllButtonText(@StringRes int textId) {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
+ if (mClearAllButtonTextId == textId) {
+ return; // nothing changed
+ }
+ mClearAllButtonTextId = textId;
+ updateClearAllButtonText();
+ }
+
+ private void updateClearAllButtonText() {
+ if (mClearAllButtonTextId == 0) {
+ return; // not initialized yet
+ }
+ mClearAllButton.setText(getContext().getString(mClearAllButtonTextId));
+ }
+
+ /** Set the accessibility content description for the "Clear all" button. */
+ public void setClearAllButtonDescription(@StringRes int contentDescriptionId) {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ return;
+ }
+ if (mClearAllButtonDescriptionId == contentDescriptionId) {
+ return; // nothing changed
+ }
+ mClearAllButtonDescriptionId = contentDescriptionId;
+ updateClearAllButtonDescription();
+ }
+
+ private void updateClearAllButtonDescription() {
+ if (mClearAllButtonDescriptionId == 0) {
+ return; // not initialized yet
+ }
+ mClearAllButton.setContentDescription(getContext().getString(mClearAllButtonDescriptionId));
+ }
+
/** Set the string for a message to be shown instead of the buttons. */
public void setMessageString(@StringRes int messageId) {
if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return;
@@ -181,6 +228,10 @@
/** Set onClickListener for the clear all (end) button. */
public void setClearAllButtonClickListener(OnClickListener listener) {
+ if (FooterViewRefactor.isEnabled()) {
+ if (mClearAllButtonClickListener == listener) return;
+ mClearAllButtonClickListener = listener;
+ }
mClearAllButton.setOnClickListener(listener);
}
@@ -214,7 +265,28 @@
mManageButton.setText(mManageNotificationText);
mManageButton.setContentDescription(mManageNotificationText);
}
- if (!FooterViewRefactor.isEnabled()) {
+ if (FooterViewRefactor.isEnabled()) {
+ updateClearAllButtonText();
+ updateClearAllButtonDescription();
+
+ updateMessageString();
+ updateMessageIcon();
+ } else {
+ // NOTE: Prior to the refactor, `updateResources` set the class properties to the right
+ // string values. It was always being called together with `updateContent`, which
+ // deals with actually associating those string values with the correct views
+ // (buttons or text).
+ // In the new code, the resource IDs are being set in the view binder (through
+ // setMessageString and similar setters). The setters themselves now deal with
+ // updating both the resource IDs and the views where appropriate (as in, calling
+ // `updateMessageString` when the resource ID changes). This eliminates the need for
+ // `updateResources`, which will eventually be removed. There are, however, still
+ // situations in which we want to update the views even if the resource IDs didn't
+ // change, such as configuration changes.
+ mClearAllButton.setText(R.string.clear_all_notifications_text);
+ mClearAllButton.setContentDescription(
+ mContext.getString(R.string.accessibility_clear_all));
+
mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText);
mSeenNotifsFooterTextView
.setCompoundDrawablesRelative(mSeenNotifsFilteredIcon, null, null, null);
@@ -230,16 +302,8 @@
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateColors();
- mClearAllButton.setText(R.string.clear_all_notifications_text);
- mClearAllButton.setContentDescription(
- mContext.getString(R.string.accessibility_clear_all));
updateResources();
updateContent();
-
- if (FooterViewRefactor.isEnabled()) {
- updateMessageString();
- updateMessageIcon();
- }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 6d823437..0299114 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -16,10 +16,14 @@
package com.android.systemui.statusbar.notification.footer.ui.viewbinder
+import android.view.View
import androidx.lifecycle.lifecycleScope
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.stopAnimating
+import com.android.systemui.util.ui.value
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
@@ -28,9 +32,31 @@
fun bind(
footer: FooterView,
viewModel: FooterViewModel,
+ clearAllNotifications: View.OnClickListener,
): DisposableHandle {
+ // Listen for changes when the view is attached.
return footer.repeatWhenAttached {
- // Listen for changes when the view is attached.
+ lifecycleScope.launch {
+ viewModel.clearAllButton.collect { button ->
+ if (button.isVisible.isAnimating) {
+ footer.setClearAllButtonVisible(
+ button.isVisible.value,
+ /* animate = */ true,
+ ) { _ ->
+ button.isVisible.stopAnimating()
+ }
+ } else {
+ footer.setClearAllButtonVisible(
+ button.isVisible.value,
+ /* animate = */ false,
+ )
+ }
+ footer.setClearAllButtonText(button.labelId)
+ footer.setClearAllButtonDescription(button.accessibilityDescriptionId)
+ footer.setClearAllButtonClickListener(clearAllNotifications)
+ }
+ }
+
lifecycleScope.launch {
viewModel.message.collect { message ->
footer.setFooterLabelVisible(message.visible)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
new file mode 100644
index 0000000..ea5abef
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.statusbar.notification.footer.ui.viewmodel
+
+import android.annotation.StringRes
+import com.android.systemui.util.ui.AnimatedValue
+
+data class FooterButtonViewModel(
+ @StringRes val labelId: Int,
+ @StringRes val accessibilityDescriptionId: Int,
+ val isVisible: AnimatedValue<Boolean>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 7390485..721bea1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -18,21 +18,50 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.ui.AnimatableEvent
+import com.android.systemui.util.ui.toAnimatedValueFlow
import dagger.Module
import dagger.Provides
import java.util.Optional
import javax.inject.Provider
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
/** ViewModel for [FooterView]. */
-class FooterViewModel(seenNotificationsInteractor: SeenNotificationsInteractor) {
- init {
- /* Check if */ FooterViewRefactor.isUnexpectedlyInLegacyMode()
- }
+class FooterViewModel(
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ seenNotificationsInteractor: SeenNotificationsInteractor,
+ shadeInteractor: ShadeInteractor,
+) {
+ val clearAllButton: Flow<FooterButtonViewModel> =
+ activeNotificationsInteractor.hasClearableNotifications
+ .sample(
+ combine(
+ shadeInteractor.isShadeFullyExpanded,
+ shadeInteractor.isShadeTouchable,
+ ::Pair
+ )
+ .onStart { emit(Pair(false, false)) }
+ ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) ->
+ val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+ AnimatableEvent(hasClearableNotifications, shouldAnimate)
+ }
+ .toAnimatedValueFlow()
+ .map { visible ->
+ FooterButtonViewModel(
+ labelId = R.string.clear_all_notifications_text,
+ accessibilityDescriptionId = R.string.accessibility_clear_all,
+ isVisible = visible,
+ )
+ }
val message: Flow<FooterMessageViewModel> =
seenNotificationsInteractor.hasFilteredOutSeenNotifications.map { hasFilteredOutNotifs ->
@@ -49,10 +78,18 @@
@Provides
@SysUISingleton
fun provideOptional(
+ activeNotificationsInteractor: Provider<ActiveNotificationsInteractor>,
seenNotificationsInteractor: Provider<SeenNotificationsInteractor>,
+ shadeInteractor: Provider<ShadeInteractor>,
): Optional<FooterViewModel> {
return if (FooterViewRefactor.isEnabled) {
- Optional.of(FooterViewModel(seenNotificationsInteractor.get()))
+ Optional.of(
+ FooterViewModel(
+ activeNotificationsInteractor.get(),
+ seenNotificationsInteractor.get(),
+ shadeInteractor.get()
+ )
+ )
} else {
Optional.empty()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
index 9f2b0a6..8e82442 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/CommonVisualInterruptionSuppressors.kt
@@ -204,6 +204,11 @@
override fun shouldSuppress(entry: NotificationEntry) = !entry.canBubble()
}
+class BubbleAppSuspendedSuppressor :
+ VisualInterruptionFilter(types = setOf(BUBBLE), reason = "app is suspended") {
+ override fun shouldSuppress(entry: NotificationEntry) = entry.ranking.isSuspended
+}
+
class BubbleNoMetadataSuppressor() :
VisualInterruptionFilter(types = setOf(BUBBLE), reason = "has no or invalid bubble metadata") {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
index 2fffd37..334e08d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptLogger.kt
@@ -56,6 +56,14 @@
}
}
+ fun logSuspendedAppBubble(entry: NotificationEntry) {
+ buffer.log(TAG, DEBUG, {
+ str1 = entry.logKey
+ }, {
+ "No bubble up: notification: app $str1 is suspended"
+ })
+ }
+
fun logNoBubbleNoMetadata(entry: NotificationEntry) {
buffer.log(TAG, DEBUG, {
str1 = entry.logKey
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index 4045380..510086d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -204,6 +204,11 @@
return false;
}
+ if (entry.getRanking().isSuspended()) {
+ mLogger.logSuspendedAppBubble(entry);
+ return false;
+ }
+
if (entry.getBubbleMetadata() == null
|| (entry.getBubbleMetadata().getShortcutId() == null
&& entry.getBubbleMetadata().getIntent() == null)) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
index 39a87de..73dd5f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderImpl.kt
@@ -159,6 +159,7 @@
addFilter(PulseLockscreenVisibilityPrivateSuppressor())
addFilter(PulseLowImportanceSuppressor())
addFilter(BubbleNotAllowedSuppressor())
+ addFilter(BubbleAppSuspendedSuppressor())
addFilter(BubbleNoMetadataSuppressor())
addFilter(HunGroupAlertBehaviorSuppressor())
addFilter(HunJustLaunchedFsiSuppressor())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index ec90a8d..162e8af 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -54,7 +54,7 @@
mContent = findContentView();
mSecondaryView = findSecondaryView();
setVisible(false /* visible */, false /* animate */);
- setSecondaryVisible(false /* visible */, false /* animate */);
+ setSecondaryVisible(false /* visible */, false /* animate */, null /* onAnimationEnd */);
setOutlineProvider(null);
}
@@ -155,15 +155,23 @@
/**
* Set the secondary view of this layout to visible.
*
- * @param visible should the secondary view be visible
- * @param animate should the change be animated
+ * @param visible True if the contents should be visible.
+ * @param animate True if we should fade to new visibility.
+ * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a
+ * parameter that represents whether the animation was cancelled.
*/
- protected void setSecondaryVisible(boolean visible, boolean animate) {
+ protected void setSecondaryVisible(boolean visible, boolean animate,
+ Consumer<Boolean> onAnimationEnded) {
if (mIsSecondaryVisible != visible) {
mSecondaryAnimating = animate;
mIsSecondaryVisible = visible;
- setViewVisible(mSecondaryView, visible, animate,
- (cancelled) -> onSecondaryVisibilityAnimationEnd());
+ Consumer<Boolean> onAnimationEndedWrapper = (cancelled) -> {
+ onContentVisibilityAnimationEnd();
+ if (onAnimationEnded != null) {
+ onAnimationEnded.accept(cancelled);
+ }
+ };
+ setViewVisible(mSecondaryView, visible, animate, onAnimationEndedWrapper);
}
if (!mSecondaryAnimating) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 38d782b..283a593 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -740,7 +740,9 @@
updateFooter();
}
- void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
+ /** Setter for filtered notifs, to be removed with the FooterViewRefactor flag. */
+ public void setHasFilteredOutSeenNotifications(boolean hasFilteredOutSeenNotifications) {
+ FooterViewRefactor.assertInLegacyMode();
mHasFilteredOutSeenNotifications = hasFilteredOutSeenNotifications;
}
@@ -749,7 +751,6 @@
if (mFooterView == null || mController == null) {
return;
}
- // TODO: move this logic to controller, which will invoke updateFooterView directly
final boolean showHistory = mController.isHistoryEnabled();
final boolean showDismissView = shouldShowDismissView();
@@ -773,13 +774,6 @@
&& !mIsRemoteInputActive;
}
- /**
- * Return whether there are any clearable notifications
- */
- boolean hasActiveClearableNotifications(@SelectedRows int selection) {
- return mController.hasActiveClearableNotifications(selection);
- }
-
public NotificationSwipeActionHelper getSwipeActionHelper() {
return mSwipeHelper;
}
@@ -1658,8 +1652,44 @@
/**
* @return the position from where the appear transition ends when expanding.
* Measured in absolute height.
+ *
+ * TODO(b/308591475): This entire logic can probably be improved as part of the empty shade
+ * refactor, but for now:
+ * - if the empty shade is visible, we can assume that the visible notif count is not 0;
+ * - if the shelf is visible, we can assume there are at least 2 notifications present (this
+ * is not true for AOD, but this logic refers to the expansion of the shade, where we never
+ * have the shelf on its own)
*/
private float getAppearEndPosition() {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ return getAppearEndPositionLegacy();
+ }
+
+ int appearPosition = mAmbientState.getStackTopMargin();
+ if (mEmptyShadeView.getVisibility() == GONE) {
+ if (isHeadsUpTransition()
+ || (mInHeadsUpPinnedMode && !mAmbientState.isDozing())) {
+ if (mShelf.getVisibility() != GONE) {
+ appearPosition += mShelf.getIntrinsicHeight() + mPaddingBetweenElements;
+ }
+ appearPosition += getTopHeadsUpPinnedHeight()
+ + getPositionInLinearLayout(mAmbientState.getTrackedHeadsUpRow());
+ } else if (mShelf.getVisibility() != GONE) {
+ appearPosition += mShelf.getIntrinsicHeight();
+ }
+ } else {
+ appearPosition = mEmptyShadeView.getHeight();
+ }
+ return appearPosition + (onKeyguard() ? mTopPadding : mIntrinsicPadding);
+ }
+
+ /**
+ * The version of {@code getAppearEndPosition} that uses the notif count. The view shouldn't
+ * need to know about that, so we want to phase this out with the footer view refactor.
+ */
+ private float getAppearEndPositionLegacy() {
+ FooterViewRefactor.assertInLegacyMode();
+
int appearPosition = mAmbientState.getStackTopMargin();
int visibleNotifCount = mController.getVisibleNotificationCount();
if (mEmptyShadeView.getVisibility() == GONE && visibleNotifCount > 0) {
@@ -1698,7 +1728,8 @@
// This can't use expansion fraction as that goes only from 0 to 1. Also when
// appear fraction for HUN is 0, expansion fraction will be already around 0.2-0.3
// and that makes translation jump immediately.
- float appearEndPosition = getAppearEndPosition();
+ float appearEndPosition = FooterViewRefactor.isEnabled() ? getAppearEndPosition()
+ : getAppearEndPositionLegacy();
float appearStartPosition = getAppearStartPosition();
float hunAppearFraction = (height - appearStartPosition)
/ (appearEndPosition - appearStartPosition);
@@ -4577,13 +4608,15 @@
if (mManageButtonClickListener != null) {
mFooterView.setManageButtonClickListener(mManageButtonClickListener);
}
- mFooterView.setClearAllButtonClickListener(v -> {
- if (mFooterClearAllListener != null) {
- mFooterClearAllListener.onClearAll();
- }
- clearNotifications(ROWS_ALL, true /* closeShade */);
- footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
- });
+ if (!FooterViewRefactor.isEnabled()) {
+ mFooterView.setClearAllButtonClickListener(v -> {
+ if (mFooterClearAllListener != null) {
+ mFooterClearAllListener.onClearAll();
+ }
+ clearNotifications(ROWS_ALL, true /* closeShade */);
+ footerView.setClearAllButtonVisible(false /* visible */, true /* animate */);
+ });
+ }
if (FooterViewRefactor.isEnabled()) {
updateFooter();
}
@@ -4599,12 +4632,21 @@
addView(mEmptyShadeView, index);
}
- void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
+ /** Legacy version, should be removed with the footer refactor flag. */
+ public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
+ FooterViewRefactor.assertInLegacyMode();
+ updateEmptyShadeView(visible, areNotificationsHiddenInShade,
+ mHasFilteredOutSeenNotifications);
+ }
+
+ /** Trigger an update for the empty shade resources and visibility. */
+ public void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade,
+ boolean hasFilteredOutSeenNotifications) {
mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
if (areNotificationsHiddenInShade) {
updateEmptyShadeView(R.string.dnd_suppressing_shade_text, 0, 0);
- } else if (mHasFilteredOutSeenNotifications) {
+ } else if (hasFilteredOutSeenNotifications) {
updateEmptyShadeView(
R.string.no_unseen_notif_text,
R.string.unlock_to_see_notif_text,
@@ -4647,9 +4689,9 @@
}
boolean animate = mIsExpanded && mAnimationsEnabled;
mFooterView.setVisible(visible, animate);
- mFooterView.setClearAllButtonVisible(showDismissView, animate);
mFooterView.showHistory(showHistory);
if (!FooterViewRefactor.isEnabled()) {
+ mFooterView.setClearAllButtonVisible(showDismissView, animate);
mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications);
}
}
@@ -4781,10 +4823,13 @@
public void removeContainerView(View v) {
Assert.isMainThread();
removeView(v);
- if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) {
- mController.updateShowEmptyShadeView();
- updateFooter();
- mController.updateImportantForAccessibility();
+ if (!FooterViewRefactor.isEnabled()) {
+ // A notification was removed, and we're not currently showing the empty shade view.
+ if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) {
+ mController.updateShowEmptyShadeView();
+ updateFooter();
+ mController.updateImportantForAccessibility();
+ }
}
updateSpeedBumpIndex();
@@ -4793,10 +4838,13 @@
public void addContainerView(View v) {
Assert.isMainThread();
addView(v);
- if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) {
- mController.updateShowEmptyShadeView();
- updateFooter();
- mController.updateImportantForAccessibility();
+ if (!FooterViewRefactor.isEnabled()) {
+ // A notification was added, and we're currently showing the empty shade view.
+ if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) {
+ mController.updateShowEmptyShadeView();
+ updateFooter();
+ mController.updateImportantForAccessibility();
+ }
}
updateSpeedBumpIndex();
@@ -4806,7 +4854,9 @@
Assert.isMainThread();
ensureRemovedFromTransientContainer(v);
addView(v, index);
- if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) {
+ // A notification was added, and we're currently showing the empty shade view.
+ if (!FooterViewRefactor.isEnabled() && v instanceof ExpandableNotificationRow
+ && mController.isShowingEmptyShadeView()) {
mController.updateShowEmptyShadeView();
updateFooter();
mController.updateImportantForAccessibility();
@@ -5079,7 +5129,8 @@
if (mEmptyShadeView.getVisibility() == GONE) {
return getMinExpansionHeight();
} else {
- return getAppearEndPosition();
+ return FooterViewRefactor.isEnabled() ? getAppearEndPosition()
+ : getAppearEndPositionLegacy();
}
}
@@ -5306,11 +5357,15 @@
return viewsToRemove;
}
+ /** Clear all clearable notifications when the user requests it. */
+ public void clearAllNotifications() {
+ clearNotifications(ROWS_ALL, /* closeShade = */ true);
+ }
+
/**
* Collects a list of visible rows, and animates them away in a staggered fashion as if they
* were dismissed. Notifications are dismissed in the backend via onClearAllAnimationsEnd.
*/
- @VisibleForTesting
void clearNotifications(@SelectedRows int selection, boolean closeShade) {
// Animate-swipe all dismissable notifications, then animate the shade closed
final ArrayList<View> viewsToAnimateAway = getVisibleViewsToAnimateAway(selection);
@@ -5610,6 +5665,7 @@
}
void setFooterClearAllListener(FooterClearAllListener listener) {
+ FooterViewRefactor.assertInLegacyMode();
mFooterClearAllListener = listener;
}
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 3e140a4..e6315fd 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
@@ -20,7 +20,6 @@
import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
import static com.android.app.animation.Interpolators.STANDARD;
-
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
@@ -108,7 +107,9 @@
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
import com.android.systemui.statusbar.notification.dagger.SilentHeader;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.logging.NotificationLogger;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
@@ -223,7 +224,9 @@
@Override
public void onViewAttachedToWindow(View v) {
mConfigurationController.addCallback(mConfigurationListener);
- mZenModeController.addCallback(mZenModeControllerCallback);
+ if (!FooterViewRefactor.isEnabled()) {
+ mZenModeController.addCallback(mZenModeControllerCallback);
+ }
final int newBarState = mStatusBarStateController.getState();
if (newBarState != mBarState) {
mStateListener.onStateChanged(newBarState);
@@ -236,7 +239,9 @@
@Override
public void onViewDetachedFromWindow(View v) {
mConfigurationController.removeCallback(mConfigurationListener);
- mZenModeController.removeCallback(mZenModeControllerCallback);
+ if (!FooterViewRefactor.isEnabled()) {
+ mZenModeController.removeCallback(mZenModeControllerCallback);
+ }
mStatusBarStateController.removeCallback(mStateListener);
}
};
@@ -292,7 +297,9 @@
final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
@Override
public void onDensityOrFontScaleChanged() {
- updateShowEmptyShadeView();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateShowEmptyShadeView();
+ }
mView.reinflateViews();
}
@@ -308,7 +315,9 @@
mView.updateBgColor();
mView.updateDecorViews();
mView.reinflateViews();
- updateShowEmptyShadeView();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateShowEmptyShadeView();
+ }
updateFooter();
}
@@ -357,7 +366,9 @@
mView.updateSensitiveness(mStatusBarStateController.goingToFullShade(),
mLockscreenUserManager.isAnyProfilePublicMode());
mView.onStatePostChange(mStatusBarStateController.fromShadeLocked());
- updateImportantForAccessibility();
+ if (!FooterViewRefactor.isEnabled()) {
+ updateImportantForAccessibility();
+ }
}
};
@@ -459,7 +470,7 @@
@Override
public void onSnooze(StatusBarNotification sbn,
- NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
+ NotificationSwipeActionHelper.SnoozeOption snoozeOption) {
mNotificationsController.setNotificationSnoozed(sbn, snoozeOption);
}
@@ -581,7 +592,7 @@
@Override
public boolean updateSwipeProgress(View animView, boolean dismissable,
- float swipeProgress) {
+ float swipeProgress) {
// Returning true prevents alpha fading.
return false;
}
@@ -673,6 +684,7 @@
UiEventLogger uiEventLogger,
NotificationRemoteInputManager remoteInputManager,
VisibilityLocationProviderDelegator visibilityLocationProviderDelegator,
+ ActiveNotificationsInteractor activeNotificationsInteractor,
SeenNotificationsInteractor seenNotificationsInteractor,
NotificationListViewBinder viewBinder,
ShadeController shadeController,
@@ -747,8 +759,10 @@
mView.setClearAllAnimationListener(this::onAnimationEnd);
mView.setClearAllListener((selection) -> mUiEventLogger.log(
NotificationPanelEvent.fromSelection(selection)));
- mView.setFooterClearAllListener(() ->
- mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+ if (!FooterViewRefactor.isEnabled()) {
+ mView.setFooterClearAllListener(() ->
+ mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES));
+ }
mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive());
mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() {
@Override
@@ -840,8 +854,10 @@
mViewBinder.bind(mView, this);
- collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
- this::onKeyguardTransitionChanged);
+ if (!FooterViewRefactor.isEnabled()) {
+ collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
+ this::onKeyguardTransitionChanged);
+ }
}
private boolean isInVisibleLocation(NotificationEntry entry) {
@@ -1031,6 +1047,8 @@
}
public int getVisibleNotificationCount() {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle footer
+ // visibility in the refactored code
return mNotifStats.getNumActiveNotifs();
}
@@ -1074,7 +1092,7 @@
}
public void setOverScrollAmount(float amount, boolean onTop, boolean animate,
- boolean cancelAnimators) {
+ boolean cancelAnimators) {
mView.setOverScrollAmount(amount, onTop, animate, cancelAnimators);
}
@@ -1124,6 +1142,7 @@
}
public void setQsFullScreen(boolean fullScreen) {
+ FooterViewRefactor.assertInLegacyMode();
mView.setQsFullScreen(fullScreen);
updateShowEmptyShadeView();
}
@@ -1273,7 +1292,10 @@
public void updateVisibility(boolean visible) {
mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
- if (mView.getVisibility() == View.VISIBLE) {
+ // Refactor note: the empty shade's visibility doesn't seem to actually depend on the
+ // parent visibility (so this update seemingly doesn't do anything). Therefore, this is not
+ // modeled in the refactored code.
+ if (!FooterViewRefactor.isEnabled() && mView.getVisibility() == View.VISIBLE) {
// Synchronize EmptyShadeView visibility with the parent container.
updateShowEmptyShadeView();
updateImportantForAccessibility();
@@ -1288,6 +1310,8 @@
* are true.
*/
public void updateShowEmptyShadeView() {
+ FooterViewRefactor.assertInLegacyMode();
+
Trace.beginSection("NSSLC.updateShowEmptyShadeView");
final boolean shouldShow = getVisibleNotificationCount() == 0
@@ -1331,6 +1355,7 @@
* auto-scrolling in NSSL.
*/
public void updateImportantForAccessibility() {
+ FooterViewRefactor.assertInLegacyMode();
if (getVisibleNotificationCount() == 0 && mView.onKeyguard()) {
mView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
} else {
@@ -1338,16 +1363,6 @@
}
}
- /**
- * @return true if {@link StatusBarStateController} is in transition to the KEYGUARD
- * and false otherwise.
- */
- private boolean isInTransitionToKeyguard() {
- final int currentState = mStatusBarStateController.getState();
- final int upcomingState = mStatusBarStateController.getCurrentOrUpcomingState();
- return (currentState != upcomingState && upcomingState == KEYGUARD);
- }
-
public boolean isShowingEmptyShadeView() {
return mView.isEmptyShadeViewVisible();
}
@@ -1395,10 +1410,14 @@
* Return whether there are any clearable notifications
*/
public boolean hasActiveClearableNotifications(@SelectedRows int selection) {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
+ // visibility in the refactored code
return hasNotifications(selection, true /* clearable */);
}
public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer
+ // visibility in the refactored code
boolean hasAlertingMatchingClearable = isClearable
? mNotifStats.getHasClearableAlertingNotifs()
: mNotifStats.getHasNonClearableAlertingNotifs();
@@ -1437,7 +1456,7 @@
public RemoteInputController.Delegate createDelegate() {
return new RemoteInputController.Delegate() {
public void setRemoteInputActive(NotificationEntry entry,
- boolean remoteInputActive) {
+ boolean remoteInputActive) {
mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive);
entry.notifyHeightChanged(true /* needsAnimation */);
updateFooter();
@@ -1556,7 +1575,7 @@
}
private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove,
- @SelectedRows int selectedRows) {
+ @SelectedRows int selectedRows) {
if (selectedRows == ROWS_ALL) {
mNotifCollection.dismissAllNotifications(
mLockscreenUserManager.getCurrentUserId());
@@ -1648,7 +1667,7 @@
* Set rounded rect clipping bounds on this view.
*/
public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius,
- int bottomRadius) {
+ int bottomRadius) {
mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius);
}
@@ -1678,6 +1697,7 @@
@VisibleForTesting
void onKeyguardTransitionChanged(TransitionStep transitionStep) {
+ FooterViewRefactor.assertInLegacyMode();
boolean isTransitionToAod = transitionStep.getTo().equals(KeyguardState.AOD)
&& (transitionStep.getFrom().equals(KeyguardState.GONE)
|| transitionStep.getFrom().equals(KeyguardState.OCCLUDED));
@@ -2003,11 +2023,21 @@
private class NotifStackControllerImpl implements NotifStackController {
@Override
public void setNotifStats(@NonNull NotifStats notifStats) {
+ // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once footer visibility
+ // is handled in the refactored stack.
mNotifStats = notifStats;
- mView.setHasFilteredOutSeenNotifications(
- mSeenNotificationsInteractor.getHasFilteredOutSeenNotifications().getValue());
+
+ if (!FooterViewRefactor.isEnabled()) {
+ mView.setHasFilteredOutSeenNotifications(
+ mSeenNotificationsInteractor
+ .getHasFilteredOutSeenNotifications().getValue());
+ }
+
updateFooter();
- updateShowEmptyShadeView();
+
+ if (!FooterViewRefactor.isEnabled()) {
+ updateShowEmptyShadeView();
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index a5b87f0..4554085 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -17,9 +17,13 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.view.LayoutInflater
+import androidx.lifecycle.lifecycleScope
import com.android.app.tracing.traceSection
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.nano.MetricsProto
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.reinflateAndBindLatest
+import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
@@ -36,12 +40,15 @@
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.statusbar.policy.ConfigurationController
import javax.inject.Inject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
/** Binds a [NotificationStackScrollLayout] to its [view model][NotificationListViewModel]. */
class NotificationListViewBinder
@Inject
constructor(
private val viewModel: NotificationListViewModel,
+ private val metricsLogger: MetricsLogger,
private val configuration: ConfigurationState,
private val configurationController: ConfigurationController,
private val falsingManager: FalsingManager,
@@ -56,7 +63,16 @@
) {
bindShelf(view)
bindFooter(view)
+ bindEmptyShade(view)
bindHideList(viewController, viewModel)
+
+ view.repeatWhenAttached {
+ lifecycleScope.launch {
+ viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
+ view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+ }
+ }
+ }
}
private fun bindShelf(parentView: NotificationStackScrollLayout) {
@@ -87,7 +103,17 @@
attachToRoot = false,
) { footerView: FooterView ->
traceSection("bind FooterView") {
- val disposableHandle = FooterViewBinder.bind(footerView, footerViewModel)
+ val disposableHandle =
+ FooterViewBinder.bind(
+ footerView,
+ footerViewModel,
+ clearAllNotifications = {
+ metricsLogger.action(
+ MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES
+ )
+ parentView.clearAllNotifications()
+ },
+ )
parentView.setFooterView(footerView)
return@reinflateAndBindLatest disposableHandle
}
@@ -95,4 +121,26 @@
}
}
}
+
+ private fun bindEmptyShade(
+ parentView: NotificationStackScrollLayout,
+ ) {
+ parentView.repeatWhenAttached {
+ lifecycleScope.launch {
+ combine(
+ viewModel.shouldShowEmptyShadeView,
+ viewModel.areNotificationsHiddenInShade,
+ viewModel.hasFilteredOutSeenNotifications,
+ ::Triple
+ )
+ .collect { (shouldShow, areNotifsHidden, hasFilteredNotifs) ->
+ parentView.updateEmptyShadeView(
+ shouldShow,
+ areNotifsHidden,
+ hasFilteredNotifs,
+ )
+ }
+ }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 4f76680..569ae24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -16,10 +16,22 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel
import com.android.systemui.statusbar.notification.shelf.ui.viewmodel.NotificationShelfViewModel
+import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor
import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.onStart
/** ViewModel for the list of notifications. */
class NotificationListViewModel
@@ -27,5 +39,78 @@
constructor(
val shelf: NotificationShelfViewModel,
val hideListViewModel: HideListViewModel,
- val footer: Optional<FooterViewModel>
-)
+ val footer: Optional<FooterViewModel>,
+ activeNotificationsInteractor: ActiveNotificationsInteractor,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ seenNotificationsInteractor: SeenNotificationsInteractor,
+ shadeInteractor: ShadeInteractor,
+ zenModeInteractor: ZenModeInteractor,
+) {
+ /**
+ * We want the NSSL to be unimportant for accessibility when there are no notifications in it
+ * while the device is on lock screen, to avoid an unlabelled NSSL view in TalkBack. Otherwise,
+ * we want it to be important for accessibility to enable accessibility auto-scrolling in NSSL.
+ * See b/242235264 for more details.
+ */
+ val isImportantForAccessibility: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(true)
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ keyguardTransitionInteractor.isFinishedInStateWhere {
+ KeyguardState.lockscreenVisibleInState(it)
+ }
+ ) { hasNotifications, isOnKeyguard ->
+ hasNotifications || !isOnKeyguard
+ }
+ .distinctUntilChanged()
+ }
+ }
+
+ val shouldShowEmptyShadeView: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ combine(
+ activeNotificationsInteractor.areAnyNotificationsPresent,
+ shadeInteractor.isQsFullscreen,
+ keyguardTransitionInteractor.isInTransitionToState(KeyguardState.AOD).onStart {
+ emit(false)
+ },
+ keyguardTransitionInteractor
+ .isFinishedInState(KeyguardState.PRIMARY_BOUNCER)
+ .onStart { emit(false) }
+ ) { hasNotifications, isQsFullScreen, transitioningToAOD, isBouncerShowing ->
+ !hasNotifications &&
+ !isQsFullScreen &&
+ // Hide empty shade view when in transition to AOD.
+ // That avoids "No Notifications" blinking when transitioning to AOD.
+ // For more details, see b/228790482.
+ !transitioningToAOD &&
+ // Don't show any notification content if the bouncer is showing. See
+ // b/267060171.
+ !isBouncerShowing
+ }
+ .distinctUntilChanged()
+ }
+ }
+
+ // TODO(b/308591475): This should be tracked separately by the empty shade.
+ val areNotificationsHiddenInShade: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ zenModeInteractor.areNotificationsHiddenInShade
+ }
+ }
+
+ // TODO(b/308591475): This should be tracked separately by the empty shade.
+ val hasFilteredOutSeenNotifications: Flow<Boolean> by lazy {
+ if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) {
+ flowOf(false)
+ } else {
+ seenNotificationsInteractor.hasFilteredOutSeenNotifications
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index d66ad58..78f48bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -14,6 +14,8 @@
package com.android.systemui.statusbar.policy
+import com.android.systemui.qs.QsEventLogger
+import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tiles.AlarmTile
import com.android.systemui.qs.tiles.CameraToggleTile
@@ -23,8 +25,18 @@
import com.android.systemui.qs.tiles.MicrophoneToggleTile
import com.android.systemui.qs.tiles.UiModeNightTile
import com.android.systemui.qs.tiles.WorkModeTile
+import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.flashlight.domain.FlashlightMapper
+import com.android.systemui.qs.tiles.impl.flashlight.domain.interactor.FlashlightTileDataInteractor
+import com.android.systemui.qs.tiles.impl.flashlight.domain.interactor.FlashlightTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.flashlight.domain.model.FlashlightTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig
+import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel
+import com.android.systemui.res.R
import dagger.Binds
import dagger.Module
+import dagger.Provides
import dagger.multibindings.IntoMap
import dagger.multibindings.StringKey
@@ -40,6 +52,42 @@
@StringKey(WorkModeTile.TILE_SPEC)
fun bindWorkModeTile(workModeTile: WorkModeTile): QSTileImpl<*>
+ companion object {
+ const val FLASHLIGHT_TILE_SPEC = "flashlight"
+
+ /** Inject config */
+ @Provides
+ @IntoMap
+ @StringKey(FLASHLIGHT_TILE_SPEC)
+ fun provideFlashlightTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(FLASHLIGHT_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.qs_flashlight_icon_off,
+ labelRes = R.string.quick_settings_flashlight_label,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /** Inject FlashlightTile into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(FLASHLIGHT_TILE_SPEC)
+ fun provideFlashlightTileViewModel(
+ factory: QSTileViewModelFactory.Static<FlashlightTileModel>,
+ mapper: FlashlightMapper,
+ stateInteractor: FlashlightTileDataInteractor,
+ userActionInteractor: FlashlightTileUserActionInteractor
+ ): QSTileViewModel =
+ factory.create(
+ TileSpec.create(FLASHLIGHT_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
+ }
+
/** Inject FlashlightTile into tileMap in QSModule */
@Binds
@IntoMap
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 125fe68..862c39c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -322,7 +322,6 @@
xPx = 30f * coordinate.x + 15,
yPx = 30f * coordinate.y + 15,
containerSizePx = 90,
- verticalOffsetPx = 0f,
)
}
@@ -369,7 +368,6 @@
xPx = dotSize * coordinate.x + 15f,
yPx = dotSize * coordinate.y + 15f,
containerSizePx = containerSize,
- verticalOffsetPx = 0f,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
index 14ec4d4..16b2ed6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
@@ -124,6 +124,39 @@
assertThat(widgets()).containsExactly(communalItemRankEntry2, communalWidgetItemEntry2)
}
+ @Test
+ fun reorderWidget_emitsWidgetsInNewOrder() =
+ testScope.runTest {
+ val widgetsToAdd = listOf(widgetInfo1, widgetInfo2)
+ val widgets = collectLastValue(communalWidgetDao.getWidgets())
+
+ widgetsToAdd.forEach {
+ val (widgetId, provider, priority) = it
+ communalWidgetDao.addWidget(
+ widgetId = widgetId,
+ provider = provider,
+ priority = priority,
+ )
+ }
+ assertThat(widgets())
+ .containsExactly(
+ communalItemRankEntry1,
+ communalWidgetItemEntry1,
+ communalItemRankEntry2,
+ communalWidgetItemEntry2
+ )
+
+ val widgetIdsInNewOrder = listOf(widgetInfo2.widgetId, widgetInfo1.widgetId)
+ communalWidgetDao.updateWidgetOrder(widgetIdsInNewOrder)
+ assertThat(widgets())
+ .containsExactly(
+ communalItemRankEntry2,
+ communalWidgetItemEntry2,
+ communalItemRankEntry1,
+ communalWidgetItemEntry1
+ )
+ }
+
data class FakeWidgetMetadata(
val widgetId: Int,
val provider: ComponentName,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index 28fae81..182712a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -202,6 +202,20 @@
}
@Test
+ fun reorderWidgets_queryDb() =
+ testScope.runTest {
+ userUnlocked(true)
+ val repository = initCommunalWidgetRepository()
+ runCurrent()
+
+ val ids = listOf(104, 103, 101)
+ repository.updateWidgetOrder(ids)
+ runCurrent()
+
+ verify(communalWidgetDao).updateWidgetOrder(ids)
+ }
+
+ @Test
fun broadcastReceiver_communalDisabled_doNotRegisterUserUnlockedBroadcastReceiver() =
testScope.runTest {
communalEnabled(false)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index e0567a4..16cfa23 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -18,7 +18,6 @@
package com.android.systemui.communal.domain.interactor
import android.app.smartspace.SmartspaceTarget
-import android.provider.Settings
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -99,24 +98,6 @@
}
@Test
- fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
- testScope.runTest {
- // Keyguard showing, and tutorial not started.
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setKeyguardOccluded(false)
- tutorialRepository.setTutorialSettingState(
- Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
- )
-
- val communalContent by collectLastValue(underTest.communalContent)
-
- assertThat(communalContent!!).isNotEmpty()
- communalContent!!.forEach { model ->
- assertThat(model is CommunalContentModel.Tutorial).isTrue()
- }
- }
-
- @Test
fun widget_tutorialCompletedAndWidgetsAvailable_showWidgetContent() =
testScope.runTest {
// Keyguard showing, and tutorial completed.
@@ -145,12 +126,11 @@
)
widgetRepository.setCommunalWidgets(widgets)
- val communalContent by collectLastValue(underTest.communalContent)
+ val widgetContent by collectLastValue(underTest.widgetContent)
- assertThat(communalContent!!).isNotEmpty()
- communalContent!!.forEachIndexed { index, model ->
- assertThat((model as CommunalContentModel.Widget).appWidgetId)
- .isEqualTo(widgets[index].appWidgetId)
+ assertThat(widgetContent!!).isNotEmpty()
+ widgetContent!!.forEachIndexed { index, model ->
+ assertThat(model.appWidgetId).isEqualTo(widgets[index].appWidgetId)
}
}
@@ -183,48 +163,9 @@
val targets = listOf(target1, target2, target3)
smartspaceRepository.setLockscreenSmartspaceTargets(targets)
- val communalContent by collectLastValue(underTest.communalContent)
- assertThat(communalContent?.size).isEqualTo(1)
- assertThat(communalContent?.get(0)?.key).isEqualTo("smartspace_target3")
- }
-
- @Test
- fun smartspace_smartspaceAndWidgetsAvailable_showSmartspaceAndWidgetContent() =
- testScope.runTest {
- // Keyguard showing, and tutorial completed.
- keyguardRepository.setKeyguardShowing(true)
- keyguardRepository.setKeyguardOccluded(false)
- tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
-
- // Widgets available.
- val widgets =
- listOf(
- CommunalWidgetContentModel(
- appWidgetId = 0,
- priority = 30,
- providerInfo = mock(),
- ),
- CommunalWidgetContentModel(
- appWidgetId = 1,
- priority = 20,
- providerInfo = mock(),
- ),
- )
- widgetRepository.setCommunalWidgets(widgets)
-
- // Smartspace available.
- val target = mock(SmartspaceTarget::class.java)
- whenever(target.smartspaceTargetId).thenReturn("target")
- whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
- whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java))
- smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target))
-
- val communalContent by collectLastValue(underTest.communalContent)
-
- assertThat(communalContent?.size).isEqualTo(3)
- assertThat(communalContent?.get(0)?.key).isEqualTo("smartspace_target")
- assertThat(communalContent?.get(1)?.key).isEqualTo("widget_0")
- assertThat(communalContent?.get(2)?.key).isEqualTo("widget_1")
+ val smartspaceContent by collectLastValue(underTest.smartspaceContent)
+ assertThat(smartspaceContent?.size).isEqualTo(1)
+ assertThat(smartspaceContent?.get(0)?.key).isEqualTo("smartspace_target3")
}
@Test
@@ -236,55 +177,11 @@
// Media is playing.
mediaRepository.mediaPlaying.value = true
- val communalContent by collectLastValue(underTest.communalContent)
+ val umoContent by collectLastValue(underTest.umoContent)
- assertThat(communalContent?.size).isEqualTo(1)
- assertThat(communalContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java)
- assertThat(communalContent?.get(0)?.key).isEqualTo(CommunalContentModel.UMO_KEY)
- }
-
- @Test
- fun ordering_smartspaceBeforeUmoBeforeWidgets() =
- testScope.runTest {
- tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
-
- // Widgets available.
- val widgets =
- listOf(
- CommunalWidgetContentModel(
- appWidgetId = 0,
- priority = 30,
- providerInfo = mock(),
- ),
- CommunalWidgetContentModel(
- appWidgetId = 1,
- priority = 20,
- providerInfo = mock(),
- ),
- )
- widgetRepository.setCommunalWidgets(widgets)
-
- // Smartspace available.
- val target = mock(SmartspaceTarget::class.java)
- whenever(target.smartspaceTargetId).thenReturn("target")
- whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER)
- whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java))
- smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target))
-
- // Media playing.
- mediaRepository.mediaPlaying.value = true
-
- val communalContent by collectLastValue(underTest.communalContent)
-
- // Order is smart space, then UMO, then widget content.
- assertThat(communalContent?.size).isEqualTo(4)
- assertThat(communalContent?.get(0))
- .isInstanceOf(CommunalContentModel.Smartspace::class.java)
- assertThat(communalContent?.get(1)).isInstanceOf(CommunalContentModel.Umo::class.java)
- assertThat(communalContent?.get(2))
- .isInstanceOf(CommunalContentModel.Widget::class.java)
- assertThat(communalContent?.get(3))
- .isInstanceOf(CommunalContentModel.Widget::class.java)
+ assertThat(umoContent?.size).isEqualTo(1)
+ assertThat(umoContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java)
+ assertThat(umoContent?.get(0)?.key).isEqualTo(CommunalContentModel.UMO_KEY)
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
new file mode 100644
index 0000000..db9e548
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt
@@ -0,0 +1,86 @@
+package com.android.systemui.qs
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.view.KeyEvent
+import android.view.View
+import android.widget.Scroller
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class PagedTileLayoutTest : SysuiTestCase() {
+
+ @Mock private lateinit var pageIndicator: PageIndicator
+ @Captor private lateinit var captor: ArgumentCaptor<View.OnKeyListener>
+
+ private lateinit var pageTileLayout: TestPagedTileLayout
+ private lateinit var scroller: Scroller
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ pageTileLayout = TestPagedTileLayout(mContext)
+ pageTileLayout.setPageIndicator(pageIndicator)
+ verify(pageIndicator).setOnKeyListener(captor.capture())
+ setViewWidth(pageTileLayout, width = PAGE_WIDTH)
+ scroller = pageTileLayout.mScroller
+ }
+
+ private fun setViewWidth(view: View, width: Int) {
+ view.left = 0
+ view.right = width
+ }
+
+ @Test
+ fun scrollsRight_afterRightArrowPressed_whenFocusOnPagerIndicator() {
+ pageTileLayout.currentPageIndex = 0
+
+ sendUpEvent(KeyEvent.KEYCODE_DPAD_RIGHT)
+
+ assertThat(scroller.isFinished).isFalse() // aka we're scrolling
+ assertThat(scroller.finalX).isEqualTo(scroller.currX + PAGE_WIDTH)
+ }
+
+ @Test
+ fun scrollsLeft_afterLeftArrowPressed_whenFocusOnPagerIndicator() {
+ pageTileLayout.currentPageIndex = 1 // we won't scroll left if we're on the first page
+
+ sendUpEvent(KeyEvent.KEYCODE_DPAD_LEFT)
+
+ assertThat(scroller.isFinished).isFalse() // aka we're scrolling
+ assertThat(scroller.finalX).isEqualTo(scroller.currX - PAGE_WIDTH)
+ }
+
+ private fun sendUpEvent(keyCode: Int) {
+ val event = KeyEvent(KeyEvent.ACTION_UP, keyCode)
+ captor.value.onKey(pageIndicator, keyCode, event)
+ }
+
+ /**
+ * Custom PagedTileLayout to easy mock "currentItem" i.e. currently visible page. Setting this
+ * up otherwise would require setting adapter etc
+ */
+ class TestPagedTileLayout(context: Context) : PagedTileLayout(context, null) {
+
+ var currentPageIndex: Int = 0
+
+ override fun getCurrentItem(): Int {
+ return currentPageIndex
+ }
+ }
+
+ companion object {
+ const val PAGE_WIDTH = 200
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index ba8a666..03878b7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -147,6 +147,7 @@
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinatorLogger;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
@@ -335,6 +336,7 @@
@Mock private JavaAdapter mJavaAdapter;
@Mock private CastController mCastController;
@Mock private SharedNotificationContainerInteractor mSharedNotificationContainerInteractor;
+ @Mock private ActiveNotificationsInteractor mActiveNotificationsInteractor;
@Mock private KeyguardClockPositionAlgorithm mKeyguardClockPositionAlgorithm;
@Mock private NaturalScrollingSettingObserver mNaturalScrollingSettingObserver;
@@ -709,6 +711,7 @@
mKeyguardInteractor,
mActivityStarter,
mSharedNotificationContainerInteractor,
+ mActiveNotificationsInteractor,
mKeyguardViewConfigurator,
mKeyguardFaceAuthInteractor,
new ResourcesSplitShadeStateController(),
@@ -783,6 +786,7 @@
mKeyguardFaceAuthInteractor,
mShadeRepository,
mShadeInteractor,
+ mActiveNotificationsInteractor,
mJavaAdapter,
mCastController,
new ResourcesSplitShadeStateController()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 26b84e3..bff47f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -80,6 +80,8 @@
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.stack.AmbientState;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor;
@@ -178,6 +180,8 @@
protected SysuiStatusBarStateController mStatusBarStateController;
protected ShadeInteractor mShadeInteractor;
+ protected ActiveNotificationsInteractor mActiveNotificationsInteractor;
+
protected Handler mMainHandler;
protected LockscreenShadeTransitionController.Callback mLockscreenShadeTransitionCallback;
@@ -290,6 +294,9 @@
)
);
+ mActiveNotificationsInteractor =
+ new ActiveNotificationsInteractor(new ActiveNotificationListRepository());
+
KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -362,6 +369,7 @@
mock(KeyguardFaceAuthInteractor.class),
mShadeRepository,
mShadeInteractor,
+ mActiveNotificationsInteractor,
new JavaAdapter(mTestScope.getBackgroundScope()),
mCastController,
splitShadeStateController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
index 428574b..fa5fad0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor
import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING
@@ -57,6 +58,7 @@
@Mock private lateinit var groupExpansionManagerImpl: GroupExpansionManagerImpl
@Mock private lateinit var notificationIconAreaController: NotificationIconAreaController
@Mock private lateinit var renderListInteractor: RenderNotificationListInteractor
+ @Mock private lateinit var activeNotificationsInteractor: ActiveNotificationsInteractor
@Mock private lateinit var stackController: NotifStackController
@Mock private lateinit var section: NotifSection
@@ -75,6 +77,7 @@
groupExpansionManagerImpl,
notificationIconAreaController,
renderListInteractor,
+ activeNotificationsInteractor,
)
coordinator.attach(pipeline)
afterRenderListListener = withArgCaptor {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
new file mode 100644
index 0000000..4ab3cd4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.statusbar.notification.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.collectLastValue
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import org.junit.Test
+
+@SmallTest
+class ActiveNotificationsInteractorTest : SysuiTestCase() {
+
+ @Component(modules = [SysUITestModule::class])
+ @SysUISingleton
+ interface TestComponent : SysUITestComponent<ActiveNotificationsInteractor> {
+ val activeNotificationListRepository: ActiveNotificationListRepository
+
+ @Component.Factory
+ interface Factory {
+ fun create(@BindsInstance test: SysuiTestCase): TestComponent
+ }
+ }
+
+ private val testComponent: TestComponent =
+ DaggerActiveNotificationsInteractorTest_TestComponent.factory().create(test = this)
+
+ @Test
+ fun testAreAnyNotificationsPresent_isTrue() =
+ testComponent.runTest {
+ val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
+
+ activeNotificationListRepository.setActiveNotifs(2)
+ runCurrent()
+
+ assertThat(areAnyNotificationsPresent).isTrue()
+ assertThat(underTest.areAnyNotificationsPresentValue).isTrue()
+ }
+
+ @Test
+ fun testAreAnyNotificationsPresent_isFalse() =
+ testComponent.runTest {
+ val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent)
+
+ activeNotificationListRepository.setActiveNotifs(0)
+ runCurrent()
+
+ assertThat(areAnyNotificationsPresent).isFalse()
+ assertThat(underTest.areAnyNotificationsPresentValue).isFalse()
+ }
+
+ @Test
+ fun testHasClearableNotifications_whenHasClearableAlertingNotifs() =
+ testComponent.runTest {
+ val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = false,
+ )
+ runCurrent()
+
+ assertThat(hasClearable).isTrue()
+ }
+
+ @Test
+ fun testHasClearableNotifications_whenHasClearableSilentNotifs() =
+ testComponent.runTest {
+ val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = false,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ assertThat(hasClearable).isTrue()
+ }
+
+ @Test
+ fun testHasClearableNotifications_whenHasNoClearableNotifs() =
+ testComponent.runTest {
+ val hasClearable by collectLastValue(underTest.hasClearableNotifications)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = false,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = false,
+ )
+ runCurrent()
+
+ assertThat(hasClearable).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
index a64ac67..22c5bae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java
@@ -114,9 +114,46 @@
}
@Test
+ public void testSetClearAllButtonText_resourceOnlyFetchedOnce() {
+ int resId = R.string.clear_all_notifications_text;
+ mView.setClearAllButtonText(resId);
+ verify(mSpyContext).getString(eq(resId));
+
+ clearInvocations(mSpyContext);
+
+ assertThat(((TextView) mView.findViewById(R.id.dismiss_text))
+ .getText().toString()).contains("Clear all");
+
+ // Set it a few more times, it shouldn't lead to the resource being fetched again
+ mView.setClearAllButtonText(resId);
+ mView.setClearAllButtonText(resId);
+
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
+ public void testSetClearAllButtonDescription_resourceOnlyFetchedOnce() {
+ int resId = R.string.accessibility_clear_all;
+ mView.setClearAllButtonDescription(resId);
+ verify(mSpyContext).getString(eq(resId));
+
+ clearInvocations(mSpyContext);
+
+ assertThat(((TextView) mView.findViewById(R.id.dismiss_text))
+ .getContentDescription().toString()).contains("Clear all notifications");
+
+ // Set it a few more times, it shouldn't lead to the resource being fetched again
+ mView.setClearAllButtonDescription(resId);
+ mView.setClearAllButtonDescription(resId);
+
+ verify(mSpyContext, never()).getString(anyInt());
+ }
+
+ @Test
public void testSetMessageString_resourceOnlyFetchedOnce() {
- mView.setMessageString(R.string.unlock_to_see_notif_text);
- verify(mSpyContext).getString(eq(R.string.unlock_to_see_notif_text));
+ int resId = R.string.unlock_to_see_notif_text;
+ mView.setMessageString(resId);
+ verify(mSpyContext).getString(eq(resId));
clearInvocations(mSpyContext);
@@ -124,22 +161,23 @@
.getText().toString()).contains("Unlock");
// Set it a few more times, it shouldn't lead to the resource being fetched again
- mView.setMessageString(R.string.unlock_to_see_notif_text);
- mView.setMessageString(R.string.unlock_to_see_notif_text);
+ mView.setMessageString(resId);
+ mView.setMessageString(resId);
verify(mSpyContext, never()).getString(anyInt());
}
@Test
public void testSetMessageIcon_resourceOnlyFetchedOnce() {
- mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
- verify(mSpyContext).getDrawable(eq(R.drawable.ic_friction_lock_closed));
+ int resId = R.drawable.ic_friction_lock_closed;
+ mView.setMessageIcon(resId);
+ verify(mSpyContext).getDrawable(eq(resId));
clearInvocations(mSpyContext);
// Set it a few more times, it shouldn't lead to the resource being fetched again
- mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
- mView.setMessageIcon(R.drawable.ic_friction_lock_closed);
+ mView.setMessageIcon(resId);
+ mView.setMessageIcon(resId);
verify(mSpyContext, never()).getDrawable(anyInt());
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 57a7c3c..94dcf7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -18,37 +18,222 @@
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.power.shared.model.WakeSleepReason
+import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.collection.render.NotifStats
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
-import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.ui.isAnimating
+import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
+import dagger.BindsInstance
+import dagger.Component
+import java.util.Optional
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
@RunWith(AndroidTestingRunner::class)
@SmallTest
class FooterViewModelTest : SysuiTestCase() {
- private val repository = ActiveNotificationListRepository()
- private val interactor = SeenNotificationsInteractor(repository)
- private val underTest = FooterViewModel(interactor)
+ private lateinit var footerViewModel: FooterViewModel
- @Test
- fun testMessageVisible_whenFilteredNotifications() = runTest {
- val message by collectLastValue(underTest.message)
+ @SysUISingleton
+ @Component(
+ modules =
+ [
+ SysUITestModule::class,
+ ActivatableNotificationViewModelModule::class,
+ FooterViewModelModule::class,
+ HeadlessSystemUserModeModule::class,
+ ]
+ )
+ interface TestComponent : SysUITestComponent<Optional<FooterViewModel>> {
+ val activeNotificationListRepository: ActiveNotificationListRepository
+ val configurationRepository: FakeConfigurationRepository
+ val keyguardRepository: FakeKeyguardRepository
+ val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ val shadeRepository: FakeShadeRepository
+ val powerRepository: FakePowerRepository
- repository.hasFilteredOutSeenNotifications.value = true
+ @Component.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance test: SysuiTestCase,
+ featureFlags: FakeFeatureFlagsClassicModule,
+ mocks: TestMocksModule,
+ ): TestComponent
+ }
+ }
- assertThat(message?.visible).isTrue()
+ private val dozeParameters: DozeParameters = mock()
+
+ private val testComponent: TestComponent =
+ DaggerFooterViewModelTest_TestComponent.factory()
+ .create(
+ test = this,
+ featureFlags =
+ FakeFeatureFlagsClassicModule {
+ set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true)
+ },
+ mocks =
+ TestMocksModule(
+ dozeParameters = dozeParameters,
+ )
+ )
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
+
+ // The underTest in the component is Optional, because that matches the provider we
+ // currently have for the footer view model.
+ footerViewModel = testComponent.underTest.get()
}
@Test
- fun testMessageVisible_whenNoFilteredNotifications() = runTest {
- val message by collectLastValue(underTest.message)
+ fun testMessageVisible_whenFilteredNotifications() =
+ testComponent.runTest {
+ val message by collectLastValue(footerViewModel.message)
- repository.hasFilteredOutSeenNotifications.value = false
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
- assertThat(message?.visible).isFalse()
- }
+ assertThat(message?.visible).isTrue()
+ }
+
+ @Test
+ fun testMessageVisible_whenNoFilteredNotifications() =
+ testComponent.runTest {
+ val message by collectLastValue(footerViewModel.message)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+
+ assertThat(message?.visible).isFalse()
+ }
+
+ @Test
+ fun testClearAllButtonVisible_whenHasClearableNotifs() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ assertThat(button?.isVisible?.value).isTrue()
+ }
+
+ @Test
+ fun testClearAllButtonVisible_whenHasNoClearableNotifs() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = false,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = false,
+ )
+ runCurrent()
+
+ assertThat(button?.isVisible?.value).isFalse()
+ }
+
+ @Test
+ fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+ runCurrent()
+
+ // WHEN shade is expanded
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ shadeRepository.setLegacyShadeExpansion(1f)
+ // AND QS not expanded
+ shadeRepository.setQsExpansion(0f)
+ // AND device is awake
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ runCurrent()
+
+ // AND there are clearable notifications
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ // THEN button visibility should animate
+ assertThat(button?.isVisible?.isAnimating).isTrue()
+ }
+
+ @Test
+ fun testClearAllButtonAnimating_whenShadeNotExpanded() =
+ testComponent.runTest {
+ val button by collectLastValue(footerViewModel.clearAllButton)
+ runCurrent()
+
+ // WHEN shade is collapsed
+ keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+ shadeRepository.setLegacyShadeExpansion(0f)
+ // AND QS not expanded
+ shadeRepository.setQsExpansion(0f)
+ // AND device is awake
+ powerRepository.updateWakefulness(
+ rawState = WakefulnessState.AWAKE,
+ lastWakeReason = WakeSleepReason.POWER_BUTTON,
+ lastSleepReason = WakeSleepReason.OTHER,
+ )
+ runCurrent()
+
+ // AND there are clearable notifications
+ activeNotificationListRepository.notifStats.value =
+ NotifStats(
+ numActiveNotifs = 2,
+ hasNonClearableAlertingNotifs = false,
+ hasClearableAlertingNotifs = true,
+ hasNonClearableSilentNotifs = false,
+ hasClearableSilentNotifs = true,
+ )
+ runCurrent()
+
+ // THEN button visibility should not animate
+ assertThat(button?.isVisible?.isAnimating).isFalse()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index 0341035..360a373 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -26,10 +26,10 @@
import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
import com.android.systemui.runTest
import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
-import com.android.systemui.statusbar.notification.shared.activeNotificationModel
import com.android.systemui.statusbar.notification.shared.byIsAmbient
import com.android.systemui.statusbar.notification.shared.byIsLastMessageFromReply
import com.android.systemui.statusbar.notification.shared.byIsPulsing
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index c2a1519..e264fc0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -43,10 +43,10 @@
import com.android.systemui.runCurrent
import com.android.systemui.runTest
import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
import com.android.systemui.statusbar.notification.data.repository.HeadsUpNotificationIconViewStateRepository
-import com.android.systemui.statusbar.notification.shared.activeNotificationModel
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.SysuiDarkIconDispatcher
import com.android.systemui.statusbar.phone.data.repository.FakeDarkIconRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 3e331a6..0a9bac9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -999,11 +999,25 @@
assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createBubble())).isFalse();
}
+ @Test
+ public void shouldNotBubbleUp_suspended() {
+ assertThat(mNotifInterruptionStateProvider.shouldBubbleUp(createSuspendedBubble()))
+ .isFalse();
+ }
+
+ private NotificationEntry createSuspendedBubble() {
+ return createBubble(null, null, true);
+ }
+
private NotificationEntry createBubble() {
- return createBubble(null, null);
+ return createBubble(null, null, false);
}
private NotificationEntry createBubble(String groupKey, Integer groupAlert) {
+ return createBubble(groupKey, groupAlert, false);
+ }
+
+ private NotificationEntry createBubble(String groupKey, Integer groupAlert, Boolean suspended) {
Notification.BubbleMetadata data = new Notification.BubbleMetadata.Builder(
PendingIntent.getActivity(mContext, 0,
new Intent().setPackage(mContext.getPackageName()),
@@ -1031,6 +1045,7 @@
.setNotification(n)
.setImportance(IMPORTANCE_HIGH)
.setCanBubble(true)
+ .setSuspended(suspended)
.build();
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
index a3b7e8c..7babff5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionProviderTestBase.kt
@@ -601,6 +601,13 @@
}
@Test
+ fun testShouldNotBubble_bubbleAppSuspended() {
+ ensureBubbleState()
+ assertShouldNotBubble(buildBubbleEntry { packageSuspended = true })
+ assertNoEventsLogged()
+ }
+
+ @Test
fun testShouldNotFsi_noFullScreenIntent() {
forEachFsiState {
assertShouldNotFsi(buildFsiEntry { hasFsi = false })
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
index ca105f3..16c5c8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shared/TestActiveNotificationModel.kt
@@ -15,7 +15,6 @@
package com.android.systemui.statusbar.notification.shared
-import android.graphics.drawable.Icon
import com.google.common.truth.Correspondence
val byKey: Correspondence<ActiveNotificationModel, String> =
@@ -38,30 +37,3 @@
)
val byIsPulsing: Correspondence<ActiveNotificationModel, Boolean> =
Correspondence.transforming({ it?.isPulsing }, "has an isPulsing value of")
-
-fun activeNotificationModel(
- key: String,
- groupKey: String? = null,
- isAmbient: Boolean = false,
- isRowDismissed: Boolean = false,
- isSilent: Boolean = false,
- isLastMessageFromReply: Boolean = false,
- isSuppressedFromStatusBar: Boolean = false,
- isPulsing: Boolean = false,
- aodIcon: Icon? = null,
- shelfIcon: Icon? = null,
- statusBarIcon: Icon? = null,
-) =
- ActiveNotificationModel(
- key = key,
- groupKey = groupKey,
- isAmbient = isAmbient,
- isRowDismissed = isRowDismissed,
- isSilent = isSilent,
- isLastMessageFromReply = isLastMessageFromReply,
- isSuppressedFromStatusBar = isSuppressedFromStatusBar,
- isPulsing = isPulsing,
- aodIcon = aodIcon,
- shelfIcon = shelfIcon,
- statusBarIcon = statusBarIcon,
- )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 5903890..ff5c026 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -36,7 +36,6 @@
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-import android.content.res.Resources;
import android.metrics.LogMaker;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -53,7 +52,6 @@
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.common.ui.ConfigurationState;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
@@ -74,7 +72,6 @@
import com.android.systemui.statusbar.RemoteInputController;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.NotifCollection;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider;
@@ -84,17 +81,15 @@
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor;
import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor;
-import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore;
import com.android.systemui.statusbar.notification.init.NotificationsController;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent;
import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.ScrimController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -170,8 +165,14 @@
@Captor
private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
+ private final ActiveNotificationListRepository mActiveNotificationsRepository =
+ new ActiveNotificationListRepository();
+
+ private final ActiveNotificationsInteractor mActiveNotificationsInteractor =
+ new ActiveNotificationsInteractor(mActiveNotificationsRepository);
+
private final SeenNotificationsInteractor mSeenNotificationsInteractor =
- new SeenNotificationsInteractor(new ActiveNotificationListRepository());
+ new SeenNotificationsInteractor(mActiveNotificationsRepository);
private NotificationStackScrollLayoutController mController;
@@ -701,6 +702,7 @@
mUiEventLogger,
mRemoteInputManager,
mVisibilityLocationProviderDelegator,
+ mActiveNotificationsInteractor,
mSeenNotificationsInteractor,
mViewBinder,
mShadeController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
index 46e8453..ac20683 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/HideNotificationsInteractorTest.kt
@@ -34,9 +34,11 @@
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractorImpl
-import com.android.systemui.util.animation.FakeAnimationStatusRepository
+import com.android.systemui.util.animation.data.repository.FakeAnimationStatusRepository
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.test.TestScope
@@ -47,8 +49,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
-import java.time.Duration
-import java.util.Optional
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
new file mode 100644
index 0000000..f00abc9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -0,0 +1,313 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+
+import android.app.NotificationManager.Policy
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.common.domain.CommonDomainLayerModule
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FakeFeatureFlagsClassicModule
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
+import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
+import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModelModule
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
+import com.android.systemui.statusbar.policy.FakeConfigurationController
+import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepository
+import com.android.systemui.unfold.UnfoldTransitionModule
+import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotificationListViewModelTest : SysuiTestCase() {
+
+ @SysUISingleton
+ @Component(
+ modules =
+ [
+ SysUITestModule::class,
+ ActivatableNotificationViewModelModule::class,
+ CommonDomainLayerModule::class,
+ FooterViewModelModule::class,
+ HeadlessSystemUserModeModule::class,
+ UnfoldTransitionModule.Bindings::class,
+ ]
+ )
+ interface TestComponent : SysUITestComponent<NotificationListViewModel> {
+ val activeNotificationListRepository: ActiveNotificationListRepository
+ val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ val shadeRepository: FakeShadeRepository
+ val zenModeRepository: FakeZenModeRepository
+ val configurationController: FakeConfigurationController
+
+ @Component.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance test: SysuiTestCase,
+ featureFlags: FakeFeatureFlagsClassicModule,
+ mocks: TestMocksModule,
+ ): TestComponent
+ }
+ }
+
+ private val testComponent: TestComponent =
+ DaggerNotificationListViewModelTest_TestComponent.factory()
+ .create(
+ test = this,
+ featureFlags =
+ FakeFeatureFlagsClassicModule {
+ set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true)
+ },
+ mocks = TestMocksModule()
+ )
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
+ mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR)
+ }
+
+ @Test
+ fun testIsImportantForAccessibility_falseWhenNoNotifs() =
+ testComponent.runTest {
+ val important by collectLastValue(underTest.isImportantForAccessibility)
+
+ // WHEN on lockscreen
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope,
+ )
+ // AND has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ testScope.runCurrent()
+
+ // THEN not important
+ assertThat(important).isFalse()
+ }
+
+ @Test
+ fun testIsImportantForAccessibility_trueWhenNotifs() =
+ testComponent.runTest {
+ val important by collectLastValue(underTest.isImportantForAccessibility)
+
+ // WHEN on lockscreen
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.GONE,
+ to = KeyguardState.LOCKSCREEN,
+ testScope,
+ )
+ // AND has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ runCurrent()
+
+ // THEN is important
+ assertThat(important).isTrue()
+ }
+
+ @Test
+ fun testIsImportantForAccessibility_trueWhenNotKeyguard() =
+ testComponent.runTest {
+ val important by collectLastValue(underTest.isImportantForAccessibility)
+
+ // WHEN not on lockscreen
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope,
+ )
+ // AND has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ runCurrent()
+
+ // THEN is still important
+ assertThat(important).isTrue()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_trueWhenNoNotifs() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ runCurrent()
+
+ // THEN should show
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenNotifs() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has notifs
+ activeNotificationListRepository.setActiveNotifs(count = 2)
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenQsExpandedDefault() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND quick settings are expanded
+ shadeRepository.legacyQsFullscreen.value = true
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_trueWhenQsExpandedInSplitShade() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND quick settings are expanded
+ shadeRepository.setQsExpansion(1f)
+ // AND split shade is enabled
+ overrideResource(R.bool.config_use_split_notification_shade, true)
+ configurationController.notifyConfigurationChanged()
+ runCurrent()
+
+ // THEN should show
+ assertThat(shouldShow).isTrue()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenTransitioningToAOD() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND transitioning to AOD
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 0f,
+ )
+ )
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testShouldShowEmptyShadeView_falseWhenBouncerShowing() =
+ testComponent.runTest {
+ val shouldShow by collectLastValue(underTest.shouldShowEmptyShadeView)
+
+ // WHEN has no notifs
+ activeNotificationListRepository.setActiveNotifs(count = 0)
+ // AND is on bouncer
+ keyguardTransitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.PRIMARY_BOUNCER,
+ testScope,
+ )
+ runCurrent()
+
+ // THEN should not show
+ assertThat(shouldShow).isFalse()
+ }
+
+ @Test
+ fun testAreNotificationsHiddenInShade_true() =
+ testComponent.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ zenModeRepository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+ runCurrent()
+
+ assertThat(hidden).isTrue()
+ }
+
+ @Test
+ fun testAreNotificationsHiddenInShade_false() =
+ testComponent.runTest {
+ val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+ zenModeRepository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ zenModeRepository.zenMode.value = Settings.Global.ZEN_MODE_OFF
+ runCurrent()
+
+ assertThat(hidden).isFalse()
+ }
+
+ @Test
+ fun testHasFilteredOutSeenNotifications_true() =
+ testComponent.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isTrue()
+ }
+
+ @Test
+ fun testHasFilteredOutSeenNotifications_false() =
+ testComponent.runTest {
+ val hasFilteredNotifs by collectLastValue(underTest.hasFilteredOutSeenNotifications)
+
+ activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
+ runCurrent()
+
+ assertThat(hasFilteredNotifs).isFalse()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 99e62ee..dbb1062 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -128,8 +128,7 @@
testComponent.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
- repository.consolidatedNotificationPolicy.value =
- policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
repository.zenMode.value = Settings.Global.ZEN_MODE_OFF
runCurrent()
@@ -141,8 +140,7 @@
testComponent.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
- repository.consolidatedNotificationPolicy.value =
- policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_STATUS_BAR)
+ repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_STATUS_BAR)
repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
runCurrent()
@@ -154,19 +152,10 @@
testComponent.runTest {
val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
- repository.consolidatedNotificationPolicy.value =
- policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+ repository.setSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
runCurrent()
assertThat(hidden).isTrue()
}
}
-
-fun policyWithSuppressedVisualEffects(suppressedVisualEffects: Int) =
- Policy(
- /* priorityCategories = */ 0,
- /* priorityCallSenders = */ 0,
- /* priorityMessageSenders = */ 0,
- /* suppressedVisualEffects = */ suppressedVisualEffects
- )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/InstanceIdSequenceFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/InstanceIdSequenceFake.kt
similarity index 88%
rename from packages/SystemUI/tests/src/com/android/systemui/InstanceIdSequenceFake.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/InstanceIdSequenceFake.kt
index 6fbe3ad..aae270d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/InstanceIdSequenceFake.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/InstanceIdSequenceFake.kt
@@ -19,14 +19,10 @@
import com.android.internal.logging.InstanceId
import com.android.internal.logging.InstanceIdSequence
-/**
- * Fake [InstanceId] generator.
- */
+/** Fake [InstanceId] generator. */
class InstanceIdSequenceFake(instanceIdMax: Int) : InstanceIdSequence(instanceIdMax) {
- /**
- * Last id used to generate a [InstanceId]. `-1` if no [InstanceId] has been generated.
- */
+ /** Last id used to generate a [InstanceId]. `-1` if no [InstanceId] has been generated. */
var lastInstanceId = -1
private set
@@ -38,4 +34,4 @@
}
return newInstanceIdInternal(lastInstanceId)
}
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
index d0c1267..3724291 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
@@ -15,6 +15,7 @@
*/
package com.android.systemui
+import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.testing.TestableContext
@@ -80,6 +81,9 @@
test.fakeBroadcastDispatcher
@Provides
+ fun provideContentResolver(context: Context): ContentResolver = context.contentResolver
+
+ @Provides
fun provideBaseShadeInteractor(
sceneContainerFlags: SceneContainerFlags,
sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 37a4f61..f57ace9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -55,7 +55,9 @@
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.ScrimController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.ZenModeController
import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.util.mockito.mock
import com.android.wm.shell.bubbles.Bubbles
import dagger.Binds
@@ -100,6 +102,10 @@
@get:Provides val keyguardViewController: KeyguardViewController = mock(),
@get:Provides val dialogLaunchAnimator: DialogLaunchAnimator = mock(),
@get:Provides val sysuiState: SysUiState = mock(),
+ @get:Provides
+ val unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider> =
+ Optional.empty(),
+ @get:Provides val zenModeController: ZenModeController = mock(),
// log buffers
@get:[Provides BroadcastDispatcherLog]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
index 3aee889..b27926c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt
@@ -65,7 +65,6 @@
widgetRepository,
mediaRepository,
smartspaceRepository,
- withDeps.communalTutorialInteractor,
appWidgetHost,
editWidgetsActivityStarter,
),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
index 36f0882..8c653a5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
@@ -26,12 +26,14 @@
import com.android.systemui.statusbar.data.FakeStatusBarDataLayerModule
import com.android.systemui.telephony.data.FakeTelephonyDataLayerModule
import com.android.systemui.user.data.FakeUserDataLayerModule
+import com.android.systemui.util.animation.data.FakeAnimationUtilDataLayerModule
import dagger.Module
@Module(
includes =
[
FakeAccessibilityDataLayerModule::class,
+ FakeAnimationUtilDataLayerModule::class,
FakeAuthenticationDataLayerModule::class,
FakeBouncerDataLayerModule::class,
FakeCommonDataLayerModule::class,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index d3744d55..4068e40 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -167,6 +167,10 @@
_isKeyguardOccluded.value = isOccluded
}
+ fun setKeyguardUnlocked(isUnlocked: Boolean) {
+ _isKeyguardUnlocked.value = isUnlocked
+ }
+
override fun setIsDozing(isDozing: Boolean) {
_isDozing.value = isDozing
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QsEventLoggerFake.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QsEventLoggerFake.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/QsEventLoggerFake.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/QsEventLoggerFake.kt
index 40aa260..baccc6f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QsEventLoggerFake.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QsEventLoggerFake.kt
@@ -5,7 +5,7 @@
* 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
+ * 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,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
new file mode 100644
index 0000000..1cb2587
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QuickSettingsKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * 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
+
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.InstanceIdSequenceFake
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.instanceIdSequenceFake: InstanceIdSequenceFake by
+ Kosmos.Fixture { InstanceIdSequenceFake(0) }
+val Kosmos.uiEventLogger: UiEventLoggerFake by Kosmos.Fixture { UiEventLoggerFake() }
+val Kosmos.qsEventLogger: QsEventLoggerFake by
+ Kosmos.Fixture { QsEventLoggerFake(uiEventLogger, instanceIdSequenceFake) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/flashlight/FlashlightTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/flashlight/FlashlightTileKosmos.kt
new file mode 100644
index 0000000..97e9b51
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/flashlight/FlashlightTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.impl.flashlight
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.policy.PolicyModule
+
+val Kosmos.qsFlashlightTileConfig by
+ Kosmos.Fixture { PolicyModule.provideFlashlightTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
new file mode 100644
index 0000000..9851b0e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.statusbar.notification.data.model
+
+import android.graphics.drawable.Icon
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
+
+/** Simple ActiveNotificationModel builder for use in tests. */
+fun activeNotificationModel(
+ key: String,
+ groupKey: String? = null,
+ isAmbient: Boolean = false,
+ isRowDismissed: Boolean = false,
+ isSilent: Boolean = false,
+ isLastMessageFromReply: Boolean = false,
+ isSuppressedFromStatusBar: Boolean = false,
+ isPulsing: Boolean = false,
+ aodIcon: Icon? = null,
+ shelfIcon: Icon? = null,
+ statusBarIcon: Icon? = null,
+) =
+ ActiveNotificationModel(
+ key = key,
+ groupKey = groupKey,
+ isAmbient = isAmbient,
+ isRowDismissed = isRowDismissed,
+ isSilent = isSilent,
+ isLastMessageFromReply = isLastMessageFromReply,
+ isSuppressedFromStatusBar = isSuppressedFromStatusBar,
+ isPulsing = isPulsing,
+ aodIcon = aodIcon,
+ shelfIcon = shelfIcon,
+ statusBarIcon = statusBarIcon,
+ )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
new file mode 100644
index 0000000..cb1ba20
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.statusbar.notification.data.repository
+
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+
+/**
+ * Make the repository hold [count] active notifications for testing. The keys of the notifications
+ * are "0", "1", ..., (count - 1).toString().
+ */
+fun ActiveNotificationListRepository.setActiveNotifs(count: Int) {
+ this.activeNotifications.value =
+ ActiveNotificationsStore.Builder()
+ .apply { repeat(count) { i -> addEntry(activeNotificationModel(key = i.toString())) } }
+ .build()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
index 4059930..c4d7867 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.policy.data.repository
-import android.app.NotificationManager
+import android.app.NotificationManager.Policy
import android.provider.Settings
import com.android.systemui.dagger.SysUISingleton
import dagger.Binds
@@ -27,14 +27,24 @@
@SysUISingleton
class FakeZenModeRepository @Inject constructor() : ZenModeRepository {
override val zenMode: MutableStateFlow<Int> = MutableStateFlow(Settings.Global.ZEN_MODE_OFF)
- override val consolidatedNotificationPolicy: MutableStateFlow<NotificationManager.Policy?> =
+ override val consolidatedNotificationPolicy: MutableStateFlow<Policy?> =
MutableStateFlow(
- NotificationManager.Policy(
+ Policy(
/* priorityCategories = */ 0,
/* priorityCallSenders = */ 0,
/* priorityMessageSenders = */ 0,
)
)
+
+ fun setSuppressedVisualEffects(suppressedVisualEffects: Int) {
+ consolidatedNotificationPolicy.value =
+ Policy(
+ /* priorityCategories = */ 0,
+ /* priorityCallSenders = */ 0,
+ /* priorityMessageSenders = */ 0,
+ /* suppressedVisualEffects = */ suppressedVisualEffects,
+ )
+ }
}
@Module
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/FakeAnimationUtilDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/FakeAnimationUtilDataLayerModule.kt
new file mode 100644
index 0000000..f7830d9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/FakeAnimationUtilDataLayerModule.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.util.animation.data
+
+import com.android.systemui.util.animation.data.repository.FakeAnimationStatusRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeAnimationStatusRepositoryModule::class])
+object FakeAnimationUtilDataLayerModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/repository/FakeAnimationStatusRepository.kt
similarity index 72%
rename from packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/repository/FakeAnimationStatusRepository.kt
index e72235c..ca6628b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/animation/FakeAnimationStatusRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/animation/data/repository/FakeAnimationStatusRepository.kt
@@ -11,15 +11,17 @@
* 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
+ * limitations under the License.
*/
-package com.android.systemui.util.animation
+package com.android.systemui.util.animation.data.repository
-import com.android.systemui.util.animation.data.repository.AnimationStatusRepository
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
-class FakeAnimationStatusRepository : AnimationStatusRepository {
+class FakeAnimationStatusRepository @Inject constructor() : AnimationStatusRepository {
// Replay 1 element as real repository always emits current status as a first element
private val animationsEnabled: MutableSharedFlow<Boolean> = MutableSharedFlow(replay = 1)
@@ -30,3 +32,8 @@
animationsEnabled.tryEmit(enabled)
}
}
+
+@Module
+interface FakeAnimationStatusRepositoryModule {
+ @Binds fun bindFake(fake: FakeAnimationStatusRepository): AnimationStatusRepository
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index eeaa423..c111ec3 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -49,7 +49,6 @@
import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
@@ -83,14 +82,6 @@
@interface PhysType {
}
- /**
- * The maximum length of a device name (in bytes in UTF-8 encoding).
- *
- * This limitation comes directly from uinput.
- * See also UINPUT_MAX_NAME_SIZE in linux/uinput.h
- */
- private static final int DEVICE_NAME_MAX_LENGTH = 80;
-
final Object mLock = new Object();
/* Token -> file descriptor associations. */
@@ -138,25 +129,17 @@
}
}
- void createDpad(@NonNull String deviceName,
- int vendorId,
- int productId,
- @NonNull IBinder deviceToken,
- int displayId) {
+ void createDpad(@NonNull String deviceName, int vendorId, int productId,
+ @NonNull IBinder deviceToken, int displayId) throws DeviceCreationException {
final String phys = createPhys(PHYS_TYPE_DPAD);
- try {
- createDeviceInternal(InputDeviceDescriptor.TYPE_DPAD, deviceName, vendorId,
+ createDeviceInternal(InputDeviceDescriptor.TYPE_DPAD, deviceName, vendorId,
productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputDpad(deviceName, vendorId, productId, phys));
- } catch (DeviceCreationException e) {
- throw new RuntimeException(
- "Failed to create virtual dpad device '" + deviceName + "'.", e);
- }
}
void createKeyboard(@NonNull String deviceName, int vendorId, int productId,
@NonNull IBinder deviceToken, int displayId, @NonNull String languageTag,
- @NonNull String layoutType) {
+ @NonNull String layoutType) throws DeviceCreationException {
final String phys = createPhys(PHYS_TYPE_KEYBOARD);
mInputManagerInternal.addKeyboardLayoutAssociation(phys, languageTag,
layoutType);
@@ -166,66 +149,42 @@
() -> mNativeWrapper.openUinputKeyboard(deviceName, vendorId, productId, phys));
} catch (DeviceCreationException e) {
mInputManagerInternal.removeKeyboardLayoutAssociation(phys);
- throw new RuntimeException(
- "Failed to create virtual keyboard device '" + deviceName + "'.", e);
+ throw e;
}
}
- void createMouse(@NonNull String deviceName,
- int vendorId,
- int productId,
- @NonNull IBinder deviceToken,
- int displayId) {
+ void createMouse(@NonNull String deviceName, int vendorId, int productId,
+ @NonNull IBinder deviceToken, int displayId) throws DeviceCreationException {
final String phys = createPhys(PHYS_TYPE_MOUSE);
- try {
- createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
- deviceToken, displayId, phys,
- () -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
- } catch (DeviceCreationException e) {
- throw new RuntimeException(
- "Failed to create virtual mouse device: '" + deviceName + "'.", e);
- }
+ createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId,
+ deviceToken, displayId, phys,
+ () -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys));
mInputManagerInternal.setVirtualMousePointerDisplayId(displayId);
}
- void createTouchscreen(@NonNull String deviceName,
- int vendorId,
- int productId,
- @NonNull IBinder deviceToken,
- int displayId,
- int height,
- int width) {
+ void createTouchscreen(@NonNull String deviceName, int vendorId, int productId,
+ @NonNull IBinder deviceToken, int displayId, int height, int width)
+ throws DeviceCreationException {
final String phys = createPhys(PHYS_TYPE_TOUCHSCREEN);
- try {
- createDeviceInternal(InputDeviceDescriptor.TYPE_TOUCHSCREEN, deviceName, vendorId,
- productId, deviceToken, displayId, phys,
- () -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
- phys, height, width));
- } catch (DeviceCreationException e) {
- throw new RuntimeException(
- "Failed to create virtual touchscreen device '" + deviceName + "'.", e);
- }
+ createDeviceInternal(InputDeviceDescriptor.TYPE_TOUCHSCREEN, deviceName, vendorId,
+ productId, deviceToken, displayId, phys,
+ () -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId, phys,
+ height, width));
}
- void createNavigationTouchpad(
- @NonNull String deviceName,
- int vendorId,
- int productId,
- @NonNull IBinder deviceToken,
- int displayId,
- int touchpadHeight,
- int touchpadWidth) {
+ void createNavigationTouchpad(@NonNull String deviceName, int vendorId, int productId,
+ @NonNull IBinder deviceToken, int displayId, int height, int width)
+ throws DeviceCreationException {
final String phys = createPhys(PHYS_TYPE_NAVIGATION_TOUCHPAD);
mInputManagerInternal.setTypeAssociation(phys, NAVIGATION_TOUCHPAD_DEVICE_TYPE);
try {
createDeviceInternal(InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD, deviceName,
vendorId, productId, deviceToken, displayId, phys,
() -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId,
- phys, touchpadHeight, touchpadWidth));
+ phys, height, width));
} catch (DeviceCreationException e) {
mInputManagerInternal.unsetTypeAssociation(phys);
- throw new RuntimeException(
- "Failed to create virtual navigation touchpad device '" + deviceName + "'.", e);
+ throw e;
}
}
@@ -234,10 +193,10 @@
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove(
token);
if (inputDeviceDescriptor == null) {
- throw new IllegalArgumentException(
- "Could not unregister input device for given token");
+ Slog.w(TAG, "Could not unregister input device for given token.");
+ } else {
+ closeInputDeviceDescriptorLocked(token, inputDeviceDescriptor);
}
- closeInputDeviceDescriptorLocked(token, inputDeviceDescriptor);
}
}
@@ -326,21 +285,11 @@
}
/**
- * Validates a device name by checking length and whether a device with the same name
- * already exists. Throws exceptions if the validation fails.
+ * Validates a device name by checking whether a device with the same name already exists.
* @param deviceName The name of the device to be validated
* @throws DeviceCreationException if {@code deviceName} is not valid.
*/
private void validateDeviceName(String deviceName) throws DeviceCreationException {
- // Comparison is greater or equal because the device name must fit into a const char*
- // including the \0-terminator. Therefore the actual number of bytes that can be used
- // for device name is DEVICE_NAME_MAX_LENGTH - 1
- if (deviceName.getBytes(StandardCharsets.UTF_8).length >= DEVICE_NAME_MAX_LENGTH) {
- throw new DeviceCreationException(
- "Input device name exceeds maximum length of " + DEVICE_NAME_MAX_LENGTH
- + "bytes: " + deviceName);
- }
-
synchronized (mLock) {
for (int i = 0; i < mInputDeviceDescriptors.size(); ++i) {
if (mInputDeviceDescriptors.valueAt(i).mName.equals(deviceName)) {
@@ -365,8 +314,7 @@
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
- throw new IllegalArgumentException(
- "Could not send key event to input device for given token");
+ return false;
}
return mNativeWrapper.writeDpadKeyEvent(inputDeviceDescriptor.getNativePointer(),
event.getKeyCode(), event.getAction(), event.getEventTimeNanos());
@@ -378,8 +326,7 @@
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
- throw new IllegalArgumentException(
- "Could not send key event to input device for given token");
+ return false;
}
return mNativeWrapper.writeKeyEvent(inputDeviceDescriptor.getNativePointer(),
event.getKeyCode(), event.getAction(), event.getEventTimeNanos());
@@ -391,13 +338,12 @@
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
- throw new IllegalArgumentException(
- "Could not send button event to input device for given token");
+ return false;
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
- throw new IllegalStateException(
- "Display id associated with this mouse is not currently targetable");
+ mInputManagerInternal.setVirtualMousePointerDisplayId(
+ inputDeviceDescriptor.getDisplayId());
}
return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(),
event.getButtonCode(), event.getAction(), event.getEventTimeNanos());
@@ -409,8 +355,7 @@
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
- throw new IllegalArgumentException(
- "Could not send touch event to input device for given token");
+ return false;
}
return mNativeWrapper.writeTouchEvent(inputDeviceDescriptor.getNativePointer(),
event.getPointerId(), event.getToolType(), event.getAction(), event.getX(),
@@ -424,13 +369,12 @@
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
- throw new IllegalArgumentException(
- "Could not send relative event to input device for given token");
+ return false;
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
- throw new IllegalStateException(
- "Display id associated with this mouse is not currently targetable");
+ mInputManagerInternal.setVirtualMousePointerDisplayId(
+ inputDeviceDescriptor.getDisplayId());
}
return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(),
event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos());
@@ -442,13 +386,12 @@
final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get(
token);
if (inputDeviceDescriptor == null) {
- throw new IllegalArgumentException(
- "Could not send scroll event to input device for given token");
+ return false;
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
- throw new IllegalStateException(
- "Display id associated with this mouse is not currently targetable");
+ mInputManagerInternal.setVirtualMousePointerDisplayId(
+ inputDeviceDescriptor.getDisplayId());
}
return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(),
event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos());
@@ -465,8 +408,8 @@
}
if (inputDeviceDescriptor.getDisplayId()
!= mInputManagerInternal.getVirtualMousePointerDisplayId()) {
- throw new IllegalStateException(
- "Display id associated with this mouse is not currently targetable");
+ mInputManagerInternal.setVirtualMousePointerDisplayId(
+ inputDeviceDescriptor.getDisplayId());
}
return LocalServices.getService(InputManagerInternal.class).getCursorPosition();
}
@@ -758,13 +701,19 @@
}
/** An internal exception that is thrown to indicate an error when opening a virtual device. */
- private static class DeviceCreationException extends Exception {
+ static class DeviceCreationException extends Exception {
+ DeviceCreationException() {
+ super();
+ }
DeviceCreationException(String message) {
super(message);
}
- DeviceCreationException(String message, Exception cause) {
+ DeviceCreationException(String message, Throwable cause) {
super(message, cause);
}
+ DeviceCreationException(Throwable cause) {
+ super(cause);
+ }
}
/**
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 118943d..e6bfeb7 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -694,6 +694,8 @@
mInputController.createDpad(config.getInputDeviceName(), config.getVendorId(),
config.getProductId(), deviceToken,
getTargetDisplayIdForInput(config.getAssociatedDisplayId()));
+ } catch (InputController.DeviceCreationException e) {
+ throw new IllegalArgumentException(e);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -705,15 +707,17 @@
super.createVirtualKeyboard_enforcePermission();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
- synchronized (mVirtualDeviceLock) {
- mLocaleList = LocaleList.forLanguageTags(config.getLanguageTag());
- }
final long ident = Binder.clearCallingIdentity();
try {
mInputController.createKeyboard(config.getInputDeviceName(), config.getVendorId(),
config.getProductId(), deviceToken,
getTargetDisplayIdForInput(config.getAssociatedDisplayId()),
config.getLanguageTag(), config.getLayoutType());
+ synchronized (mVirtualDeviceLock) {
+ mLocaleList = LocaleList.forLanguageTags(config.getLanguageTag());
+ }
+ } catch (InputController.DeviceCreationException e) {
+ throw new IllegalArgumentException(e);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -729,6 +733,8 @@
try {
mInputController.createMouse(config.getInputDeviceName(), config.getVendorId(),
config.getProductId(), deviceToken, config.getAssociatedDisplayId());
+ } catch (InputController.DeviceCreationException e) {
+ throw new IllegalArgumentException(e);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -741,19 +747,13 @@
super.createVirtualTouchscreen_enforcePermission();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
- int screenHeight = config.getHeight();
- int screenWidth = config.getWidth();
- if (screenHeight <= 0 || screenWidth <= 0) {
- throw new IllegalArgumentException(
- "Cannot create a virtual touchscreen, screen dimensions must be positive. Got: "
- + "(" + screenWidth + ", " + screenHeight + ")");
- }
-
final long ident = Binder.clearCallingIdentity();
try {
mInputController.createTouchscreen(config.getInputDeviceName(), config.getVendorId(),
config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
- screenHeight, screenWidth);
+ config.getHeight(), config.getWidth());
+ } catch (InputController.DeviceCreationException e) {
+ throw new IllegalArgumentException(e);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -766,21 +766,15 @@
super.createVirtualNavigationTouchpad_enforcePermission();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
- int touchpadHeight = config.getHeight();
- int touchpadWidth = config.getWidth();
- if (touchpadHeight <= 0 || touchpadWidth <= 0) {
- throw new IllegalArgumentException(
- "Cannot create a virtual navigation touchpad, touchpad dimensions must be positive."
- + " Got: (" + touchpadHeight + ", " + touchpadWidth + ")");
- }
-
final long ident = Binder.clearCallingIdentity();
try {
mInputController.createNavigationTouchpad(
config.getInputDeviceName(), config.getVendorId(),
config.getProductId(), deviceToken,
getTargetDisplayIdForInput(config.getAssociatedDisplayId()),
- touchpadHeight, touchpadWidth);
+ config.getHeight(), config.getWidth());
+ } catch (InputController.DeviceCreationException e) {
+ throw new IllegalArgumentException(e);
} finally {
Binder.restoreCallingIdentity(ident);
}
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index 202f68b..a570d09 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -20,9 +20,11 @@
import android.companion.virtual.camera.IVirtualCameraCallback;
import android.companion.virtual.camera.VirtualCameraConfig;
import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.companion.virtualcamera.Format;
import android.companion.virtualcamera.IVirtualCameraService;
import android.companion.virtualcamera.SupportedStreamConfiguration;
import android.companion.virtualcamera.VirtualCameraConfiguration;
+import android.graphics.ImageFormat;
import android.os.RemoteException;
import android.view.Surface;
@@ -85,10 +87,14 @@
SupportedStreamConfiguration supportedConfig = new SupportedStreamConfiguration();
supportedConfig.height = stream.getHeight();
supportedConfig.width = stream.getWidth();
- supportedConfig.pixelFormat = stream.getFormat();
+ supportedConfig.pixelFormat = convertFormat(stream.getFormat());
return supportedConfig;
}
+ private static int convertFormat(int format) {
+ return format == ImageFormat.YUV_420_888 ? Format.YUV_420_888 : Format.UNKNOWN;
+ }
+
private VirtualCameraConversionUtil() {
}
}
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index 9716cf6..3ce91c8 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -1076,6 +1076,16 @@
public boolean APP_PROFILER_PSS_PROFILING_DISABLED;
+ /**
+ * The modifier used to adjust PSS thresholds in OomAdjuster when RSS is collected instead.
+ */
+ static final String KEY_PSS_TO_RSS_THRESHOLD_MODIFIER =
+ "pss_to_rss_threshold_modifier";
+
+ private final float mDefaultPssToRssThresholdModifier;
+
+ public float PSS_TO_RSS_THRESHOLD_MODIFIER;
+
private final OnPropertiesChangedListener mOnDeviceConfigChangedListener =
new OnPropertiesChangedListener() {
@Override
@@ -1254,6 +1264,9 @@
case KEY_DISABLE_APP_PROFILER_PSS_PROFILING:
updateDisableAppProfilerPssProfiling();
break;
+ case KEY_PSS_TO_RSS_THRESHOLD_MODIFIER:
+ updatePssToRssThresholdModifier();
+ break;
default:
updateFGSPermissionEnforcementFlagsIfNecessary(name);
break;
@@ -1339,6 +1352,10 @@
mDefaultDisableAppProfilerPssProfiling = context.getResources().getBoolean(
R.bool.config_am_disablePssProfiling);
APP_PROFILER_PSS_PROFILING_DISABLED = mDefaultDisableAppProfilerPssProfiling;
+
+ mDefaultPssToRssThresholdModifier = context.getResources().getFloat(
+ com.android.internal.R.dimen.config_am_pssToRssThresholdModifier);
+ PSS_TO_RSS_THRESHOLD_MODIFIER = mDefaultPssToRssThresholdModifier;
}
public void start(ContentResolver resolver) {
@@ -2051,6 +2068,12 @@
mDefaultDisableAppProfilerPssProfiling);
}
+ private void updatePssToRssThresholdModifier() {
+ PSS_TO_RSS_THRESHOLD_MODIFIER = DeviceConfig.getFloat(
+ DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, KEY_PSS_TO_RSS_THRESHOLD_MODIFIER,
+ mDefaultPssToRssThresholdModifier);
+ }
+
@NeverCompile // Avoid size overhead of debugging code.
void dump(PrintWriter pw) {
pw.println("ACTIVITY MANAGER SETTINGS (dumpsys activity settings) "
@@ -2242,6 +2265,9 @@
pw.print(" "); pw.print(KEY_DISABLE_APP_PROFILER_PSS_PROFILING);
pw.print("="); pw.println(APP_PROFILER_PSS_PROFILING_DISABLED);
+ pw.print(" "); pw.print(KEY_PSS_TO_RSS_THRESHOLD_MODIFIER);
+ pw.print("="); pw.println(PSS_TO_RSS_THRESHOLD_MODIFIER);
+
pw.println();
if (mOverrideMaxCachedProcesses >= 0) {
pw.print(" mOverrideMaxCachedProcesses="); pw.println(mOverrideMaxCachedProcesses);
diff --git a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
index 5dd0a3f..55b161a 100644
--- a/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
+++ b/services/core/java/com/android/server/am/ActivityManagerDebugConfig.java
@@ -71,6 +71,7 @@
static final boolean DEBUG_PROCESSES = DEBUG_ALL || false;
static final boolean DEBUG_PROVIDER = DEBUG_ALL || false;
static final boolean DEBUG_PSS = DEBUG_ALL || false;
+ static final boolean DEBUG_RSS = DEBUG_ALL || false;
static final boolean DEBUG_SERVICE = DEBUG_ALL || false;
static final boolean DEBUG_FOREGROUND_SERVICE = DEBUG_ALL || false;
static final boolean DEBUG_SERVICE_EXECUTING = DEBUG_ALL || false;
@@ -91,6 +92,7 @@
? "_ProcessObservers" : "";
static final String POSTFIX_PROCESSES = (APPEND_CATEGORY_NAME) ? "_Processes" : "";
static final String POSTFIX_PSS = (APPEND_CATEGORY_NAME) ? "_Pss" : "";
+ static final String POSTFIX_RSS = (APPEND_CATEGORY_NAME) ? "_Rss" : "";
static final String POSTFIX_SERVICE = (APPEND_CATEGORY_NAME) ? "_Service" : "";
static final String POSTFIX_SERVICE_EXECUTING =
(APPEND_CATEGORY_NAME) ? "_ServiceExecuting" : "";
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b19b644..c64fb23 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -327,6 +327,7 @@
import android.os.DropBoxManager;
import android.os.FactoryTest;
import android.os.FileUtils;
+import android.os.Flags;
import android.os.Handler;
import android.os.IBinder;
import android.os.IDeviceIdentifiersPolicyService;
@@ -8563,6 +8564,12 @@
// If the processes' memory has increased by more than 1% of the total memory,
// or 10 MB, whichever is greater, then the processes' are eligible to be killed.
final long totalMemoryInKb = getTotalMemory() / 1000;
+
+ // This threshold should be applicable to both PSS and RSS because the value is absolute
+ // and represents an increase in process memory relative to its own previous state.
+ //
+ // TODO(b/296454553): Tune this value during the flag rollout process if more processes
+ // seem to be getting killed than before.
final long memoryGrowthThreshold =
Math.max(totalMemoryInKb / 100, MINIMUM_MEMORY_GROWTH_THRESHOLD);
mProcessList.forEachLruProcessesLOSP(false, proc -> {
@@ -8575,24 +8582,34 @@
if (state.isNotCachedSinceIdle()) {
if (setProcState >= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
&& setProcState <= ActivityManager.PROCESS_STATE_SERVICE) {
- final long initialIdlePss, lastPss, lastSwapPss;
+ final long initialIdlePssOrRss, lastPssOrRss, lastSwapPss;
synchronized (mAppProfiler.mProfilerLock) {
- initialIdlePss = pr.getInitialIdlePss();
- lastPss = pr.getLastPss();
+ initialIdlePssOrRss = pr.getInitialIdlePssOrRss();
+ lastPssOrRss = !Flags.removeAppProfilerPssCollection()
+ ? pr.getLastPss() : pr.getLastRss();
lastSwapPss = pr.getLastSwapPss();
}
- if (doKilling && initialIdlePss != 0
- && lastPss > (initialIdlePss * 3 / 2)
- && lastPss > (initialIdlePss + memoryGrowthThreshold)) {
+ if (doKilling && initialIdlePssOrRss != 0
+ && lastPssOrRss > (initialIdlePssOrRss * 3 / 2)
+ && lastPssOrRss > (initialIdlePssOrRss + memoryGrowthThreshold)) {
final StringBuilder sb2 = new StringBuilder(128);
sb2.append("Kill");
sb2.append(proc.processName);
- sb2.append(" in idle maint: pss=");
- sb2.append(lastPss);
- sb2.append(", swapPss=");
- sb2.append(lastSwapPss);
- sb2.append(", initialPss=");
- sb2.append(initialIdlePss);
+ if (!Flags.removeAppProfilerPssCollection()) {
+ sb2.append(" in idle maint: pss=");
+ } else {
+ sb2.append(" in idle maint: rss=");
+ }
+ sb2.append(lastPssOrRss);
+
+ if (!Flags.removeAppProfilerPssCollection()) {
+ sb2.append(", swapPss=");
+ sb2.append(lastSwapPss);
+ sb2.append(", initialPss=");
+ } else {
+ sb2.append(", initialRss=");
+ }
+ sb2.append(initialIdlePssOrRss);
sb2.append(", period=");
TimeUtils.formatDuration(timeSinceLastIdle, sb2);
sb2.append(", lowRamPeriod=");
@@ -8600,8 +8617,9 @@
Slog.wtfQuiet(TAG, sb2.toString());
mHandler.post(() -> {
synchronized (ActivityManagerService.this) {
- proc.killLocked("idle maint (pss " + lastPss
- + " from " + initialIdlePss + ")",
+ proc.killLocked(!Flags.removeAppProfilerPssCollection()
+ ? "idle maint (pss " : "idle maint (rss " + lastPssOrRss
+ + " from " + initialIdlePssOrRss + ")",
ApplicationExitInfo.REASON_OTHER,
ApplicationExitInfo.SUBREASON_MEMORY_PRESSURE,
true);
@@ -8613,7 +8631,7 @@
&& setProcState >= ActivityManager.PROCESS_STATE_PERSISTENT) {
state.setNotCachedSinceIdle(true);
synchronized (mAppProfiler.mProfilerLock) {
- pr.setInitialIdlePss(0);
+ pr.setInitialIdlePssOrRss(0);
mAppProfiler.updateNextPssTimeLPf(
state.getSetProcState(), proc.mProfile, now, true);
}
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 3cf4332..2e0aec9 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -28,7 +28,9 @@
import static com.android.internal.app.procstats.ProcessStats.ADJ_MEM_FACTOR_NORMAL;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_OOM_ADJ;
import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PSS;
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_RSS;
import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_PSS;
+import static com.android.server.am.ActivityManagerDebugConfig.POSTFIX_RSS;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.am.ActivityManagerService.DUMP_MEM_OOM_ADJ;
@@ -64,6 +66,7 @@
import android.os.Binder;
import android.os.Build;
import android.os.Debug;
+import android.os.Flags;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -125,6 +128,7 @@
private static final String TAG = TAG_WITH_CLASS_NAME ? "ProcessList" : TAG_AM;
static final String TAG_PSS = TAG + POSTFIX_PSS;
+ static final String TAG_RSS = TAG + POSTFIX_RSS;
static final String TAG_OOM_ADJ = ActivityManagerService.TAG_OOM_ADJ;
@@ -183,10 +187,10 @@
private volatile long mPssDeferralTime = 0;
/**
- * Processes we want to collect PSS data from.
+ * Processes we want to collect PSS or RSS data from.
*/
@GuardedBy("mProfilerLock")
- private final ArrayList<ProcessProfileRecord> mPendingPssProfiles = new ArrayList<>();
+ private final ArrayList<ProcessProfileRecord> mPendingPssOrRssProfiles = new ArrayList<>();
/**
* Depth of overlapping activity-start PSS deferral notes
@@ -200,18 +204,18 @@
private long mLastFullPssTime = SystemClock.uptimeMillis();
/**
- * If set, the next time we collect PSS data we should do a full collection
- * with data from native processes and the kernel.
+ * If set, the next time we collect PSS or RSS data we should do a full collection with data
+ * from native processes and the kernel.
*/
@GuardedBy("mProfilerLock")
- private boolean mFullPssPending = false;
+ private boolean mFullPssOrRssPending = false;
/**
- * If true, we are running under a test environment so will sample PSS from processes
- * much more rapidly to try to collect better data when the tests are rapidly
- * running through apps.
+ * If true, we are running under a test environment so will sample PSS or RSS from processes
+ * much more rapidly to try to collect better data when the tests are rapidly running through
+ * apps.
*/
- private volatile boolean mTestPssMode = false;
+ private volatile boolean mTestPssOrRssMode = false;
private final LowMemDetector mLowMemDetector;
@@ -598,7 +602,11 @@
public void handleMessage(Message msg) {
switch (msg.what) {
case COLLECT_PSS_BG_MSG:
- collectPssInBackground();
+ if (!Flags.removeAppProfilerPssCollection()) {
+ collectPssInBackground();
+ } else {
+ collectRssInBackground();
+ }
break;
case DEFER_PSS_MSG:
deferPssForActivityStart();
@@ -619,8 +627,8 @@
long start = SystemClock.uptimeMillis();
MemInfoReader memInfo = null;
synchronized (mProfilerLock) {
- if (mFullPssPending) {
- mFullPssPending = false;
+ if (mFullPssOrRssPending) {
+ mFullPssOrRssPending = false;
memInfo = new MemInfoReader();
}
}
@@ -673,16 +681,16 @@
int pid = -1;
long lastPssTime;
synchronized (mProfilerLock) {
- if (mPendingPssProfiles.size() <= 0) {
- if (mTestPssMode || DEBUG_PSS) {
+ if (mPendingPssOrRssProfiles.size() <= 0) {
+ if (mTestPssOrRssMode || DEBUG_PSS) {
Slog.d(TAG_PSS,
"Collected pss of " + num + " processes in "
+ (SystemClock.uptimeMillis() - start) + "ms");
}
- mPendingPssProfiles.clear();
+ mPendingPssOrRssProfiles.clear();
return;
}
- profile = mPendingPssProfiles.remove(0);
+ profile = mPendingPssOrRssProfiles.remove(0);
procState = profile.getPssProcState();
statType = profile.getPssStatType();
lastPssTime = profile.getLastPssTime();
@@ -740,13 +748,149 @@
} while (true);
}
+ // This method is analogous to collectPssInBackground() and is intended to be used as a
+ // replacement if Flags.removeAppProfilerPssCollection() is enabled. References to PSS in
+ // methods outside of AppProfiler have generally been kept where a new RSS equivalent is not
+ // technically necessary. These can be updated once the flag is completely rolled out.
+ private void collectRssInBackground() {
+ long start = SystemClock.uptimeMillis();
+ MemInfoReader memInfo = null;
+ synchronized (mProfilerLock) {
+ if (mFullPssOrRssPending) {
+ mFullPssOrRssPending = false;
+ memInfo = new MemInfoReader();
+ }
+ }
+ if (memInfo != null) {
+ updateCpuStatsNow();
+ long nativeTotalRss = 0;
+ final List<ProcessCpuTracker.Stats> stats;
+ synchronized (mProcessCpuTracker) {
+ stats = mProcessCpuTracker.getStats(st -> {
+ return st.vsize > 0 && st.uid < FIRST_APPLICATION_UID;
+ });
+ }
+
+ // We assume that if PSS collection isn't needed or desired, RSS collection can be
+ // disabled as well.
+ if (!mService.mConstants.APP_PROFILER_PSS_PROFILING_DISABLED) {
+ final int numOfStats = stats.size();
+ for (int j = 0; j < numOfStats; j++) {
+ synchronized (mService.mPidsSelfLocked) {
+ if (mService.mPidsSelfLocked.indexOfKey(stats.get(j).pid) >= 0) {
+ // This is one of our own processes; skip it.
+ continue;
+ }
+ }
+ nativeTotalRss += Debug.getRss(stats.get(j).pid, null);
+ }
+ }
+
+ memInfo.readMemInfo();
+ synchronized (mService.mProcessStats.mLock) {
+ // We assume that an enabled DEBUG_PSS can apply to RSS as well, since only one of
+ // either collectPssInBackground() or collectRssInBackground() will be used.
+ if (DEBUG_RSS) {
+ Slog.d(TAG_RSS, "Collected native and kernel memory in "
+ + (SystemClock.uptimeMillis() - start) + "ms");
+ }
+ final long cachedKb = memInfo.getCachedSizeKb();
+ final long freeKb = memInfo.getFreeSizeKb();
+ final long zramKb = memInfo.getZramTotalSizeKb();
+ final long kernelKb = memInfo.getKernelUsedSizeKb();
+ // The last value needs to be updated in log tags to refer to RSS; this will be
+ // updated once the flag is fully rolled out.
+ EventLogTags.writeAmMeminfo(cachedKb * 1024, freeKb * 1024, zramKb * 1024,
+ kernelKb * 1024, nativeTotalRss * 1024);
+ mService.mProcessStats.addSysMemUsageLocked(cachedKb, freeKb, zramKb, kernelKb,
+ nativeTotalRss);
+ }
+ }
+
+ // This loop differs from its original form in collectPssInBackground(), as it does not
+ // collect USS or SwapPss (since those are reported in smaps, not status).
+ int num = 0;
+ do {
+ ProcessProfileRecord profile;
+ int procState;
+ int statType;
+ int pid = -1;
+ long lastRssTime;
+ synchronized (mProfilerLock) {
+ if (mPendingPssOrRssProfiles.size() <= 0) {
+ if (mTestPssOrRssMode || DEBUG_RSS) {
+ Slog.d(TAG_RSS,
+ "Collected rss of " + num + " processes in "
+ + (SystemClock.uptimeMillis() - start) + "ms");
+ }
+ mPendingPssOrRssProfiles.clear();
+ return;
+ }
+ profile = mPendingPssOrRssProfiles.remove(0);
+ procState = profile.getPssProcState();
+ statType = profile.getPssStatType();
+ lastRssTime = profile.getLastPssTime();
+ long now = SystemClock.uptimeMillis();
+ if (profile.getThread() != null && procState == profile.getSetProcState()
+ && (lastRssTime + ProcessList.PSS_SAFE_TIME_FROM_STATE_CHANGE) < now) {
+ pid = profile.getPid();
+ } else {
+ profile.abortNextPssTime();
+ if (DEBUG_RSS) {
+ Slog.d(TAG_RSS, "Skipped rss collection of " + pid
+ + ": still need "
+ + (lastRssTime + ProcessList.PSS_SAFE_TIME_FROM_STATE_CHANGE - now)
+ + "ms until safe");
+ }
+ profile = null;
+ pid = 0;
+ }
+ }
+ if (profile != null) {
+ long startTime = SystemClock.currentThreadTimeMillis();
+ // skip background RSS calculation under the following situations:
+ // - app is capturing camera imagery
+ // - app is frozen and we have already collected RSS once.
+ final boolean skipRSSCollection =
+ (profile.mApp.mOptRecord != null
+ && profile.mApp.mOptRecord.skipPSSCollectionBecauseFrozen())
+ || mService.isCameraActiveForUid(profile.mApp.uid)
+ || mService.mConstants.APP_PROFILER_PSS_PROFILING_DISABLED;
+ long rss = skipRSSCollection ? 0 : Debug.getRss(pid, null);
+ long endTime = SystemClock.currentThreadTimeMillis();
+ synchronized (mProfilerLock) {
+ if (rss != 0 && profile.getThread() != null
+ && profile.getSetProcState() == procState
+ && profile.getPid() == pid && profile.getLastPssTime() == lastRssTime) {
+ num++;
+ profile.commitNextPssTime();
+ recordRssSampleLPf(profile, procState, rss, statType, endTime - startTime,
+ SystemClock.uptimeMillis());
+ } else {
+ profile.abortNextPssTime();
+ if (DEBUG_RSS) {
+ Slog.d(TAG_RSS, "Skipped rss collection of " + pid
+ + ": " + (profile.getThread() == null ? "NO_THREAD " : "")
+ + (skipRSSCollection ? "SKIP_RSS_COLLECTION " : "")
+ + (profile.getPid() != pid ? "PID_CHANGED " : "")
+ + " initState=" + procState + " curState="
+ + profile.getSetProcState() + " "
+ + (profile.getLastPssTime() != lastRssTime
+ ? "TIME_CHANGED" : ""));
+ }
+ }
+ }
+ }
+ } while (true);
+ }
+
@GuardedBy("mProfilerLock")
void updateNextPssTimeLPf(int procState, ProcessProfileRecord profile, long now,
boolean forceUpdate) {
if (!forceUpdate) {
if (now <= profile.getNextPssTime() && now <= Math.max(profile.getLastPssTime()
+ ProcessList.PSS_MAX_INTERVAL, profile.getLastStateTime()
- + ProcessList.minTimeFromStateChange(mTestPssMode))) {
+ + ProcessList.minTimeFromStateChange(mTestPssOrRssMode))) {
// update is not due, ignore it.
return;
}
@@ -755,7 +899,7 @@
}
}
profile.setNextPssTime(profile.computeNextPssTime(procState,
- mTestPssMode, mService.mAtmInternal.isSleeping(), now));
+ mTestPssOrRssMode, mService.mAtmInternal.isSleeping(), now));
}
/**
@@ -776,8 +920,8 @@
+ " lastPss=" + profile.getLastPss()
+ " state=" + ProcessList.makeProcStateString(procState));
}
- if (profile.getInitialIdlePss() == 0) {
- profile.setInitialIdlePss(pss);
+ if (profile.getInitialIdlePssOrRss() == 0) {
+ profile.setInitialIdlePssOrRss(pss);
}
profile.setLastPss(pss);
profile.setLastSwapPss(swapPss);
@@ -813,6 +957,72 @@
}
}
+ /**
+ * Record new RSS sample for a process.
+ *
+ * This method is analogous to recordPssSampleLPf() and is intended to be used as a replacement
+ * if Flags.removeAppProfilerPssCollection() is enabled. Functionally, this differs in that PSS,
+ * SwapPss, and USS are no longer collected and reported.
+ *
+ * This method will also poll PSS if the app has requested that a heap dump be taken if its PSS
+ * reaches some threshold set with ActivityManager.setWatchHeapLimit().
+ */
+ @GuardedBy("mProfilerLock")
+ private void recordRssSampleLPf(ProcessProfileRecord profile, int procState, long rss,
+ int statType, long rssDuration, long now) {
+ final ProcessRecord proc = profile.mApp;
+ // TODO(b/296454553): writeAmPss needs to be renamed to writeAmRss, and the zeroed out
+ // fields need to be removed. This will be updated once the flag is fully rolled out to
+ // avoid churn in the .logtags file, which has a mapping of IDs to tags (and is also
+ // technically deprecated).
+ EventLogTags.writeAmPss(
+ profile.getPid(), proc.uid, proc.processName, /* pss = */ 0, /* uss = */ 0,
+ /* swapPss = */ 0, rss * 1024, statType, procState, rssDuration);
+ profile.setLastPssTime(now);
+ // The PSS here is emitted in logs, so we can zero it out instead of subbing in RSS.
+ profile.addPss(/* pss = */ 0, /* uss = */ 0, rss, true, statType, rssDuration);
+ if (DEBUG_RSS) {
+ Slog.d(TAG_RSS,
+ "rss of " + proc.toShortString() + ": " + rss
+ + " lastRss=" + profile.getLastRss()
+ + " state=" + ProcessList.makeProcStateString(procState));
+ }
+ if (profile.getInitialIdlePssOrRss() == 0) {
+ profile.setInitialIdlePssOrRss(rss);
+ }
+ profile.setLastRss(rss);
+ if (procState >= ActivityManager.PROCESS_STATE_HOME) {
+ profile.setLastCachedRss(rss);
+ }
+
+ final SparseArray<Pair<Long, String>> watchUids =
+ mMemWatchProcesses.getMap().get(proc.processName);
+ Long check = null;
+ if (watchUids != null) {
+ Pair<Long, String> val = watchUids.get(proc.uid);
+ if (val == null) {
+ val = watchUids.get(0);
+ }
+ if (val != null) {
+ check = val.first;
+ }
+ }
+
+ if (check != null) {
+ long pss = Debug.getPss(profile.getPid(), null, null);
+ if ((pss * 1024) >= check && profile.getThread() != null
+ && mMemWatchDumpProcName == null) {
+ if (Build.IS_DEBUGGABLE || proc.isDebuggable()) {
+ Slog.w(TAG, "Process " + proc + " exceeded pss limit " + check + "; reporting");
+ startHeapDumpLPf(profile, false);
+ } else {
+ Slog.w(TAG, "Process " + proc + " exceeded pss limit " + check
+ + ", but debugging not enabled");
+ }
+ }
+ }
+ }
+
private final class RecordPssRunnable implements Runnable {
private final ProcessProfileRecord mProfile;
private final Uri mDumpUri;
@@ -984,10 +1194,10 @@
*/
@GuardedBy("mProfilerLock")
private boolean requestPssLPf(ProcessProfileRecord profile, int procState) {
- if (mPendingPssProfiles.contains(profile)) {
+ if (mPendingPssOrRssProfiles.contains(profile)) {
return false;
}
- if (mPendingPssProfiles.size() == 0) {
+ if (mPendingPssOrRssProfiles.size() == 0) {
final long deferral = (mPssDeferralTime > 0 && mActivityStartingNesting.get() > 0)
? mPssDeferralTime : 0;
if (DEBUG_PSS && deferral > 0) {
@@ -999,7 +1209,7 @@
if (DEBUG_PSS) Slog.d(TAG_PSS, "Requesting pss of: " + profile.mApp);
profile.setPssProcState(procState);
profile.setPssStatType(ProcessStats.ADD_PSS_INTERNAL_SINGLE);
- mPendingPssProfiles.add(profile);
+ mPendingPssOrRssProfiles.add(profile);
return true;
}
@@ -1009,7 +1219,7 @@
*/
@GuardedBy("mProfilerLock")
private void deferPssIfNeededLPf() {
- if (mPendingPssProfiles.size() > 0) {
+ if (mPendingPssOrRssProfiles.size() > 0) {
mBgHandler.removeMessages(BgHandler.COLLECT_PSS_BG_MSG);
mBgHandler.sendEmptyMessageDelayed(BgHandler.COLLECT_PSS_BG_MSG, mPssDeferralTime);
}
@@ -1063,12 +1273,12 @@
Slog.d(TAG_PSS, "Requesting pss of all procs! memLowered=" + memLowered);
}
mLastFullPssTime = now;
- mFullPssPending = true;
- for (int i = mPendingPssProfiles.size() - 1; i >= 0; i--) {
- mPendingPssProfiles.get(i).abortNextPssTime();
+ mFullPssOrRssPending = true;
+ for (int i = mPendingPssOrRssProfiles.size() - 1; i >= 0; i--) {
+ mPendingPssOrRssProfiles.get(i).abortNextPssTime();
}
- mPendingPssProfiles.ensureCapacity(mService.mProcessList.getLruSizeLOSP());
- mPendingPssProfiles.clear();
+ mPendingPssOrRssProfiles.ensureCapacity(mService.mProcessList.getLruSizeLOSP());
+ mPendingPssOrRssProfiles.clear();
mService.mProcessList.forEachLruProcessesLOSP(false, app -> {
final ProcessProfileRecord profile = app.mProfile;
if (profile.getThread() == null
@@ -1083,7 +1293,7 @@
profile.setPssStatType(always ? ProcessStats.ADD_PSS_INTERNAL_ALL_POLL
: ProcessStats.ADD_PSS_INTERNAL_ALL_MEM);
updateNextPssTimeLPf(profile.getSetProcState(), profile, now, true);
- mPendingPssProfiles.add(profile);
+ mPendingPssOrRssProfiles.add(profile);
}
});
if (!mBgHandler.hasMessages(BgHandler.COLLECT_PSS_BG_MSG)) {
@@ -1094,7 +1304,7 @@
void setTestPssMode(boolean enabled) {
synchronized (mProcLock) {
- mTestPssMode = enabled;
+ mTestPssOrRssMode = enabled;
if (enabled) {
// Whenever we enable the mode, we want to take a snapshot all of current
// process mem use.
@@ -1104,7 +1314,7 @@
}
boolean getTestPssMode() {
- return mTestPssMode;
+ return mTestPssOrRssMode;
}
@GuardedBy("mService")
@@ -2346,7 +2556,7 @@
synchronized (mProfilerLock) {
final ProcessProfileRecord profile = app.mProfile;
mProcessesToGc.remove(app);
- mPendingPssProfiles.remove(profile);
+ mPendingPssOrRssProfiles.remove(profile);
profile.abortNextPssTime();
}
}
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index b303346..b00dcd6 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -144,6 +144,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.ServiceInfo;
import android.net.NetworkPolicyManager;
+import android.os.Flags;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManagerInternal;
@@ -2418,9 +2419,23 @@
// normally be a B service, but if we are low on RAM and it
// is large we want to force it down since we would prefer to
// keep launcher over it.
+ long lastPssOrRss = !Flags.removeAppProfilerPssCollection()
+ ? app.mProfile.getLastPss() : app.mProfile.getLastRss();
+
+ // RSS is larger than PSS, but the RSS/PSS ratio varies per-process based on how
+ // many shared pages a process uses. The threshold is increased if the flag for
+ // reading RSS instead of PSS is enabled.
+ //
+ // TODO(b/296454553): Tune the second value so that the relative number of
+ // service B is similar before/after this flag is enabled.
+ double thresholdModifier = !Flags.removeAppProfilerPssCollection()
+ ? 1
+ : mConstants.PSS_TO_RSS_THRESHOLD_MODIFIER;
+ double cachedRestoreThreshold =
+ mProcessList.getCachedRestoreThresholdKb() * thresholdModifier;
+
if (!mService.mAppProfiler.isLastMemoryLevelNormal()
- && app.mProfile.getLastPss()
- >= mProcessList.getCachedRestoreThresholdKb()) {
+ && lastPssOrRss >= cachedRestoreThreshold) {
state.setServiceHighRam(true);
state.setServiceB(true);
//Slog.i(TAG, "ADJ " + app + " high ram!");
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 2efac12..cb2b5fb 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -94,6 +94,7 @@
import android.os.Build;
import android.os.Bundle;
import android.os.DropBoxManager;
+import android.os.Flags;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -4594,6 +4595,8 @@
r.mProfile.getLastPss() * 1024, new StringBuilder()));
proto.write(ProcessOomProto.Detail.LAST_SWAP_PSS, DebugUtils.sizeValueToString(
r.mProfile.getLastSwapPss() * 1024, new StringBuilder()));
+ // TODO(b/296454553): This proto field should be replaced with last cached RSS once
+ // AppProfiler is no longer collecting PSS.
proto.write(ProcessOomProto.Detail.LAST_CACHED_PSS, DebugUtils.sizeValueToString(
r.mProfile.getLastCachedPss() * 1024, new StringBuilder()));
proto.write(ProcessOomProto.Detail.CACHED, state.isCached());
@@ -4725,12 +4728,20 @@
pw.print(" ");
pw.print("state: cur="); pw.print(makeProcStateString(state.getCurProcState()));
pw.print(" set="); pw.print(makeProcStateString(state.getSetProcState()));
- pw.print(" lastPss=");
- DebugUtils.printSizeValue(pw, r.mProfile.getLastPss() * 1024);
- pw.print(" lastSwapPss=");
- DebugUtils.printSizeValue(pw, r.mProfile.getLastSwapPss() * 1024);
- pw.print(" lastCachedPss=");
- DebugUtils.printSizeValue(pw, r.mProfile.getLastCachedPss() * 1024);
+ // These values won't be collected if the flag is enabled.
+ if (!Flags.removeAppProfilerPssCollection()) {
+ pw.print(" lastPss=");
+ DebugUtils.printSizeValue(pw, r.mProfile.getLastPss() * 1024);
+ pw.print(" lastSwapPss=");
+ DebugUtils.printSizeValue(pw, r.mProfile.getLastSwapPss() * 1024);
+ pw.print(" lastCachedPss=");
+ DebugUtils.printSizeValue(pw, r.mProfile.getLastCachedPss() * 1024);
+ } else {
+ pw.print(" lastRss=");
+ DebugUtils.printSizeValue(pw, r.mProfile.getLastRss() * 1024);
+ pw.print(" lastCachedRss=");
+ DebugUtils.printSizeValue(pw, r.mProfile.getLastCachedRss() * 1024);
+ }
pw.println();
pw.print(prefix);
pw.print(" ");
diff --git a/services/core/java/com/android/server/am/ProcessProfileRecord.java b/services/core/java/com/android/server/am/ProcessProfileRecord.java
index 354f3d3..8ca64f8 100644
--- a/services/core/java/com/android/server/am/ProcessProfileRecord.java
+++ b/services/core/java/com/android/server/am/ProcessProfileRecord.java
@@ -23,6 +23,7 @@
import android.app.ProcessMemoryState.HostingComponentType;
import android.content.pm.ApplicationInfo;
import android.os.Debug;
+import android.os.Flags;
import android.os.Process;
import android.os.SystemClock;
import android.util.DebugUtils;
@@ -32,7 +33,6 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.app.procstats.ProcessState;
import com.android.internal.app.procstats.ProcessStats;
-import com.android.internal.util.FrameworkStatsLog;
import com.android.server.am.ProcessList.ProcStateMemTracker;
import com.android.server.power.stats.BatteryStatsImpl;
@@ -42,6 +42,8 @@
/**
* Profiling info of the process, such as PSS, cpu, etc.
+ *
+ * TODO(b/297542292): Update PSS names with RSS once AppProfiler's PSS profiling has been replaced.
*/
final class ProcessProfileRecord {
final ProcessRecord mApp;
@@ -76,7 +78,7 @@
* Initial memory pss of process for idle maintenance.
*/
@GuardedBy("mProfilerLock")
- private long mInitialIdlePss;
+ private long mInitialIdlePssOrRss;
/**
* Last computed memory pss.
@@ -109,6 +111,14 @@
private long mLastRss;
/**
+ * Last computed rss when in cached state.
+ *
+ * This value is not set or retrieved unless Flags.removeAppProfilerPssCollection() is true.
+ */
+ @GuardedBy("mProfilerLock")
+ private long mLastCachedRss;
+
+ /**
* Cache of last retrieve memory info, to throttle how frequently apps can request it.
*/
@GuardedBy("mProfilerLock")
@@ -347,13 +357,13 @@
}
@GuardedBy("mProfilerLock")
- long getInitialIdlePss() {
- return mInitialIdlePss;
+ long getInitialIdlePssOrRss() {
+ return mInitialIdlePssOrRss;
}
@GuardedBy("mProfilerLock")
- void setInitialIdlePss(long initialIdlePss) {
- mInitialIdlePss = initialIdlePss;
+ void setInitialIdlePssOrRss(long initialIdlePssOrRss) {
+ mInitialIdlePssOrRss = initialIdlePssOrRss;
}
@GuardedBy("mProfilerLock")
@@ -377,6 +387,16 @@
}
@GuardedBy("mProfilerLock")
+ long getLastCachedRss() {
+ return mLastCachedRss;
+ }
+
+ @GuardedBy("mProfilerLock")
+ void setLastCachedRss(long lastCachedRss) {
+ mLastCachedRss = lastCachedRss;
+ }
+
+ @GuardedBy("mProfilerLock")
long getLastSwapPss() {
return mLastSwapPss;
}
@@ -530,26 +550,6 @@
}
}
- void reportCachedKill() {
- synchronized (mService.mProcessStats.mLock) {
- final ProcessState tracker = mBaseProcessTracker;
- if (tracker != null) {
- final PackageList pkgList = mApp.getPkgList();
- synchronized (pkgList) {
- tracker.reportCachedKill(pkgList.getPackageListLocked(), mLastCachedPss);
- pkgList.forEachPackageProcessStats(holder ->
- FrameworkStatsLog.write(FrameworkStatsLog.CACHED_KILL_REPORTED,
- getUidForAttribution(mApp),
- holder.state.getName(),
- holder.state.getPackage(),
- mLastCachedPss,
- holder.appVersion)
- );
- }
- }
- }
- }
-
void setProcessTrackerState(int procState, int memFactor) {
synchronized (mService.mProcessStats.mLock) {
final ProcessState tracker = mBaseProcessTracker;
@@ -676,27 +676,46 @@
@GuardedBy("mService")
void dumpPss(PrintWriter pw, String prefix, long nowUptime) {
synchronized (mProfilerLock) {
- pw.print(prefix);
- pw.print("lastPssTime=");
- TimeUtils.formatDuration(mLastPssTime, nowUptime, pw);
- pw.print(" pssProcState=");
- pw.print(mPssProcState);
- pw.print(" pssStatType=");
- pw.print(mPssStatType);
- pw.print(" nextPssTime=");
- TimeUtils.formatDuration(mNextPssTime, nowUptime, pw);
- pw.println();
- pw.print(prefix);
- pw.print("lastPss=");
- DebugUtils.printSizeValue(pw, mLastPss * 1024);
- pw.print(" lastSwapPss=");
- DebugUtils.printSizeValue(pw, mLastSwapPss * 1024);
- pw.print(" lastCachedPss=");
- DebugUtils.printSizeValue(pw, mLastCachedPss * 1024);
- pw.print(" lastCachedSwapPss=");
- DebugUtils.printSizeValue(pw, mLastCachedSwapPss * 1024);
- pw.print(" lastRss=");
- DebugUtils.printSizeValue(pw, mLastRss * 1024);
+ // TODO(b/297542292): Remove this case once PSS profiling is replaced
+ if (!Flags.removeAppProfilerPssCollection()) {
+ pw.print(prefix);
+ pw.print("lastPssTime=");
+ TimeUtils.formatDuration(mLastPssTime, nowUptime, pw);
+ pw.print(" pssProcState=");
+ pw.print(mPssProcState);
+ pw.print(" pssStatType=");
+ pw.print(mPssStatType);
+ pw.print(" nextPssTime=");
+ TimeUtils.formatDuration(mNextPssTime, nowUptime, pw);
+ pw.println();
+ pw.print(prefix);
+ pw.print("lastPss=");
+ DebugUtils.printSizeValue(pw, mLastPss * 1024);
+ pw.print(" lastSwapPss=");
+ DebugUtils.printSizeValue(pw, mLastSwapPss * 1024);
+ pw.print(" lastCachedPss=");
+ DebugUtils.printSizeValue(pw, mLastCachedPss * 1024);
+ pw.print(" lastCachedSwapPss=");
+ DebugUtils.printSizeValue(pw, mLastCachedSwapPss * 1024);
+ pw.print(" lastRss=");
+ DebugUtils.printSizeValue(pw, mLastRss * 1024);
+ } else {
+ pw.print(prefix);
+ pw.print("lastRssTime=");
+ TimeUtils.formatDuration(mLastPssTime, nowUptime, pw);
+ pw.print(" rssProcState=");
+ pw.print(mPssProcState);
+ pw.print(" rssStatType=");
+ pw.print(mPssStatType);
+ pw.print(" nextRssTime=");
+ TimeUtils.formatDuration(mNextPssTime, nowUptime, pw);
+ pw.println();
+ pw.print(prefix);
+ pw.print("lastRss=");
+ DebugUtils.printSizeValue(pw, mLastRss * 1024);
+ pw.print(" lastCachedRss=");
+ DebugUtils.printSizeValue(pw, mLastCachedRss * 1024);
+ }
pw.println();
pw.print(prefix);
pw.print("trimMemoryLevel=");
diff --git a/services/core/java/com/android/server/am/ProcessStateRecord.java b/services/core/java/com/android/server/am/ProcessStateRecord.java
index 27c0876..8723c5d 100644
--- a/services/core/java/com/android/server/am/ProcessStateRecord.java
+++ b/services/core/java/com/android/server/am/ProcessStateRecord.java
@@ -29,6 +29,7 @@
import android.annotation.ElapsedRealtimeLong;
import android.app.ActivityManager;
import android.content.ComponentName;
+import android.os.Flags;
import android.os.SystemClock;
import android.os.Trace;
import android.util.Slog;
@@ -1366,7 +1367,12 @@
}
if (mNotCachedSinceIdle) {
pw.print(prefix); pw.print("notCachedSinceIdle="); pw.print(mNotCachedSinceIdle);
- pw.print(" initialIdlePss="); pw.println(mApp.mProfile.getInitialIdlePss());
+ if (!Flags.removeAppProfilerPssCollection()) {
+ pw.print(" initialIdlePss=");
+ } else {
+ pw.print(" initialIdleRss=");
+ }
+ pw.println(mApp.mProfile.getInitialIdlePssOrRss());
}
if (hasTopUi() || hasOverlayUi() || mRunningRemoteAnimation) {
pw.print(prefix); pw.print("hasTopUi="); pw.print(hasTopUi());
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 028be88..192fd6f 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -123,6 +123,7 @@
"angle",
"app_widgets",
"arc_next",
+ "avic",
"bluetooth",
"build",
"biometrics",
@@ -140,6 +141,7 @@
"context_hub",
"core_experiments_team_internal",
"core_graphics",
+ "dck_framework",
"game",
"haptics",
"hardware_backed_security_mainline",
@@ -184,6 +186,7 @@
"wear_security",
"wear_system_health",
"wear_systems",
+ "wear_sysui",
"window_surfaces",
"windowing_frontend",
};
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 1e38c0f..9031cde 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -118,6 +118,7 @@
import android.media.ICommunicationDeviceDispatcher;
import android.media.IDeviceVolumeBehaviorDispatcher;
import android.media.IDevicesForAttributesCallback;
+import android.media.ILoudnessCodecUpdatesDispatcher;
import android.media.IMuteAwaitConnectionCallback;
import android.media.IPlaybackConfigDispatcher;
import android.media.IPreferredMixerAttributesDispatcher;
@@ -132,6 +133,7 @@
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.IStreamAliasingDispatcher;
import android.media.IVolumeController;
+import android.media.LoudnessCodecFormat;
import android.media.MediaMetrics;
import android.media.MediaRecorder.AudioSource;
import android.media.PlayerBase;
@@ -4370,6 +4372,7 @@
if (mMediaPlaybackActive.getAndSet(mediaActive) != mediaActive && mediaActive) {
mSoundDoseHelper.scheduleMusicActiveCheck();
}
+
// Update playback active state for all apps in audio mode stack.
// When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
// and request an audio mode update immediately. Upon any other change, queue the message
@@ -10590,6 +10593,51 @@
return anonymizeAudioDeviceAttributesUnchecked(ada);
}
+ // ========================================================================================
+ // LoudnessCodecConfigurator
+
+ @Override
+ public void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
+ // TODO: implement
+ }
+
+ @Override
+ public void unregisterLoudnessCodecUpdatesDispatcher(
+ ILoudnessCodecUpdatesDispatcher dispatcher) {
+ // TODO: implement
+ }
+
+ @Override
+ public void startLoudnessCodecUpdates(int piid) {
+ // TODO: implement
+ }
+
+ @Override
+ public void stopLoudnessCodecUpdates(int piid) {
+ // TODO: implement
+ }
+
+ @Override
+ public void addLoudnesssCodecFormat(int piid, LoudnessCodecFormat format) {
+ // TODO: implement
+ }
+
+ @Override
+ public void addLoudnesssCodecFormatList(int piid, List<LoudnessCodecFormat> format) {
+ // TODO: implement
+ }
+
+ @Override
+ public void removeLoudnessCodecFormat(int piid, LoudnessCodecFormat format) {
+ // TODO: implement
+ }
+
+ @Override
+ public PersistableBundle getLoudnessParams(int piid, LoudnessCodecFormat format) {
+ // TODO: implement
+ return null;
+ }
+
//==========================================================================================
// camera sound is forced if any of the resources corresponding to one active SIM
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index aab491e..8e0289e 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -47,9 +47,6 @@
private final NativeInputManagerService mNative;
private final Map<Uri, Consumer<String /* reason*/>> mObservers;
- // Cache prevent notifying same KeyRepeatInfo data to native code multiple times.
- private KeyRepeatInfo mLastKeyRepeatInfoSettingsUpdate;
-
InputSettingsObserver(Context context, Handler handler, InputManagerService service,
NativeInputManagerService nativeIms) {
super(handler);
@@ -84,9 +81,9 @@
Map.entry(Settings.System.getUriFor(Settings.System.SHOW_KEY_PRESSES),
(reason) -> updateShowKeyPresses()),
Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_TIMEOUT_MS),
- (reason) -> updateKeyRepeatInfo(getLatestLongPressTimeoutValue())),
+ (reason) -> updateKeyRepeatInfo()),
Map.entry(Settings.Secure.getUriFor(Settings.Secure.KEY_REPEAT_DELAY_MS),
- (reason) -> updateKeyRepeatInfo(getLatestLongPressTimeoutValue())),
+ (reason) -> updateKeyRepeatInfo()),
Map.entry(Settings.System.getUriFor(Settings.System.SHOW_ROTARY_INPUT),
(reason) -> updateShowRotaryInput()));
}
@@ -182,46 +179,32 @@
}
private void updateLongPressTimeout(String reason) {
- final int longPressTimeoutValue = getLatestLongPressTimeoutValue();
-
- // Before the key repeat timeout was introduced, some users relied on changing
- // LONG_PRESS_TIMEOUT settings to also change the key repeat timeout. To support this
- // backward compatibility, we'll preemptively update key repeat info here, in case where
- // key repeat timeout was never set, and user is still relying on long press timeout value.
- updateKeyRepeatInfo(longPressTimeoutValue);
+ // Not using ViewConfiguration.getLongPressTimeout here because it may return a stale value.
+ final int longPressTimeoutMs = Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.LONG_PRESS_TIMEOUT, ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT,
+ UserHandle.USER_CURRENT);
final boolean featureEnabledFlag =
DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_INPUT_NATIVE_BOOT,
DEEP_PRESS_ENABLED, true /* default */);
final boolean enabled =
featureEnabledFlag
- && longPressTimeoutValue <= ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT;
+ && longPressTimeoutMs <= ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT;
Log.i(TAG, (enabled ? "Enabling" : "Disabling") + " motion classifier because " + reason
+ ": feature " + (featureEnabledFlag ? "enabled" : "disabled")
- + ", long press timeout = " + longPressTimeoutValue);
+ + ", long press timeout = " + longPressTimeoutMs + " ms");
mNative.setMotionClassifierEnabled(enabled);
}
- private void updateKeyRepeatInfo(int fallbackKeyRepeatTimeoutValue) {
- // Not using ViewConfiguration.getKeyRepeatTimeout here because it may return a stale value.
+ private void updateKeyRepeatInfo() {
+ // Use ViewConfiguration getters only as fallbacks because they may return stale values.
final int timeoutMs = Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.KEY_REPEAT_TIMEOUT_MS, fallbackKeyRepeatTimeoutValue,
+ Settings.Secure.KEY_REPEAT_TIMEOUT_MS, ViewConfiguration.getKeyRepeatTimeout(),
UserHandle.USER_CURRENT);
final int delayMs = Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.KEY_REPEAT_DELAY_MS, ViewConfiguration.getKeyRepeatDelay(),
UserHandle.USER_CURRENT);
- if (mLastKeyRepeatInfoSettingsUpdate == null || !mLastKeyRepeatInfoSettingsUpdate.isEqualTo(
- timeoutMs, delayMs)) {
- mNative.setKeyRepeatConfiguration(timeoutMs, delayMs);
- mLastKeyRepeatInfoSettingsUpdate = new KeyRepeatInfo(timeoutMs, delayMs);
- }
- }
-
- // Not using ViewConfiguration.getLongPressTimeout here because it may return a stale value.
- private int getLatestLongPressTimeoutValue() {
- return Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.LONG_PRESS_TIMEOUT, ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT,
- UserHandle.USER_CURRENT);
+ mNative.setKeyRepeatConfiguration(timeoutMs, delayMs);
}
private void updateMaximumObscuringOpacityForTouch() {
@@ -233,19 +216,4 @@
}
mNative.setMaximumObscuringOpacityForTouch(opacity);
}
-
- private static class KeyRepeatInfo {
- private final int mKeyRepeatTimeoutMs;
- private final int mKeyRepeatDelayMs;
-
- private KeyRepeatInfo(int keyRepeatTimeoutMs, int keyRepeatDelayMs) {
- this.mKeyRepeatTimeoutMs = keyRepeatTimeoutMs;
- this.mKeyRepeatDelayMs = keyRepeatDelayMs;
- }
-
- public boolean isEqualTo(int keyRepeatTimeoutMs, int keyRepeatDelayMs) {
- return mKeyRepeatTimeoutMs == keyRepeatTimeoutMs
- && mKeyRepeatDelayMs == keyRepeatDelayMs;
- }
- }
}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a158b18..c24d6a0 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -527,12 +527,27 @@
@Override
public boolean canHandleVolumeKey() {
if (isPlaybackTypeLocal()) {
+ if (DEBUG) {
+ Log.d(TAG, "Local MediaSessionRecord can handle volume key");
+ }
return true;
}
if (mVolumeControlType == VOLUME_CONTROL_FIXED) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Local MediaSessionRecord with FIXED volume control can't handle volume"
+ + " key");
+ }
return false;
}
if (mVolumeAdjustmentForRemoteGroupSessions) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Volume adjustment for remote group sessions allowed so MediaSessionRecord"
+ + " can handle volume key");
+ }
return true;
}
// See b/228021646 for details.
@@ -540,7 +555,18 @@
List<RoutingSessionInfo> sessions = mRouter2Manager.getRoutingSessions(mPackageName);
boolean foundNonSystemSession = false;
boolean remoteSessionAllowVolumeAdjustment = true;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Found "
+ + sessions.size()
+ + " routing sessions for package name "
+ + mPackageName);
+ }
for (RoutingSessionInfo session : sessions) {
+ if (DEBUG) {
+ Log.d(TAG, "Found routingSessionInfo: " + session);
+ }
if (!session.isSystemSession()) {
foundNonSystemSession = true;
if (session.getVolumeHandling() == PLAYBACK_VOLUME_FIXED) {
@@ -549,9 +575,15 @@
}
}
if (!foundNonSystemSession) {
- Log.d(TAG, "Package " + mPackageName
- + " has a remote media session but no associated routing session");
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Package "
+ + mPackageName
+ + " has a remote media session but no associated routing session");
+ }
}
+
return foundNonSystemSession && remoteSessionAllowVolumeAdjustment;
}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index a1704c6..cb05084 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -860,6 +860,9 @@
rule.enabled = automaticZenRule.isEnabled();
rule.modified = automaticZenRule.isModified();
rule.zenPolicy = automaticZenRule.getZenPolicy();
+ if (Flags.modesApi()) {
+ rule.zenDeviceEffects = automaticZenRule.getDeviceEffects();
+ }
rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
rule.configurationActivity = automaticZenRule.getConfigurationActivity();
@@ -888,6 +891,7 @@
.setIconResId(rule.iconResId)
.setType(rule.type)
.setZenPolicy(rule.zenPolicy)
+ .setDeviceEffects(rule.zenDeviceEffects)
.setEnabled(rule.enabled)
.setInterruptionFilter(
NotificationManager.zenModeToInterruptionFilter(rule.zenMode))
diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java
index 5e76ae5..11a6d1b 100644
--- a/services/core/java/com/android/server/pm/ComputerEngine.java
+++ b/services/core/java/com/android/server/pm/ComputerEngine.java
@@ -1533,6 +1533,7 @@
ai, flags, state, userId);
pi.signingInfo = ps.getSigningInfo();
pi.signatures = getDeprecatedSignatures(pi.signingInfo.getSigningDetails(), flags);
+ pi.setArchiveTimeMillis(state.getArchiveTimeMillis());
if (DEBUG_PACKAGE_INFO) {
Log.v(TAG, "ps.pkg is n/a for ["
diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java
index f45571a..65c6329 100644
--- a/services/core/java/com/android/server/pm/DeletePackageHelper.java
+++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java
@@ -21,6 +21,7 @@
import static android.content.pm.Flags.sdkLibIndependence;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
+import static android.content.pm.PackageManager.DELETE_ARCHIVE;
import static android.content.pm.PackageManager.DELETE_KEEP_DATA;
import static android.content.pm.PackageManager.DELETE_SUCCEEDED;
import static android.content.pm.PackageManager.MATCH_KNOWN_PACKAGES;
@@ -606,6 +607,10 @@
firstInstallTime,
PackageManager.USER_MIN_ASPECT_RATIO_UNSET,
archiveState);
+
+ if ((flags & DELETE_ARCHIVE) != 0) {
+ ps.modifyUserState(nextUserId).setArchiveTimeMillis(System.currentTimeMillis());
+ }
}
mPm.mSettings.writeKernelMappingLPr(ps);
}
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 3cf5481..72090f2 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -91,13 +91,15 @@
@IntDef({
INSTALL_PERMISSION_FIXED,
UPDATE_AVAILABLE,
- FORCE_QUERYABLE_OVERRIDE
+ FORCE_QUERYABLE_OVERRIDE,
+ SCANNED_AS_STOPPED_SYSTEM_APP
})
public @interface Flags {
}
private static final int INSTALL_PERMISSION_FIXED = 1;
private static final int UPDATE_AVAILABLE = 1 << 1;
private static final int FORCE_QUERYABLE_OVERRIDE = 1 << 2;
+ private static final int SCANNED_AS_STOPPED_SYSTEM_APP = 1 << 3;
}
private int mBooleans;
@@ -768,6 +770,11 @@
onChanged();
}
+ void setArchiveTimeMillis(long value, int userId) {
+ modifyUserState(userId).setArchiveTimeMillis(value);
+ onChanged();
+ }
+
boolean getInstalled(int userId) {
return readUserState(userId).isInstalled();
}
@@ -881,6 +888,12 @@
onChanged();
}
+ public PackageSetting setScannedAsStoppedSystemApp(boolean stop) {
+ setBoolean(Booleans.SCANNED_AS_STOPPED_SYSTEM_APP, stop);
+ onChanged();
+ return this;
+ }
+
boolean getNotLaunched(int userId) {
return readUserState(userId).isNotLaunched();
}
@@ -1559,6 +1572,11 @@
return (getFlags() & ApplicationInfo.FLAG_PERSISTENT) != 0;
}
+ @Override
+ public boolean isScannedAsStoppedSystemApp() {
+ return getBoolean(Booleans.SCANNED_AS_STOPPED_SYSTEM_APP);
+ }
+
// Code below generated by codegen v1.0.23.
@@ -1707,10 +1725,10 @@
}
@DataClass.Generated(
- time = 1698188444364L,
+ time = 1700251133016L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/PackageSetting.java",
- inputSignatures = "private int mBooleans\nprivate int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate int mAppId\nprivate @android.annotation.Nullable com.android.server.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate float mLoadingProgress\nprivate long mLoadingCompletedTime\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate long mLastModifiedTime\nprivate long lastUpdateTime\nprivate long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate int categoryOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate @android.annotation.Nullable java.lang.String mAppMetadataFilePath\nprivate int mTargetSdkVersion\nprivate @android.annotation.Nullable byte[] mRestrictUpdateHash\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\nprivate com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic com.android.server.pm.PackageSetting snapshot()\npublic void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic com.android.server.pm.PackageSetting setAppId(int)\npublic com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic com.android.server.pm.PackageSetting setInstallerPackage(java.lang.String,int)\npublic com.android.server.pm.PackageSetting setUpdateOwnerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPkg(com.android.server.pm.pkg.AndroidPackage)\npublic com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic com.android.server.pm.PackageSetting setSharedUserAppId(int)\npublic com.android.server.pm.PackageSetting setTargetSdkVersion(int)\npublic com.android.server.pm.PackageSetting setRestrictUpdateHash(byte[])\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprivate void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic void updateFrom(com.android.server.pm.PackageSetting)\n com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic boolean isPrivileged()\npublic boolean isOem()\npublic boolean isVendor()\npublic boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic boolean isSystemExt()\npublic boolean isOdm()\npublic boolean isSystem()\npublic boolean isRequestLegacyExternalStorage()\npublic boolean isUserDataFragile()\npublic android.content.pm.SigningDetails getSigningDetails()\npublic com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n void setEnabled(int,int,java.lang.String)\n int getEnabled(int)\n void setInstalled(boolean,int)\n boolean getInstalled(int)\n int getInstallReason(int)\n void setInstallReason(int,int)\n int getUninstallReason(int)\n void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n boolean isInstalledOrHasDataOnAnyOtherUser(int[],int)\n int[] queryInstalledUsers(int[],boolean)\n int[] queryUsersInstalledOrHasData(int[])\n long getCeDataInode(int)\n long getDeDataInode(int)\n void setCeDataInode(long,int)\n void setDeDataInode(long,int)\n boolean getStopped(int)\n void setStopped(boolean,int)\n boolean getNotLaunched(int)\n void setNotLaunched(boolean,int)\n boolean getHidden(int)\n void setHidden(boolean,int)\n int getDistractionFlags(int)\n void setDistractionFlags(int,int)\npublic boolean getInstantApp(int)\n void setInstantApp(boolean,int)\n boolean getVirtualPreload(int)\n void setVirtualPreload(boolean,int)\n void setUserState(int,long,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long,int,com.android.server.pm.pkg.ArchiveState)\n void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n void addDisabledComponent(java.lang.String,int)\n void addEnabledComponent(java.lang.String,int)\n boolean enableComponentLPw(java.lang.String,int)\n boolean disableComponentLPw(java.lang.String,int)\n boolean restoreComponentLPw(java.lang.String,int)\n int getCurrentEnabledStateLPr(java.lang.String,int)\n void removeUser(int)\npublic int[] getNotInstalledUserIds()\n void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\nprivate static void writeArchiveState(android.util.proto.ProtoOutputStream,com.android.server.pm.pkg.ArchiveState)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic boolean isIncremental()\npublic boolean isLoading()\npublic com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic com.android.server.pm.PackageSetting setLoadingCompletedTime(long)\npublic com.android.server.pm.PackageSetting setAppMetadataFilePath(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<com.android.server.pm.pkg.SharedLibrary> getSharedLibraryDependencies()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryInfo(android.content.pm.SharedLibraryInfo)\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryFile(java.lang.String)\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getApexModuleName()\npublic com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic com.android.server.pm.PackageSetting setApexModuleName(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbi()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbi()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getSeInfo()\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbiLegacy()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbiLegacy()\npublic @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy @java.lang.Override int getHiddenApiEnforcementPolicy()\npublic @java.lang.Override boolean isApex()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isDefaultToDeviceProtectedStorage()\npublic @java.lang.Override boolean isPersistent()\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\nprivate static final int INSTALL_PERMISSION_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
+ inputSignatures = "private int mBooleans\nprivate int mSharedUserAppId\nprivate @android.annotation.Nullable java.util.Map<java.lang.String,java.util.Set<java.lang.String>> mimeGroups\nprivate @android.annotation.Nullable java.lang.String[] usesSdkLibraries\nprivate @android.annotation.Nullable long[] usesSdkLibrariesVersionsMajor\nprivate @android.annotation.Nullable java.lang.String[] usesStaticLibraries\nprivate @android.annotation.Nullable long[] usesStaticLibrariesVersions\nprivate @android.annotation.Nullable @java.lang.Deprecated java.lang.String legacyNativeLibraryPath\nprivate @android.annotation.NonNull java.lang.String mName\nprivate @android.annotation.Nullable java.lang.String mRealName\nprivate int mAppId\nprivate @android.annotation.Nullable com.android.server.pm.parsing.pkg.AndroidPackageInternal pkg\nprivate @android.annotation.NonNull java.io.File mPath\nprivate @android.annotation.NonNull java.lang.String mPathString\nprivate float mLoadingProgress\nprivate long mLoadingCompletedTime\nprivate @android.annotation.Nullable java.lang.String mPrimaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mSecondaryCpuAbi\nprivate @android.annotation.Nullable java.lang.String mCpuAbiOverride\nprivate long mLastModifiedTime\nprivate long lastUpdateTime\nprivate long versionCode\nprivate @android.annotation.NonNull com.android.server.pm.PackageSignatures signatures\nprivate @android.annotation.NonNull com.android.server.pm.PackageKeySetData keySetData\nprivate final @android.annotation.NonNull android.util.SparseArray<com.android.server.pm.pkg.PackageUserStateImpl> mUserStates\nprivate @android.annotation.NonNull com.android.server.pm.InstallSource installSource\nprivate @android.annotation.Nullable java.lang.String volumeUuid\nprivate int categoryOverride\nprivate final @android.annotation.NonNull com.android.server.pm.pkg.PackageStateUnserialized pkgState\nprivate @android.annotation.NonNull java.util.UUID mDomainSetId\nprivate @android.annotation.Nullable java.lang.String mAppMetadataFilePath\nprivate int mTargetSdkVersion\nprivate @android.annotation.Nullable byte[] mRestrictUpdateHash\nprivate final @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> mSnapshot\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\nprivate com.android.server.utils.SnapshotCache<com.android.server.pm.PackageSetting> makeCache()\npublic com.android.server.pm.PackageSetting snapshot()\npublic void dumpDebug(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\npublic com.android.server.pm.PackageSetting setAppId(int)\npublic com.android.server.pm.PackageSetting setCpuAbiOverride(java.lang.String)\npublic com.android.server.pm.PackageSetting setFirstInstallTimeFromReplaced(com.android.server.pm.pkg.PackageStateInternal,int[])\npublic com.android.server.pm.PackageSetting setFirstInstallTime(long,int)\npublic com.android.server.pm.PackageSetting setForceQueryableOverride(boolean)\npublic com.android.server.pm.PackageSetting setInstallerPackage(java.lang.String,int)\npublic com.android.server.pm.PackageSetting setUpdateOwnerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setInstallSource(com.android.server.pm.InstallSource)\n com.android.server.pm.PackageSetting removeInstallerPackage(java.lang.String)\npublic com.android.server.pm.PackageSetting setIsOrphaned(boolean)\npublic com.android.server.pm.PackageSetting setKeySetData(com.android.server.pm.PackageKeySetData)\npublic com.android.server.pm.PackageSetting setLastModifiedTime(long)\npublic com.android.server.pm.PackageSetting setLastUpdateTime(long)\npublic com.android.server.pm.PackageSetting setLongVersionCode(long)\npublic boolean setMimeGroup(java.lang.String,android.util.ArraySet<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPkg(com.android.server.pm.pkg.AndroidPackage)\npublic com.android.server.pm.PackageSetting setPkgStateLibraryFiles(java.util.Collection<java.lang.String>)\npublic com.android.server.pm.PackageSetting setPrimaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSecondaryCpuAbi(java.lang.String)\npublic com.android.server.pm.PackageSetting setSignatures(com.android.server.pm.PackageSignatures)\npublic com.android.server.pm.PackageSetting setVolumeUuid(java.lang.String)\npublic @java.lang.Override boolean isExternalStorage()\npublic com.android.server.pm.PackageSetting setUpdateAvailable(boolean)\npublic com.android.server.pm.PackageSetting setSharedUserAppId(int)\npublic com.android.server.pm.PackageSetting setTargetSdkVersion(int)\npublic com.android.server.pm.PackageSetting setRestrictUpdateHash(byte[])\npublic @java.lang.Override int getSharedUserAppId()\npublic @java.lang.Override boolean hasSharedUser()\npublic @java.lang.Override java.lang.String toString()\nprivate void copyMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic void updateFrom(com.android.server.pm.PackageSetting)\n com.android.server.pm.PackageSetting updateMimeGroups(java.util.Set<java.lang.String>)\npublic @java.lang.Deprecated @java.lang.Override com.android.server.pm.permission.LegacyPermissionState getLegacyPermissionState()\npublic com.android.server.pm.PackageSetting setInstallPermissionsFixed(boolean)\npublic boolean isPrivileged()\npublic boolean isOem()\npublic boolean isVendor()\npublic boolean isProduct()\npublic @java.lang.Override boolean isRequiredForSystemUser()\npublic boolean isSystemExt()\npublic boolean isOdm()\npublic boolean isSystem()\npublic boolean isRequestLegacyExternalStorage()\npublic boolean isUserDataFragile()\npublic android.content.pm.SigningDetails getSigningDetails()\npublic com.android.server.pm.PackageSetting setSigningDetails(android.content.pm.SigningDetails)\npublic void copyPackageSetting(com.android.server.pm.PackageSetting,boolean)\n @com.android.internal.annotations.VisibleForTesting com.android.server.pm.pkg.PackageUserStateImpl modifyUserState(int)\npublic com.android.server.pm.pkg.PackageUserStateImpl getOrCreateUserState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateInternal readUserState(int)\n void setEnabled(int,int,java.lang.String)\n int getEnabled(int)\n void setInstalled(boolean,int)\n boolean getInstalled(int)\n int getInstallReason(int)\n void setInstallReason(int,int)\n int getUninstallReason(int)\n void setUninstallReason(int,int)\n @android.annotation.NonNull android.content.pm.overlay.OverlayPaths getOverlayPaths(int)\n boolean setOverlayPathsForLibrary(java.lang.String,android.content.pm.overlay.OverlayPaths,int)\n boolean isInstalledOrHasDataOnAnyOtherUser(int[],int)\n int[] queryInstalledUsers(int[],boolean)\n int[] queryUsersInstalledOrHasData(int[])\n long getCeDataInode(int)\n long getDeDataInode(int)\n void setCeDataInode(long,int)\n void setDeDataInode(long,int)\n boolean getStopped(int)\n void setStopped(boolean,int)\npublic com.android.server.pm.PackageSetting setScannedAsStoppedSystemApp(boolean)\n boolean getNotLaunched(int)\n void setNotLaunched(boolean,int)\n boolean getHidden(int)\n void setHidden(boolean,int)\n int getDistractionFlags(int)\n void setDistractionFlags(int,int)\npublic boolean getInstantApp(int)\n void setInstantApp(boolean,int)\n boolean getVirtualPreload(int)\n void setVirtualPreload(boolean,int)\n void setUserState(int,long,long,int,boolean,boolean,boolean,boolean,int,android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>,boolean,boolean,java.lang.String,android.util.ArraySet<java.lang.String>,android.util.ArraySet<java.lang.String>,int,int,java.lang.String,java.lang.String,long,int,com.android.server.pm.pkg.ArchiveState)\n void setUserState(int,com.android.server.pm.pkg.PackageUserStateInternal)\n com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponents(int)\n com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponents(int)\n void setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setEnabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n void setDisabledComponentsCopy(com.android.server.utils.WatchedArraySet<java.lang.String>,int)\n com.android.server.pm.pkg.PackageUserStateImpl modifyUserStateComponents(int,boolean,boolean)\n void addDisabledComponent(java.lang.String,int)\n void addEnabledComponent(java.lang.String,int)\n boolean enableComponentLPw(java.lang.String,int)\n boolean disableComponentLPw(java.lang.String,int)\n boolean restoreComponentLPw(java.lang.String,int)\n int getCurrentEnabledStateLPr(java.lang.String,int)\n void removeUser(int)\npublic int[] getNotInstalledUserIds()\n void writePackageUserPermissionsProto(android.util.proto.ProtoOutputStream,long,java.util.List<android.content.pm.UserInfo>,com.android.server.pm.permission.LegacyPermissionDataProvider)\nprotected void writeUsersInfoToProto(android.util.proto.ProtoOutputStream,long)\nprivate static void writeArchiveState(android.util.proto.ProtoOutputStream,com.android.server.pm.pkg.ArchiveState)\npublic @com.android.internal.annotations.VisibleForTesting com.android.server.pm.PackageSetting setPath(java.io.File)\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideNonLocalizedLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer,int)\npublic void resetOverrideComponentLabelIcon(int)\npublic @android.annotation.Nullable java.lang.String getSplashScreenTheme(int)\npublic boolean isIncremental()\npublic boolean isLoading()\npublic com.android.server.pm.PackageSetting setLoadingProgress(float)\npublic com.android.server.pm.PackageSetting setLoadingCompletedTime(long)\npublic com.android.server.pm.PackageSetting setAppMetadataFilePath(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override long getVersionCode()\npublic @android.annotation.Nullable @java.lang.Override java.util.Map<java.lang.String,java.util.Set<java.lang.String>> getMimeGroups()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String getPackageName()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.pm.pkg.AndroidPackage getAndroidPackage()\npublic @android.annotation.NonNull android.content.pm.SigningInfo getSigningInfo()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesSdkLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesSdkLibrariesVersionsMajor()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getUsesStaticLibraries()\npublic @android.annotation.NonNull @java.lang.Override long[] getUsesStaticLibrariesVersions()\npublic @android.annotation.NonNull @java.lang.Override java.util.List<com.android.server.pm.pkg.SharedLibrary> getSharedLibraryDependencies()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryInfo(android.content.pm.SharedLibraryInfo)\npublic @android.annotation.NonNull @java.lang.Override java.util.List<java.lang.String> getUsesLibraryFiles()\npublic @android.annotation.NonNull com.android.server.pm.PackageSetting addUsesLibraryFile(java.lang.String)\npublic @java.lang.Override boolean isHiddenUntilInstalled()\npublic @android.annotation.NonNull @java.lang.Override long[] getLastPackageUsageTime()\npublic @java.lang.Override boolean isUpdatedSystemApp()\npublic @java.lang.Override boolean isApkInUpdatedApex()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getApexModuleName()\npublic com.android.server.pm.PackageSetting setDomainSetId(java.util.UUID)\npublic com.android.server.pm.PackageSetting setCategoryOverride(int)\npublic com.android.server.pm.PackageSetting setLegacyNativeLibraryPath(java.lang.String)\npublic com.android.server.pm.PackageSetting setMimeGroups(java.util.Map<java.lang.String,java.util.Set<java.lang.String>>)\npublic com.android.server.pm.PackageSetting setUsesSdkLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesSdkLibrariesVersionsMajor(long[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibraries(java.lang.String[])\npublic com.android.server.pm.PackageSetting setUsesStaticLibrariesVersions(long[])\npublic com.android.server.pm.PackageSetting setApexModuleName(java.lang.String)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageStateUnserialized getTransientState()\npublic @android.annotation.NonNull android.util.SparseArray<? extends PackageUserStateInternal> getUserStates()\npublic com.android.server.pm.PackageSetting addMimeTypes(java.lang.String,java.util.Set<java.lang.String>)\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserState getStateForUser(android.os.UserHandle)\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbi()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbi()\npublic @android.annotation.Nullable @java.lang.Override java.lang.String getSeInfo()\npublic @android.annotation.Nullable java.lang.String getPrimaryCpuAbiLegacy()\npublic @android.annotation.Nullable java.lang.String getSecondaryCpuAbiLegacy()\npublic @android.content.pm.ApplicationInfo.HiddenApiEnforcementPolicy @java.lang.Override int getHiddenApiEnforcementPolicy()\npublic @java.lang.Override boolean isApex()\npublic @java.lang.Override boolean isForceQueryableOverride()\npublic @java.lang.Override boolean isUpdateAvailable()\npublic @java.lang.Override boolean isInstallPermissionsFixed()\npublic @java.lang.Override boolean isDefaultToDeviceProtectedStorage()\npublic @java.lang.Override boolean isPersistent()\npublic @java.lang.Override boolean isScannedAsStoppedSystemApp()\nclass PackageSetting extends com.android.server.pm.SettingBase implements [com.android.server.pm.pkg.PackageStateInternal]\nprivate static final int INSTALL_PERMISSION_FIXED\nprivate static final int UPDATE_AVAILABLE\nprivate static final int FORCE_QUERYABLE_OVERRIDE\nprivate static final int SCANNED_AS_STOPPED_SYSTEM_APP\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genGetters=true, genConstructor=false, genSetters=false, genBuilder=false)")
@Deprecated
private void __metadata() {}
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index a8196f3..639d6d7 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -409,12 +409,17 @@
if (DEBUG_REMOVE) {
Slog.d(TAG, "Updating installed state to false because of DELETE_KEEP_DATA");
}
+ final boolean isArchive = (flags & PackageManager.DELETE_ARCHIVE) != 0;
+ final long currentTimeMillis = System.currentTimeMillis();
for (int userId : outInfo.mRemovedUsers) {
if (DEBUG_REMOVE) {
final boolean wasInstalled = deletedPs.getInstalled(userId);
Slog.d(TAG, " user " + userId + ": " + wasInstalled + " => " + false);
}
deletedPs.setInstalled(/* installed= */ false, userId);
+ if (isArchive) {
+ deletedPs.modifyUserState(userId).setArchiveTimeMillis(currentTimeMillis);
+ }
}
}
// make sure to preserve per-user installed state if this removal was just
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index fd70cb1..107dc76 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -373,6 +373,8 @@
private static final String ATTR_ARCHIVE_ICON_PATH = "icon-path";
private static final String ATTR_ARCHIVE_MONOCHROME_ICON_PATH = "monochrome-icon-path";
+ private static final String ATTR_ARCHIVE_TIME = "archive-time";
+
private final Handler mHandler;
private final PackageManagerTracedLock mLock;
@@ -948,6 +950,7 @@
ret.getPkgState().setUpdatedSystemApp(false);
ret.setTargetSdkVersion(p.getTargetSdkVersion());
ret.setRestrictUpdateHash(p.getRestrictUpdateHash());
+ ret.setScannedAsStoppedSystemApp(p.isScannedAsStoppedSystemApp());
}
mDisabledSysPackages.remove(name);
return ret;
@@ -1162,6 +1165,7 @@
Slog.i(PackageManagerService.TAG, "Stopping system package " + pkgName, e);
}
pkgSetting.setStopped(true, installUserId);
+ pkgSetting.setScannedAsStoppedSystemApp(true);
}
if (sharedUser != null) {
pkgSetting.setAppId(sharedUser.mAppId);
@@ -1929,6 +1933,8 @@
ATTR_SPLASH_SCREEN_THEME);
final long firstInstallTime = parser.getAttributeLongHex(null,
ATTR_FIRST_INSTALL_TIME, 0);
+ final long archiveTime = parser.getAttributeLongHex(null,
+ ATTR_ARCHIVE_TIME, 0);
final int minAspectRatio = parser.getAttributeInt(null,
ATTR_MIN_ASPECT_RATIO,
PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
@@ -2016,7 +2022,7 @@
firstInstallTime != 0 ? firstInstallTime
: origFirstInstallTimes.getOrDefault(name, 0L),
minAspectRatio, archiveState);
-
+ ps.setArchiveTimeMillis(archiveTime, userId);
mDomainVerificationManager.setLegacyUserState(name, userId, verifState);
} else if (tagName.equals("preferred-activities")) {
readPreferredActivitiesLPw(parser, userId);
@@ -2379,6 +2385,8 @@
}
serializer.attributeLongHex(null, ATTR_FIRST_INSTALL_TIME,
ustate.getFirstInstallTimeMillis());
+ serializer.attributeLongHex(null, ATTR_ARCHIVE_TIME,
+ ustate.getArchiveTimeMillis());
if (ustate.getUninstallReason()
!= PackageManager.UNINSTALL_REASON_UNKNOWN) {
serializer.attributeInt(null, ATTR_UNINSTALL_REASON,
@@ -3072,6 +3080,8 @@
serializer.attributeBytesBase64(null, "restrictUpdateHash",
pkg.getRestrictUpdateHash());
}
+ serializer.attributeBoolean(null, "scannedAsStoppedSystemApp",
+ pkg.isScannedAsStoppedSystemApp());
if (pkg.getLegacyNativeLibraryPath() != null) {
serializer.attribute(null, "nativeLibraryPath", pkg.getLegacyNativeLibraryPath());
}
@@ -3140,6 +3150,8 @@
serializer.attributeBytesBase64(null, "restrictUpdateHash",
pkg.getRestrictUpdateHash());
}
+ serializer.attributeBoolean(null, "scannedAsStoppedSystemApp",
+ pkg.isScannedAsStoppedSystemApp());
if (!pkg.hasSharedUser()) {
serializer.attributeInt(null, "userId", pkg.getAppId());
} else {
@@ -3873,6 +3885,8 @@
int targetSdkVersion = parser.getAttributeInt(null, "targetSdkVersion", 0);
byte[] restrictUpdateHash = parser.getAttributeBytesBase64(null, "restrictUpdateHash",
null);
+ boolean isScannedAsStoppedSystemApp = parser.getAttributeBoolean(null,
+ "scannedAsStoppedSystemApp", false);
int pkgFlags = 0;
int pkgPrivateFlags = 0;
@@ -3893,7 +3907,8 @@
.setCpuAbiOverride(cpuAbiOverrideStr)
.setLongVersionCode(versionCode)
.setTargetSdkVersion(targetSdkVersion)
- .setRestrictUpdateHash(restrictUpdateHash);
+ .setRestrictUpdateHash(restrictUpdateHash)
+ .setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp);
long timeStamp = parser.getAttributeLongHex(null, "ft", 0);
if (timeStamp == 0) {
timeStamp = parser.getAttributeLong(null, "ts", 0);
@@ -3988,6 +4003,7 @@
String appMetadataFilePath = null;
int targetSdkVersion = 0;
byte[] restrictUpdateHash = null;
+ boolean isScannedAsStoppedSystemApp = false;
try {
name = parser.getAttributeValue(null, ATTR_NAME);
realName = parser.getAttributeValue(null, "realName");
@@ -4028,6 +4044,8 @@
categoryHint = parser.getAttributeInt(null, "categoryHint",
ApplicationInfo.CATEGORY_UNDEFINED);
appMetadataFilePath = parser.getAttributeValue(null, "appMetadataFilePath");
+ isScannedAsStoppedSystemApp = parser.getAttributeBoolean(null,
+ "scannedAsStoppedSystemApp", false);
String domainSetIdString = parser.getAttributeValue(null, "domainSetId");
@@ -4174,7 +4192,8 @@
.setLoadingCompletedTime(loadingCompletedTime)
.setAppMetadataFilePath(appMetadataFilePath)
.setTargetSdkVersion(targetSdkVersion)
- .setRestrictUpdateHash(restrictUpdateHash);
+ .setRestrictUpdateHash(restrictUpdateHash)
+ .setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp);
// Handle legacy string here for single-user mode
final String enabledStr = parser.getAttributeValue(null, ATTR_ENABLED);
if (enabledStr != null) {
@@ -4992,6 +5011,8 @@
pw.append(prefix).append(" queriesIntents=")
.println(ps.getPkg().getQueriesIntents());
}
+ pw.print(prefix); pw.print(" scannedAsStoppedSystemApp=");
+ pw.println(ps.isScannedAsStoppedSystemApp());
pw.print(prefix); pw.print(" supportsScreens=[");
boolean first = true;
if (pkg.isSmallScreensSupported()) {
@@ -5272,6 +5293,10 @@
date.setTime(pus.getFirstInstallTimeMillis());
pw.println(sdf.format(date));
+ pw.print(" archiveTime=");
+ date.setTime(pus.getArchiveTimeMillis());
+ pw.println(sdf.format(date));
+
pw.print(" uninstallReason=");
pw.println(userState.getUninstallReason());
diff --git a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
index 91a70a6..926e018 100644
--- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
+++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java
@@ -152,6 +152,7 @@
info.compileSdkVersionCodename = pkg.getCompileSdkVersionCodeName();
info.firstInstallTime = firstInstallTime;
info.lastUpdateTime = lastUpdateTime;
+ info.setArchiveTimeMillis(state.getArchiveTimeMillis());
if ((flags & PackageManager.GET_GIDS) != 0) {
info.gids = gids;
}
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index 883b066..8bd2d94 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -70,6 +70,7 @@
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.admin.DevicePolicyManagerInternal;
+import android.companion.virtual.VirtualDeviceManager;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.Context;
@@ -5327,7 +5328,8 @@
IOnPermissionsChangeListener callback = mPermissionListeners
.getBroadcastItem(i);
try {
- callback.onPermissionsChanged(uid);
+ callback.onPermissionsChanged(uid,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT);
} catch (RemoteException e) {
Log.e(TAG, "Permission listener is dead", e);
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index e7137bb..10b59c7 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -456,4 +456,12 @@
@Immutable.Ignore
@Nullable
byte[] getRestrictUpdateHash();
+
+ /**
+ * whether the package has been scanned as a stopped system app. A package will be
+ * scanned in the stopped state if it is a system app that has a launcher entry and is
+ * <b>not</b> exempted by {@code <initial-package-state>} tag, and is not an APEX
+ * @hide
+ */
+ boolean isScannedAsStoppedSystemApp();
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserState.java b/services/core/java/com/android/server/pm/pkg/PackageUserState.java
index 2a81a86..8eb3466 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserState.java
@@ -256,4 +256,10 @@
* @hide
*/
boolean dataExists();
+
+ /**
+ * Timestamp of when the app is archived on the user.
+ * @hide
+ */
+ long getArchiveTimeMillis();
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
index 2f4ad2d8..defd343 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateDefault.java
@@ -206,4 +206,9 @@
public boolean dataExists() {
return true;
}
+
+ @Override
+ public long getArchiveTimeMillis() {
+ return 0;
+ }
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
index a76a7ce0..c0ea7cc 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
@@ -135,6 +135,8 @@
@Nullable
private ArchiveState mArchiveState;
+ private @CurrentTimeMillisLong long mArchiveTimeMillis;
+
@NonNull
final SnapshotCache<PackageUserStateImpl> mSnapshot;
@@ -187,6 +189,7 @@
? null : other.mComponentLabelIconOverrideMap.snapshot();
mFirstInstallTimeMillis = other.mFirstInstallTimeMillis;
mArchiveState = other.mArchiveState;
+ mArchiveTimeMillis = other.mArchiveTimeMillis;
mSnapshot = new SnapshotCache.Sealed<>();
}
@@ -602,8 +605,6 @@
/**
* Sets the value for {@link #getArchiveState()}.
- *
- * @hide
*/
@NonNull
public PackageUserStateImpl setArchiveState(@NonNull ArchiveState archiveState) {
@@ -612,6 +613,16 @@
return this;
}
+ /**
+ * Sets the timestamp when the app is archived on this user.
+ */
+ @NonNull
+ public PackageUserStateImpl setArchiveTimeMillis(@CurrentTimeMillisLong long value) {
+ mArchiveTimeMillis = value;
+ onChanged();
+ return this;
+ }
+
@NonNull
@Override
public Map<String, OverlayPaths> getSharedLibraryOverlayPaths() {
@@ -800,6 +811,11 @@
}
@DataClass.Generated.Member
+ public @CurrentTimeMillisLong long getArchiveTimeMillis() {
+ return mArchiveTimeMillis;
+ }
+
+ @DataClass.Generated.Member
public @NonNull SnapshotCache<PackageUserStateImpl> getSnapshot() {
return mSnapshot;
}
@@ -876,6 +892,7 @@
&& mFirstInstallTimeMillis == that.mFirstInstallTimeMillis
&& watchableEquals(that.mWatchable)
&& Objects.equals(mArchiveState, that.mArchiveState)
+ && mArchiveTimeMillis == that.mArchiveTimeMillis
&& snapshotEquals(that.mSnapshot);
}
@@ -906,15 +923,16 @@
_hash = 31 * _hash + Long.hashCode(mFirstInstallTimeMillis);
_hash = 31 * _hash + watchableHashCode();
_hash = 31 * _hash + Objects.hashCode(mArchiveState);
+ _hash = 31 * _hash + Long.hashCode(mArchiveTimeMillis);
_hash = 31 * _hash + snapshotHashCode();
return _hash;
}
@DataClass.Generated(
- time = 1694196888631L,
+ time = 1699917927942L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java",
- inputSignatures = "private int mBooleans\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate long mCeDataInode\nprivate long mDeDataInode\nprivate int mDistractionFlags\nprivate @android.content.pm.PackageManager.EnabledState int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.content.pm.PackageManager.UserMinAspectRatio int mMinAspectRatio\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate @android.annotation.CurrentTimeMillisLong long mFirstInstallTimeMillis\nprivate @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nprivate @android.annotation.Nullable com.android.server.pm.pkg.ArchiveState mArchiveState\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\nprivate com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(java.lang.String,com.android.server.pm.pkg.SuspendParams)\npublic com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setMinAspectRatio(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTimeMillis(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setArchiveState(com.android.server.pm.pkg.ArchiveState)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setWatchable(com.android.server.utils.Watchable)\nprivate boolean watchableEquals(com.android.server.utils.Watchable)\nprivate int watchableHashCode()\nprivate boolean snapshotEquals(com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl>)\nprivate int snapshotHashCode()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isQuarantined()\npublic @java.lang.Override boolean dataExists()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\nprivate static final int INSTALLED\nprivate static final int STOPPED\nprivate static final int NOT_LAUNCHED\nprivate static final int HIDDEN\nprivate static final int INSTANT_APP\nprivate static final int VIRTUAL_PRELOADED\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
+ inputSignatures = "private int mBooleans\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mDisabledComponentsWatched\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArraySet<java.lang.String> mEnabledComponentsWatched\nprivate long mCeDataInode\nprivate long mDeDataInode\nprivate int mDistractionFlags\nprivate @android.content.pm.PackageManager.EnabledState int mEnabledState\nprivate @android.content.pm.PackageManager.InstallReason int mInstallReason\nprivate @android.content.pm.PackageManager.UninstallReason int mUninstallReason\nprivate @android.annotation.Nullable java.lang.String mHarmfulAppWarning\nprivate @android.annotation.Nullable java.lang.String mLastDisableAppCaller\nprivate @android.annotation.Nullable android.content.pm.overlay.OverlayPaths mOverlayPaths\nprotected @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths> mSharedLibraryOverlayPaths\nprivate @android.annotation.Nullable java.lang.String mSplashScreenTheme\nprivate @android.content.pm.PackageManager.UserMinAspectRatio int mMinAspectRatio\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams> mSuspendParams\nprivate @android.annotation.Nullable com.android.server.utils.WatchedArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>> mComponentLabelIconOverrideMap\nprivate @android.annotation.CurrentTimeMillisLong long mFirstInstallTimeMillis\nprivate @android.annotation.Nullable com.android.server.utils.Watchable mWatchable\nprivate @android.annotation.Nullable com.android.server.pm.pkg.ArchiveState mArchiveState\nprivate @android.annotation.CurrentTimeMillisLong long mArchiveTimeMillis\nfinal @android.annotation.NonNull com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> mSnapshot\nprivate void setBoolean(int,boolean)\nprivate boolean getBoolean(int)\nprivate com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl> makeCache()\nprivate void onChanged()\npublic @android.annotation.NonNull @java.lang.Override com.android.server.pm.pkg.PackageUserStateImpl snapshot()\npublic @android.annotation.Nullable boolean setOverlayPaths(android.content.pm.overlay.OverlayPaths)\npublic boolean setSharedLibraryOverlayPaths(java.lang.String,android.content.pm.overlay.OverlayPaths)\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getDisabledComponentsNoCopy()\npublic @android.annotation.Nullable @java.lang.Override com.android.server.utils.WatchedArraySet<java.lang.String> getEnabledComponentsNoCopy()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getDisabledComponents()\npublic @android.annotation.NonNull @java.lang.Override android.util.ArraySet<java.lang.String> getEnabledComponents()\npublic @java.lang.Override boolean isComponentEnabled(java.lang.String)\npublic @java.lang.Override boolean isComponentDisabled(java.lang.String)\npublic @java.lang.Override android.content.pm.overlay.OverlayPaths getAllOverlayPaths()\npublic @com.android.internal.annotations.VisibleForTesting boolean overrideLabelAndIcon(android.content.ComponentName,java.lang.String,java.lang.Integer)\npublic void resetOverrideComponentLabelIcon()\npublic @android.annotation.Nullable android.util.Pair<java.lang.String,java.lang.Integer> getOverrideLabelIconForComponent(android.content.ComponentName)\npublic @java.lang.Override boolean isSuspended()\npublic com.android.server.pm.pkg.PackageUserStateImpl putSuspendParams(java.lang.String,com.android.server.pm.pkg.SuspendParams)\npublic com.android.server.pm.pkg.PackageUserStateImpl removeSuspension(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(android.util.ArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDisabledComponents(com.android.server.utils.WatchedArraySet<java.lang.String>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setCeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDeDataInode(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstalled(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setStopped(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setNotLaunched(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHidden(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setDistractionFlags(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstantApp(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setVirtualPreload(boolean)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setEnabledState(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setInstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setUninstallReason(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setHarmfulAppWarning(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setLastDisableAppCaller(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSharedLibraryOverlayPaths(android.util.ArrayMap<java.lang.String,android.content.pm.overlay.OverlayPaths>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSplashScreenTheme(java.lang.String)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setMinAspectRatio(int)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setSuspendParams(android.util.ArrayMap<java.lang.String,com.android.server.pm.pkg.SuspendParams>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setComponentLabelIconOverrideMap(android.util.ArrayMap<android.content.ComponentName,android.util.Pair<java.lang.String,java.lang.Integer>>)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setFirstInstallTimeMillis(long)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setArchiveState(com.android.server.pm.pkg.ArchiveState)\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setArchiveTimeMillis(long)\npublic @android.annotation.NonNull @java.lang.Override java.util.Map<java.lang.String,android.content.pm.overlay.OverlayPaths> getSharedLibraryOverlayPaths()\npublic @android.annotation.NonNull com.android.server.pm.pkg.PackageUserStateImpl setWatchable(com.android.server.utils.Watchable)\nprivate boolean watchableEquals(com.android.server.utils.Watchable)\nprivate int watchableHashCode()\nprivate boolean snapshotEquals(com.android.server.utils.SnapshotCache<com.android.server.pm.pkg.PackageUserStateImpl>)\nprivate int snapshotHashCode()\npublic @java.lang.Override boolean isInstalled()\npublic @java.lang.Override boolean isStopped()\npublic @java.lang.Override boolean isNotLaunched()\npublic @java.lang.Override boolean isHidden()\npublic @java.lang.Override boolean isInstantApp()\npublic @java.lang.Override boolean isVirtualPreload()\npublic @java.lang.Override boolean isQuarantined()\npublic @java.lang.Override boolean dataExists()\nclass PackageUserStateImpl extends com.android.server.utils.WatchableImpl implements [com.android.server.pm.pkg.PackageUserStateInternal, com.android.server.utils.Snappable]\nprivate static final int INSTALLED\nprivate static final int STOPPED\nprivate static final int NOT_LAUNCHED\nprivate static final int HIDDEN\nprivate static final int INSTANT_APP\nprivate static final int VIRTUAL_PRELOADED\nclass Booleans extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genBuilder=false, genEqualsHashCode=true)")
@Deprecated
private void __metadata() {}
diff --git a/services/core/java/com/android/server/pm/resolution/ComponentResolver.java b/services/core/java/com/android/server/pm/resolution/ComponentResolver.java
index ed6d3b9..532a7f8 100644
--- a/services/core/java/com/android/server/pm/resolution/ComponentResolver.java
+++ b/services/core/java/com/android/server/pm/resolution/ComponentResolver.java
@@ -943,6 +943,12 @@
return false;
}
+ if (packageState.isSystem()) {
+ // A system app can be considered in the stopped state only if it was originally
+ // scanned in the stopped state.
+ return packageState.isScannedAsStoppedSystemApp() &&
+ packageState.getUserStateOrDefault(userId).isStopped();
+ }
return packageState.getUserStateOrDefault(userId).isStopped();
}
}
diff --git a/services/core/java/com/android/server/power/OWNERS b/services/core/java/com/android/server/power/OWNERS
index 1970ee4..94340ec 100644
--- a/services/core/java/com/android/server/power/OWNERS
+++ b/services/core/java/com/android/server/power/OWNERS
@@ -2,6 +2,6 @@
santoscordon@google.com
philipjunker@google.com
-per-file ThermalManagerService.java=wvw@google.com
+per-file ThermalManagerService.java=file:/THERMAL_OWNERS
per-file LowPowerStandbyController.java=qingxun@google.com
-per-file LowPowerStandbyControllerInternal.java=qingxun@google.com
\ No newline at end of file
+per-file LowPowerStandbyControllerInternal.java=qingxun@google.com
diff --git a/services/core/java/com/android/server/timedetector/TEST_MAPPING b/services/core/java/com/android/server/timedetector/TEST_MAPPING
index 5c37680..17d327e 100644
--- a/services/core/java/com/android/server/timedetector/TEST_MAPPING
+++ b/services/core/java/com/android/server/timedetector/TEST_MAPPING
@@ -7,10 +7,7 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
- }
- ],
- // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
- "postsubmit": [
+ },
{
"name": "FrameworksTimeServicesTests"
}
diff --git a/services/core/java/com/android/server/timezonedetector/TEST_MAPPING b/services/core/java/com/android/server/timezonedetector/TEST_MAPPING
index 63dd7b4..358618a 100644
--- a/services/core/java/com/android/server/timezonedetector/TEST_MAPPING
+++ b/services/core/java/com/android/server/timezonedetector/TEST_MAPPING
@@ -7,15 +7,15 @@
"exclude-annotation": "androidx.test.filters.FlakyTest"
}
]
+ },
+ {
+ "name": "FrameworksTimeServicesTests"
}
],
// TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
"postsubmit": [
{
"name": "CtsLocationTimeZoneManagerHostTest"
- },
- {
- "name": "FrameworksTimeServicesTests"
}
]
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5ecbc2b..081759d 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -115,7 +115,6 @@
import static android.view.Display.INVALID_DISPLAY;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
-import static android.view.SurfaceControl.getGlobalTransaction;
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -5699,14 +5698,10 @@
// can be synchronized with showing the next surface in the transition.
if (!usingShellTransitions && !isVisible() && !delayed
&& !displayContent.mAppTransition.isTransitionSet()) {
- SurfaceControl.openTransaction();
- try {
- forAllWindows(win -> {
- win.mWinAnimator.hide(getGlobalTransaction(), "immediately hidden");
- }, true);
- } finally {
- SurfaceControl.closeTransaction();
- }
+ forAllWindows(win -> {
+ win.mWinAnimator.hide(getPendingTransaction(), "immediately hidden");
+ }, true);
+ scheduleAnimation();
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 3ec58f0..a9f11e4 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -125,6 +125,7 @@
import static com.android.server.wm.Task.REPARENT_KEEP_ROOT_TASK_AT_FRONT;
import static com.android.server.wm.WindowManagerService.MY_PID;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
+import static com.android.sdksandbox.flags.Flags.sandboxActivitySdkBasedContext;
import android.Manifest;
import android.annotation.IntDef;
@@ -165,6 +166,7 @@
import android.app.assist.AssistContent;
import android.app.assist.AssistStructure;
import android.app.compat.CompatChanges;
+import android.app.sdksandbox.sandboxactivity.SdkSandboxActivityAuthority;
import android.app.usage.UsageStatsManagerInternal;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
@@ -1260,6 +1262,13 @@
true /*validateIncomingUser*/);
}
+ static boolean isSdkSandboxActivity(Context context, Intent intent) {
+ return intent != null
+ && (sandboxActivitySdkBasedContext()
+ ? SdkSandboxActivityAuthority.isSdkSandboxActivity(context, intent)
+ : intent.isSandboxActivity(context));
+ }
+
private int startActivityAsUser(IApplicationThread caller, String callingPackage,
@Nullable String callingFeatureId, Intent intent, String resolvedType,
IBinder resultTo, String resultWho, int requestCode, int startFlags,
@@ -1270,7 +1279,7 @@
assertPackageMatchesCallingUid(callingPackage);
enforceNotIsolatedCaller("startActivityAsUser");
- if (intent != null && intent.isSandboxActivity(mContext)) {
+ if (isSdkSandboxActivity(mContext, intent)) {
SdkSandboxManagerLocal sdkSandboxManagerLocal = LocalManagerRegistry.getManager(
SdkSandboxManagerLocal.class);
sdkSandboxManagerLocal.enforceAllowedToHostSandboxedActivity(
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index ce8d051..90eeed2 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -1084,7 +1084,7 @@
// Remove the process record so it won't be considered as alive.
mService.mProcessNames.remove(wpc.mName, wpc.mUid);
mService.mProcessMap.remove(wpc.getPid());
- } else if (r.intent.isSandboxActivity(mService.mContext)) {
+ } else if (ActivityTaskManagerService.isSdkSandboxActivity(mService.mContext, r.intent)) {
Slog.e(TAG, "Abort sandbox activity launching as no sandbox process to host it.");
r.finishIfPossible("No sandbox process for the activity", false /* oomAdj */);
r.launchFailed = true;
@@ -1628,7 +1628,12 @@
}
}
- private void removeRootTaskInSurfaceTransaction(Task rootTask) {
+ /**
+ * Removes the root task associated with the given {@param rootTask}. If the {@param rootTask}
+ * is the pinned task, then its child tasks are not explicitly removed when the root task is
+ * destroyed, but instead moved back onto the TaskDisplayArea.
+ */
+ void removeRootTask(Task rootTask) {
if (rootTask.getWindowingMode() == WINDOWING_MODE_PINNED) {
removePinnedRootTaskInSurfaceTransaction(rootTask);
} else {
@@ -1639,15 +1644,6 @@
}
/**
- * Removes the root task associated with the given {@param task}. If the {@param task} is the
- * pinned task, then its child tasks are not explicitly removed when the root task is
- * destroyed, but instead moved back onto the TaskDisplayArea.
- */
- void removeRootTask(Task task) {
- mWindowManager.inSurfaceTransaction(() -> removeRootTaskInSurfaceTransaction(task));
- }
-
- /**
* Removes the task with the specified task id.
*
* @param taskId Identifier of the task to be removed.
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 805bff2..05087f8 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -71,7 +71,6 @@
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
import static com.android.server.wm.WallpaperAnimationAdapter.shouldStartWallpaperAnimation;
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
-import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
@@ -82,7 +81,6 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
-import android.util.Slog;
import android.view.Display;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationDefinition;
@@ -1179,16 +1177,7 @@
}
app.updateReportedVisibilityLocked();
app.waitingToShow = false;
- if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
- ">>> OPEN TRANSACTION handleAppTransitionReady()");
- mService.openSurfaceTransaction();
- try {
- app.showAllWindowsLocked();
- } finally {
- mService.closeSurfaceTransaction("handleAppTransitionReady");
- if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
- "<<< CLOSE TRANSACTION handleAppTransitionReady()");
- }
+ app.showAllWindowsLocked();
if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailUp()) {
app.attachThumbnailAnimation();
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index a3e1c8c..b66c4c3 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -545,18 +545,15 @@
BalVerdict resultForCaller = checkBackgroundActivityStartAllowedByCaller(state);
if (!state.hasRealCaller()) {
+ BalVerdict resultForRealCaller = null; // nothing to compute
if (resultForCaller.allows()) {
if (DEBUG_ACTIVITY_STARTS) {
Slog.d(TAG, "Background activity start allowed. "
- + state.dump(resultForCaller));
+ + state.dump(resultForCaller, resultForRealCaller));
}
return statsLog(resultForCaller, state);
}
- // anything that has fallen through would currently be aborted
- Slog.w(TAG, "Background activity launch blocked! "
- + state.dump(resultForCaller));
- showBalBlockedToast("BAL blocked", state);
- return statsLog(BalVerdict.BLOCK, state);
+ return abortLaunch(state, resultForCaller, resultForRealCaller);
}
// The realCaller result is only calculated for PendingIntents (indicated by a valid
@@ -588,11 +585,13 @@
}
return statsLog(resultForRealCaller, state);
}
- if (resultForCaller.allows() && resultForRealCaller.allows()
+ boolean callerCanAllow = resultForCaller.allows()
&& checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED
+ == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+ boolean realCallerCanAllow = resultForRealCaller.allows()
&& checkedOptions.getPendingIntentBackgroundActivityStartMode()
- == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+ == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
+ if (callerCanAllow && realCallerCanAllow) {
// Both caller and real caller allow with system defined behavior
if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) {
Slog.wtf(TAG,
@@ -608,10 +607,9 @@
"Without Android 15 BAL hardening this activity start would be allowed"
+ " (missing opt in by PI creator or sender)! "
+ state.dump(resultForCaller, resultForRealCaller));
- // fall through to abort
- } else if (resultForCaller.allows()
- && checkedOptions.getPendingIntentCreatorBackgroundActivityStartMode()
- == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+ return abortLaunch(state, resultForCaller, resultForRealCaller);
+ }
+ if (callerCanAllow) {
// Allowed before V by creator
if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) {
Slog.wtf(TAG,
@@ -626,10 +624,9 @@
"Without Android 15 BAL hardening this activity start would be allowed"
+ " (missing opt in by PI creator)! "
+ state.dump(resultForCaller, resultForRealCaller));
- // fall through to abort
- } else if (resultForRealCaller.allows()
- && checkedOptions.getPendingIntentBackgroundActivityStartMode()
- == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
+ return abortLaunch(state, resultForCaller, resultForRealCaller);
+ }
+ if (realCallerCanAllow) {
// Allowed before U by sender
if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) {
Slog.wtf(TAG,
@@ -643,9 +640,14 @@
Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed"
+ " (missing opt in by PI sender)! "
+ state.dump(resultForCaller, resultForRealCaller));
- // fall through to abort
+ return abortLaunch(state, resultForCaller, resultForRealCaller);
}
- // anything that has fallen through would currently be aborted
+ // neither the caller not the realCaller can allow or have explicitly opted out
+ return abortLaunch(state, resultForCaller, resultForRealCaller);
+ }
+
+ private BalVerdict abortLaunch(BalState state, BalVerdict resultForCaller,
+ BalVerdict resultForRealCaller) {
Slog.w(TAG, "Background activity launch blocked! "
+ state.dump(resultForCaller, resultForRealCaller));
showBalBlockedToast("BAL blocked", state);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4924810..2c224e4 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -1004,6 +1004,24 @@
mTmpApplySurfaceChangesTransactionState.obscured;
final RootWindowContainer root = mWmService.mRoot;
+ if (w.mHasSurface) {
+ // Take care of the window being ready to display.
+ final boolean committed = w.mWinAnimator.commitFinishDrawingLocked();
+ if (isDefaultDisplay && committed) {
+ if (w.hasWallpaper()) {
+ ProtoLog.v(WM_DEBUG_WALLPAPER,
+ "First draw done in potential wallpaper target %s", w);
+ mWallpaperMayChange = true;
+ pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
+ if (DEBUG_LAYOUT_REPEATS) {
+ surfacePlacer.debugLayoutRepeats(
+ "wallpaper and commitFinishDrawingLocked true",
+ pendingLayoutChanges);
+ }
+ }
+ }
+ }
+
// Update effect.
w.mObscured = mTmpApplySurfaceChangesTransactionState.obscured;
@@ -1090,30 +1108,9 @@
w.handleWindowMovedIfNeeded();
- final WindowStateAnimator winAnimator = w.mWinAnimator;
-
//Slog.i(TAG, "Window " + this + " clearing mContentChanged - done placing");
w.resetContentChanged();
- // Moved from updateWindowsAndWallpaperLocked().
- if (w.mHasSurface) {
- // Take care of the window being ready to display.
- final boolean committed = winAnimator.commitFinishDrawingLocked();
- if (isDefaultDisplay && committed) {
- if (w.hasWallpaper()) {
- ProtoLog.v(WM_DEBUG_WALLPAPER,
- "First draw done in potential wallpaper target %s", w);
- mWallpaperMayChange = true;
- pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
- if (DEBUG_LAYOUT_REPEATS) {
- surfacePlacer.debugLayoutRepeats(
- "wallpaper and commitFinishDrawingLocked true",
- pendingLayoutChanges);
- }
- }
- }
- }
-
final ActivityRecord activity = w.mActivityRecord;
if (activity != null && activity.isVisibleRequested()) {
activity.updateLetterboxSurface(w);
@@ -5587,12 +5584,7 @@
void prepareSurfaces() {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "prepareSurfaces");
try {
- final Transaction transaction = getPendingTransaction();
super.prepareSurfaces();
-
- // TODO: Once we totally eliminate global transaction we will pass transaction in here
- // rather than merging to global.
- SurfaceControl.mergeToGlobalTransaction(transaction);
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
diff --git a/services/core/java/com/android/server/wm/RecentsAnimation.java b/services/core/java/com/android/server/wm/RecentsAnimation.java
index b738c1c..5269d35 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimation.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimation.java
@@ -307,7 +307,7 @@
mService.stopAppSwitches();
}
- mWindowManager.inSurfaceTransaction(() -> {
+ inSurfaceTransaction(() -> {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
"RecentsAnimation#onAnimationFinished_inSurfaceTransaction");
mService.deferWindowLayout();
@@ -419,6 +419,11 @@
}
}
+ // No-op wrapper to keep legacy code.
+ private static void inSurfaceTransaction(Runnable exec) {
+ exec.run();
+ }
+
/** Gives the owner of recents animation higher priority. */
private void setProcessAnimating(boolean animating) {
if (mCaller == null) return;
diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java
index eb639b6..a98b9f7 100644
--- a/services/core/java/com/android/server/wm/RemoteAnimationController.java
+++ b/services/core/java/com/android/server/wm/RemoteAnimationController.java
@@ -307,7 +307,6 @@
mIsFinishing = true;
unlinkToDeathOfRunner();
releaseFinishedCallback();
- mService.openSurfaceTransaction();
try {
ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS,
"onAnimationFinished(): Notify animation finished:");
@@ -348,7 +347,6 @@
Slog.e(TAG, "Failed to finish remote animation", e);
throw e;
} finally {
- mService.closeSurfaceTransaction("RemoteAnimationController#finished");
mIsFinishing = false;
}
// Reset input for all activities when the remote animation is finished.
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index fe2c250..0c235ba 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -75,7 +75,6 @@
import static com.android.server.wm.TaskFragment.TASK_FRAGMENT_VISIBILITY_INVISIBLE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_TRACE;
-import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.H.WINDOW_FREEZE_TIMEOUT;
@@ -788,23 +787,13 @@
final DisplayContent defaultDisplay = mWmService.getDefaultDisplayContentLocked();
final WindowSurfacePlacer surfacePlacer = mWmService.mWindowPlacerLocked;
- if (SHOW_LIGHT_TRANSACTIONS) {
- Slog.i(TAG,
- ">>> OPEN TRANSACTION performLayoutAndPlaceSurfaces");
- }
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "applySurfaceChanges");
- mWmService.openSurfaceTransaction();
try {
applySurfaceChangesTransaction();
} catch (RuntimeException e) {
Slog.wtf(TAG, "Unhandled exception in Window Manager", e);
} finally {
- mWmService.closeSurfaceTransaction("performLayoutAndPlaceSurfaces");
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- if (SHOW_LIGHT_TRANSACTIONS) {
- Slog.i(TAG,
- "<<< CLOSE TRANSACTION performLayoutAndPlaceSurfaces");
- }
}
// Send any pending task-info changes that were queued-up during a layout deferment
@@ -998,9 +987,6 @@
// Give the display manager a chance to adjust properties like display rotation if it needs
// to.
mWmService.mDisplayManagerInternal.performTraversal(t);
- if (t != defaultDc.mSyncTransaction) {
- SurfaceControl.mergeToGlobalTransaction(t);
- }
}
/**
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 33f61fe..7375512 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -4545,12 +4545,6 @@
* @param creating {@code true} if this is being run during task construction.
*/
void setWindowingMode(int preferredWindowingMode, boolean creating) {
- mWmService.inSurfaceTransaction(() -> setWindowingModeInSurfaceTransaction(
- preferredWindowingMode, creating));
- }
-
- private void setWindowingModeInSurfaceTransaction(int preferredWindowingMode,
- boolean creating) {
final TaskDisplayArea taskDisplayArea = getDisplayArea();
if (taskDisplayArea == null) {
Slog.d(TAG, "taskDisplayArea is null, bail early");
@@ -5976,18 +5970,16 @@
"Can't exit pinned mode if it's not pinned already.");
}
- mWmService.inSurfaceTransaction(() -> {
- final Task task = getBottomMostTask();
- setWindowingMode(WINDOWING_MODE_UNDEFINED);
+ final Task task = getBottomMostTask();
+ setWindowingMode(WINDOWING_MODE_UNDEFINED);
- // Task could have been removed from the hierarchy due to windowing mode change
- // where its only child is reparented back to their original parent task.
- if (isAttached()) {
- getDisplayArea().positionChildAt(POSITION_TOP, this, false /* includingParents */);
- }
+ // Task could have been removed from the hierarchy due to windowing mode change
+ // where its only child is reparented back to their original parent task.
+ if (isAttached()) {
+ getDisplayArea().positionChildAt(POSITION_TOP, this, false /* includingParents */);
+ }
- mTaskSupervisor.scheduleUpdatePictureInPictureModeIfNeeded(task, this);
- });
+ mTaskSupervisor.scheduleUpdatePictureInPictureModeIfNeeded(task, this);
}
private int setBounds(Rect existing, Rect bounds) {
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index e95d265..fd22f15 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -128,7 +128,6 @@
}
ProtoLog.i(WM_SHOW_TRANSACTIONS, ">>> OPEN TRANSACTION animate");
- mService.openSurfaceTransaction();
try {
// Remove all deferred displays, tasks, and activities.
root.handleCompleteDeferredRemoval();
@@ -163,6 +162,7 @@
dc.mLastContainsRunningSurfaceAnimator = false;
dc.enableHighFrameRate(false);
}
+ mTransaction.merge(dc.getPendingTransaction());
}
cancelAnimation();
@@ -196,8 +196,8 @@
updateRunningExpensiveAnimationsLegacy();
}
- SurfaceControl.mergeToGlobalTransaction(mTransaction);
- mService.closeSurfaceTransaction("WindowAnimator");
+ mTransaction.apply();
+ mService.mWindowTracing.logState("WindowAnimator");
ProtoLog.i(WM_SHOW_TRANSACTIONS, "<<< CLOSE TRANSACTION animate");
mService.mAtmService.mTaskOrganizerController.dispatchPendingEvents();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 08acd24..a69a07f 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -1048,29 +1048,6 @@
SystemPerformanceHinter mSystemPerformanceHinter;
- void openSurfaceTransaction() {
- try {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "openSurfaceTransaction");
- SurfaceControl.openTransaction();
- } finally {
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- }
- }
-
- /**
- * Closes a surface transaction.
- * @param where debug string indicating where the transaction originated
- */
- void closeSurfaceTransaction(String where) {
- try {
- Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "closeSurfaceTransaction");
- SurfaceControl.closeTransaction();
- mWindowTracing.logState(where);
- } finally {
- Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
- }
- }
-
/** Listener to notify activity manager about app transitions. */
final WindowManagerInternal.AppTransitionListener mActivityManagerAppTransitionNotifier
= new WindowManagerInternal.AppTransitionListener() {
@@ -8591,39 +8568,6 @@
mAppFreezeListeners.remove(listener);
}
- /**
- * WARNING: This interrupts surface updates, be careful! Don't
- * execute within the transaction for longer than you would
- * execute on an animation thread.
- * WARNING: This method contains locks known to the State of California
- * to cause Deadlocks and other conditions.
- *
- * Begins a surface transaction with which the AM can batch operations.
- * All Surface updates performed by the WindowManager following this
- * will not appear on screen until after the call to
- * closeSurfaceTransaction.
- *
- * ActivityManager can use this to ensure multiple 'commands' will all
- * be reflected in a single frame. For example when reparenting a window
- * which was previously hidden due to it's parent properties, we may
- * need to ensure it is hidden in the same frame that the properties
- * from the new parent are inherited, otherwise it could be revealed
- * mistakenly.
- *
- * TODO(b/36393204): We can investigate totally replacing #deferSurfaceLayout
- * with something like this but it seems that some existing cases of
- * deferSurfaceLayout may be a little too broad, in particular the total
- * enclosure of startActivityUnchecked which could run for quite some time.
- */
- void inSurfaceTransaction(Runnable exec) {
- SurfaceControl.openTransaction();
- try {
- exec.run();
- } finally {
- SurfaceControl.closeTransaction();
- }
- }
-
/** Called to inform window manager if non-Vr UI shoul be disabled or not. */
public void disableNonVrUi(boolean disable) {
synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 5293292..3e43908 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -29,7 +29,6 @@
import static android.os.PowerManager.DRAW_WAKE_LOCK;
import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.view.SurfaceControl.Transaction;
-import static android.view.SurfaceControl.getGlobalTransaction;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;
import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION;
@@ -2794,7 +2793,7 @@
clearPolicyVisibilityFlag(LEGACY_POLICY_VISIBILITY);
}
if (!isVisibleByPolicy()) {
- mWinAnimator.hide(getGlobalTransaction(), "checkPolicyVisibilityChange");
+ mWinAnimator.hide(getPendingTransaction(), "checkPolicyVisibilityChange");
if (isFocused()) {
ProtoLog.i(WM_DEBUG_FOCUS_LIGHT,
"setAnimationLocked: setting mFocusMayChange true");
diff --git a/services/core/java/com/android/server/wm/WindowSurfaceController.java b/services/core/java/com/android/server/wm/WindowSurfaceController.java
index 6c15c22..d348491 100644
--- a/services/core/java/com/android/server/wm/WindowSurfaceController.java
+++ b/services/core/java/com/android/server/wm/WindowSurfaceController.java
@@ -20,7 +20,6 @@
import static android.view.SurfaceControl.METADATA_OWNER_PID;
import static android.view.SurfaceControl.METADATA_OWNER_UID;
import static android.view.SurfaceControl.METADATA_WINDOW_TYPE;
-import static android.view.SurfaceControl.getGlobalTransaction;
import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_SURFACE_ALLOC;
import static com.android.internal.protolog.ProtoLogGroup.WM_SHOW_TRANSACTIONS;
@@ -148,14 +147,9 @@
if (mSurfaceControl == null) {
return;
}
- if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG, ">>> OPEN TRANSACTION setOpaqueLocked");
- mService.openSurfaceTransaction();
- try {
- getGlobalTransaction().setOpaque(mSurfaceControl, isOpaque);
- } finally {
- mService.closeSurfaceTransaction("setOpaqueLocked");
- if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG, "<<< CLOSE TRANSACTION setOpaqueLocked");
- }
+
+ mAnimator.mWin.getPendingTransaction().setOpaque(mSurfaceControl, isOpaque);
+ mService.scheduleAnimationLocked();
}
void setSecure(boolean isSecure) {
@@ -165,18 +159,15 @@
return;
}
if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG, ">>> OPEN TRANSACTION setSecureLocked");
- mService.openSurfaceTransaction();
- try {
- getGlobalTransaction().setSecure(mSurfaceControl, isSecure);
- final DisplayContent dc = mAnimator.mWin.mDisplayContent;
- if (dc != null) {
- dc.refreshImeSecureFlag(getGlobalTransaction());
- }
- } finally {
- mService.closeSurfaceTransaction("setSecure");
- if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG, "<<< CLOSE TRANSACTION setSecureLocked");
+ final SurfaceControl.Transaction t = mAnimator.mWin.getPendingTransaction();
+ t.setSecure(mSurfaceControl, isSecure);
+
+ final DisplayContent dc = mAnimator.mWin.mDisplayContent;
+ if (dc != null) {
+ dc.refreshImeSecureFlag(t);
}
+ mService.scheduleAnimationLocked();
}
void setColorSpaceAgnostic(SurfaceControl.Transaction t, boolean agnostic) {
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index 010604f..02032c7 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -1706,7 +1706,7 @@
}
/** Listener for permission flags changes. */
- abstract class OnPermissionFlagsChangedListener {
+ interface OnPermissionFlagsChangedListener {
/**
* Called when a permission flags change has been made to the upcoming new state.
*
@@ -1714,7 +1714,7 @@
* and only call external code after [onStateMutated] when the new state has actually become
* the current state visible to external code.
*/
- abstract fun onPermissionFlagsChanged(
+ fun onPermissionFlagsChanged(
appId: Int,
userId: Int,
permissionName: String,
@@ -1727,6 +1727,6 @@
*
* Implementations should keep this method fast to avoid stalling the locked state mutation.
*/
- abstract fun onStateMutated()
+ fun onStateMutated()
}
}
diff --git a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
index 7db09f9..bb68bc5 100644
--- a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
@@ -263,10 +263,6 @@
synchronized(listenersLock) { listeners = listeners + listener }
}
- fun removeOnPermissionFlagsChangedListener(listener: OnDevicePermissionFlagsChangedListener) {
- synchronized(listenersLock) { listeners = listeners - listener }
- }
-
private fun isDeviceAwarePermission(permissionName: String): Boolean =
DEVICE_AWARE_PERMISSIONS.contains(permissionName)
@@ -283,11 +279,8 @@
}
}
- /**
- * TODO: b/289355341 - implement listener for permission changes Listener for permission flags
- * changes.
- */
- abstract class OnDevicePermissionFlagsChangedListener {
+ /** Listener for permission flags changes. */
+ interface OnDevicePermissionFlagsChangedListener {
/**
* Called when a permission flags change has been made to the upcoming new state.
*
@@ -295,7 +288,7 @@
* and only call external code after [onStateMutated] when the new state has actually become
* the current state visible to external code.
*/
- abstract fun onDevicePermissionFlagsChanged(
+ fun onDevicePermissionFlagsChanged(
appId: Int,
userId: Int,
deviceId: String,
@@ -309,6 +302,6 @@
*
* Implementations should keep this method fast to avoid stalling the locked state mutation.
*/
- abstract fun onStateMutated()
+ fun onStateMutated()
}
}
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index ab3d78c..0d196b4 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -19,6 +19,7 @@
import android.Manifest
import android.app.ActivityManager
import android.app.AppOpsManager
+import android.companion.virtual.VirtualDeviceManager
import android.compat.annotation.ChangeId
import android.compat.annotation.EnabledAfter
import android.content.Context
@@ -169,6 +170,7 @@
onPermissionsChangeListeners = OnPermissionsChangeListeners(FgThread.get().looper)
onPermissionFlagsChangedListener = OnPermissionFlagsChangedListener()
policy.addOnPermissionFlagsChangedListener(onPermissionFlagsChangedListener)
+ devicePolicy.addOnPermissionFlagsChangedListener(onPermissionFlagsChangedListener)
}
override fun getAllPermissionGroups(flags: Int): List<PermissionGroupInfo> {
@@ -2616,10 +2618,11 @@
/** Callback invoked when interesting actions have been taken on a permission. */
private inner class OnPermissionFlagsChangedListener :
- AppIdPermissionPolicy.OnPermissionFlagsChangedListener() {
+ AppIdPermissionPolicy.OnPermissionFlagsChangedListener,
+ DevicePermissionPolicy.OnDevicePermissionFlagsChangedListener {
private var isPermissionFlagsChanged = false
- private val runtimePermissionChangedUids = MutableIntSet()
+ private val runtimePermissionChangedUidDevices = MutableIntMap<MutableSet<String>>()
// Mapping from UID to whether only notifications permissions are revoked.
private val runtimePermissionRevokedUids = SparseBooleanArray()
private val gidsChangedUids = MutableIntSet()
@@ -2642,6 +2645,24 @@
oldFlags: Int,
newFlags: Int
) {
+ onDevicePermissionFlagsChanged(
+ appId,
+ userId,
+ VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT,
+ permissionName,
+ oldFlags,
+ newFlags
+ )
+ }
+
+ override fun onDevicePermissionFlagsChanged(
+ appId: Int,
+ userId: Int,
+ deviceId: String,
+ permissionName: String,
+ oldFlags: Int,
+ newFlags: Int
+ ) {
isPermissionFlagsChanged = true
val uid = UserHandle.getUid(userId, appId)
@@ -2655,12 +2676,12 @@
// permission flags have changed for a non-runtime permission, now we no longer do
// that because permission flags are only for runtime permissions and the listeners
// aren't being notified of non-runtime permission grant state changes anyway.
- runtimePermissionChangedUids += uid
if (wasPermissionGranted && !isPermissionGranted) {
runtimePermissionRevokedUids[uid] =
permissionName in NOTIFICATIONS_PERMISSIONS &&
runtimePermissionRevokedUids.get(uid, true)
}
+ runtimePermissionChangedUidDevices.getOrPut(uid) { mutableSetOf() } += deviceId
}
if (permission.hasGids && !wasPermissionGranted && isPermissionGranted) {
@@ -2674,10 +2695,12 @@
isPermissionFlagsChanged = false
}
- runtimePermissionChangedUids.forEachIndexed { _, uid ->
- onPermissionsChangeListeners.onPermissionsChanged(uid)
+ runtimePermissionChangedUidDevices.forEachIndexed { _, uid, deviceIds ->
+ deviceIds.forEach { deviceId ->
+ onPermissionsChangeListeners.onPermissionsChanged(uid, deviceId)
+ }
}
- runtimePermissionChangedUids.clear()
+ runtimePermissionChangedUidDevices.clear()
if (!isKillRuntimePermissionRevokedUidsSkipped) {
val reason =
@@ -2749,15 +2772,16 @@
when (msg.what) {
MSG_ON_PERMISSIONS_CHANGED -> {
val uid = msg.arg1
- handleOnPermissionsChanged(uid)
+ val deviceId = msg.obj as String
+ handleOnPermissionsChanged(uid, deviceId)
}
}
}
- private fun handleOnPermissionsChanged(uid: Int) {
+ private fun handleOnPermissionsChanged(uid: Int, deviceId: String) {
listeners.broadcast { listener ->
try {
- listener.onPermissionsChanged(uid)
+ listener.onPermissionsChanged(uid, deviceId)
} catch (e: RemoteException) {
Slog.e(LOG_TAG, "Error when calling OnPermissionsChangeListener", e)
}
@@ -2772,9 +2796,9 @@
listeners.unregister(listener)
}
- fun onPermissionsChanged(uid: Int) {
+ fun onPermissionsChanged(uid: Int, deviceId: String) {
if (listeners.registeredCallbackCount > 0) {
- obtainMessage(MSG_ON_PERMISSIONS_CHANGED, uid, 0).sendToTarget()
+ obtainMessage(MSG_ON_PERMISSIONS_CHANGED, uid, 0, deviceId).sendToTarget()
}
}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index b63a58a..2134278 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
import android.app.Instrumentation;
import android.content.Context;
@@ -48,6 +49,7 @@
import com.android.apps.inputmethod.simpleime.ims.InputMethodServiceWrapper;
import com.android.apps.inputmethod.simpleime.testing.TestActivity;
+import com.android.internal.inputmethod.InputMethodNavButtonFlags;
import org.junit.After;
import org.junit.Before;
@@ -635,6 +637,82 @@
.getRootWindowInsets().getInsetsIgnoringVisibility(captionBar()));
}
+ /**
+ * This checks that trying to show and hide the navigation bar takes effect
+ * when the IME does draw the IME navigation bar.
+ */
+ @Test
+ public void testShowHideImeNavigationBar_doesDrawImeNavBar() throws Exception {
+ boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+ .hasNavigationBar(mInputMethodService.getDisplayId());
+ assumeTrue("Must have a navigation bar", hasNavigationBar);
+
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // Show IME
+ verifyInputViewStatusOnMainSync(
+ () -> {
+ mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+ InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR
+ | InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN
+ );
+ assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue();
+ },
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue();
+
+ // Try to hide IME nav bar
+ mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
+ .getInsetsController().hide(captionBar()));
+ mInstrumentation.waitForIdleSync();
+ assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
+
+ // Try to show IME nav bar
+ mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
+ .getInsetsController().show(captionBar()));
+ mInstrumentation.waitForIdleSync();
+ assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue();
+ }
+ /**
+ * This checks that trying to show and hide the navigation bar has no effect
+ * when the IME does not draw the IME navigation bar.
+ *
+ * Note: The IME navigation bar is *never* visible in 3 button navigation mode.
+ */
+ @Test
+ public void testShowHideImeNavigationBar_doesNotDrawImeNavBar() throws Exception {
+ boolean hasNavigationBar = WindowManagerGlobal.getWindowManagerService()
+ .hasNavigationBar(mInputMethodService.getDisplayId());
+ assumeTrue("Must have a navigation bar", hasNavigationBar);
+
+ setShowImeWithHardKeyboard(true /* enabled */);
+
+ // Show IME
+ verifyInputViewStatusOnMainSync(() -> {
+ mInputMethodService.getInputMethodInternal().onNavButtonFlagsChanged(
+ 0 /* navButtonFlags */);
+ assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue();
+ },
+ true /* expected */,
+ true /* inputViewStarted */);
+ assertThat(mInputMethodService.isInputViewShown()).isTrue();
+ assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
+
+ // Try to hide IME nav bar
+ mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
+ .getInsetsController().hide(captionBar()));
+ mInstrumentation.waitForIdleSync();
+ assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
+
+ // Try to show IME nav bar
+ mInstrumentation.runOnMainSync(() -> mInputMethodService.getWindow().getWindow()
+ .getInsetsController().show(captionBar()));
+ mInstrumentation.waitForIdleSync();
+ assertThat(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse();
+ }
+
private void verifyInputViewStatus(
Runnable runnable, boolean expected, boolean inputViewStarted)
throws InterruptedException {
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
index 10f8510..4958f1c 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java
@@ -44,6 +44,7 @@
import static com.android.server.job.controllers.ConnectivityController.TRANSPORT_AFFINITY_AVOID;
import static com.android.server.job.controllers.ConnectivityController.TRANSPORT_AFFINITY_PREFER;
import static com.android.server.job.controllers.ConnectivityController.TRANSPORT_AFFINITY_UNDEFINED;
+import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONNECTIVITY;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -51,6 +52,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
@@ -989,6 +991,7 @@
final ConnectivityController controller = new ConnectivityController(mService,
mFlexibilityController);
+ InOrder flexControllerInOrder = inOrder(mFlexibilityController);
final Network meteredNet = mock(Network.class);
final NetworkCapabilities meteredCaps = createCapabilitiesBuilder().build();
@@ -1042,10 +1045,13 @@
answerNetwork(generalCallback, redCallback, null, null, null);
answerNetwork(generalCallback, blueCallback, null, null, null);
- assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ flexControllerInOrder.verify(mFlexibilityController, never())
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), anyBoolean(), anyLong());
+
+ assertFalse(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(red2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(blue2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
assertFalse(red.areTransportAffinitiesSatisfied());
assertFalse(blue.areTransportAffinitiesSatisfied());
assertFalse(red2.areTransportAffinitiesSatisfied());
@@ -1059,13 +1065,15 @@
generalCallback.onCapabilitiesChanged(meteredNet, meteredCaps);
- assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ assertFalse(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(red2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(blue2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
// No transport is specified. Accept the network for transport affinity.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false);
controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(true), anyLong());
assertFalse(red.areTransportAffinitiesSatisfied());
assertTrue(blue.areTransportAffinitiesSatisfied());
assertFalse(red2.areTransportAffinitiesSatisfied());
@@ -1073,6 +1081,8 @@
// No transport is specified. Avoid the network for transport affinity.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true);
controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(false), anyLong());
assertFalse(red.areTransportAffinitiesSatisfied());
assertFalse(blue.areTransportAffinitiesSatisfied());
assertFalse(red2.areTransportAffinitiesSatisfied());
@@ -1086,12 +1096,17 @@
generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
- assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ flexControllerInOrder.verify(mFlexibilityController, never())
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), anyBoolean(), anyLong());
+
+ assertFalse(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
// No transport is specified. Accept the network for transport affinity.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false);
controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(true), anyLong());
assertFalse(red.areTransportAffinitiesSatisfied());
assertTrue(blue.areTransportAffinitiesSatisfied());
assertFalse(red2.areTransportAffinitiesSatisfied());
@@ -1099,6 +1114,8 @@
// No transport is specified. Avoid the network for transport affinity.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true);
controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(false), anyLong());
assertFalse(red.areTransportAffinitiesSatisfied());
assertFalse(blue.areTransportAffinitiesSatisfied());
assertFalse(red2.areTransportAffinitiesSatisfied());
@@ -1112,8 +1129,13 @@
generalCallback.onLost(meteredNet);
- assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ // Only the metered network is lost. The unmetered network still satisfies the
+ // affinities.
+ flexControllerInOrder.verify(mFlexibilityController, never())
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), anyBoolean(), anyLong());
+
+ assertTrue(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
}
// Specific UID was blocked
@@ -1123,8 +1145,12 @@
generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCaps);
- assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ // No change
+ flexControllerInOrder.verify(mFlexibilityController, never())
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), anyBoolean(), anyLong());
+
+ assertFalse(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
}
// Metered wifi
@@ -1134,10 +1160,13 @@
generalCallback.onCapabilitiesChanged(meteredNet, meteredWifiCaps);
- assertFalse(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(true), anyLong());
+
+ assertFalse(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(red2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
// Wifi is preferred.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false);
@@ -1164,10 +1193,14 @@
generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredCelullarCaps);
- assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ // Metered network still has wifi transport
+ flexControllerInOrder.verify(mFlexibilityController, never())
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(false), anyLong());
+
+ assertTrue(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(red2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(blue2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
// Cellular is avoided.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false);
@@ -1185,6 +1218,14 @@
assertFalse(blue2.areTransportAffinitiesSatisfied());
}
+ // Remove wifi transport
+ {
+ generalCallback.onCapabilitiesChanged(meteredNet, meteredCaps);
+
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(false), anyLong());
+ }
+
// Undefined affinity
final NetworkCapabilities unmeteredTestCaps = createCapabilitiesBuilder()
.addCapability(NET_CAPABILITY_NOT_METERED)
@@ -1198,14 +1239,16 @@
generalCallback.onCapabilitiesChanged(unmeteredNet, unmeteredTestCaps);
- assertTrue(red.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertTrue(blue.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(red2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(blue2.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ assertTrue(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertTrue(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(red2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(blue2.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
// Undefined is preferred.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false);
controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(true), anyLong());
assertTrue(red.areTransportAffinitiesSatisfied());
assertTrue(blue.areTransportAffinitiesSatisfied());
assertFalse(red2.areTransportAffinitiesSatisfied());
@@ -1213,6 +1256,46 @@
// Undefined is avoided.
setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true);
controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(false), anyLong());
+ assertFalse(red.areTransportAffinitiesSatisfied());
+ assertFalse(blue.areTransportAffinitiesSatisfied());
+ assertFalse(red2.areTransportAffinitiesSatisfied());
+ assertFalse(blue2.areTransportAffinitiesSatisfied());
+ }
+
+ // Lost all networks
+ {
+ // Set network as accepted to help confirm onLost notifies flex controller
+ setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false);
+ controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(true), anyLong());
+
+ answerNetwork(generalCallback, redCallback, unmeteredNet, null, null);
+ answerNetwork(generalCallback, blueCallback, unmeteredNet, null, null);
+
+ generalCallback.onLost(meteredNet);
+ generalCallback.onLost(unmeteredNet);
+
+ flexControllerInOrder.verify(mFlexibilityController)
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), eq(false), anyLong());
+
+ assertFalse(red.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(blue.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, false);
+ controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController, never())
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), anyBoolean(), anyLong());
+ assertFalse(red.areTransportAffinitiesSatisfied());
+ assertFalse(blue.areTransportAffinitiesSatisfied());
+ assertFalse(red2.areTransportAffinitiesSatisfied());
+ assertFalse(blue2.areTransportAffinitiesSatisfied());
+ // Undefined is avoided.
+ setDeviceConfigBoolean(controller, KEY_AVOID_UNDEFINED_TRANSPORT_AFFINITY, true);
+ controller.onConstantsUpdatedLocked();
+ flexControllerInOrder.verify(mFlexibilityController, never())
+ .setConstraintSatisfied(eq(CONSTRAINT_CONNECTIVITY), anyBoolean(), anyLong());
assertFalse(red.areTransportAffinitiesSatisfied());
assertFalse(blue.areTransportAffinitiesSatisfied());
assertFalse(red2.areTransportAffinitiesSatisfied());
@@ -1275,7 +1358,7 @@
final ConnectivityController controller = spy(
new ConnectivityController(mService, mFlexibilityController));
doReturn(true).when(controller)
- .wouldBeReadyWithConstraintLocked(any(), eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+ .wouldBeReadyWithConstraintLocked(any(), eq(CONSTRAINT_CONNECTIVITY));
doReturn(true).when(controller).isNetworkAvailable(any());
final JobStatus red = createJobStatus(createJob()
.setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
@@ -1318,7 +1401,7 @@
final ConnectivityController controller = spy(
new ConnectivityController(mService, mFlexibilityController));
doReturn(false).when(controller)
- .wouldBeReadyWithConstraintLocked(any(), eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+ .wouldBeReadyWithConstraintLocked(any(), eq(CONSTRAINT_CONNECTIVITY));
final JobStatus red = createJobStatus(createJob()
.setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), 0)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED), UID_RED);
@@ -1388,7 +1471,7 @@
// Both jobs would still be ready. Exception should not be revoked.
doReturn(true).when(controller)
- .wouldBeReadyWithConstraintLocked(any(), eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+ .wouldBeReadyWithConstraintLocked(any(), eq(CONSTRAINT_CONNECTIVITY));
doReturn(true).when(controller).isNetworkAvailable(any());
controller.reevaluateStateLocked(UID_RED);
inOrder.verify(mNetPolicyManagerInternal, never())
@@ -1396,9 +1479,9 @@
// One job is still ready. Exception should not be revoked.
doReturn(true).when(controller).wouldBeReadyWithConstraintLocked(
- eq(redOne), eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+ eq(redOne), eq(CONSTRAINT_CONNECTIVITY));
doReturn(false).when(controller).wouldBeReadyWithConstraintLocked(
- eq(redTwo), eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+ eq(redTwo), eq(CONSTRAINT_CONNECTIVITY));
controller.reevaluateStateLocked(UID_RED);
inOrder.verify(mNetPolicyManagerInternal, never())
.setAppIdleWhitelist(eq(UID_RED), anyBoolean());
@@ -1406,7 +1489,7 @@
// Both jobs are not ready. Exception should be revoked.
doReturn(false).when(controller)
- .wouldBeReadyWithConstraintLocked(any(), eq(JobStatus.CONSTRAINT_CONNECTIVITY));
+ .wouldBeReadyWithConstraintLocked(any(), eq(CONSTRAINT_CONNECTIVITY));
controller.reevaluateStateLocked(UID_RED);
inOrder.verify(mNetPolicyManagerInternal, times(1))
.setAppIdleWhitelist(eq(UID_RED), eq(false));
@@ -1473,26 +1556,26 @@
controller.maybeStartTrackingJobLocked(unnetworked, null);
answerNetwork(callback.getValue(), redCallback.getValue(), null, cellularNet, cellularCaps);
- assertTrue(networked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
- assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ assertTrue(networked.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
+ assertFalse(unnetworked.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
networked.setStandbyBucket(RESTRICTED_INDEX);
unnetworked.setStandbyBucket(RESTRICTED_INDEX);
controller.startTrackingRestrictedJobLocked(networked);
controller.startTrackingRestrictedJobLocked(unnetworked);
- assertFalse(networked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ assertFalse(networked.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
// Unnetworked shouldn't be affected by ConnectivityController since it doesn't have a
// connectivity constraint.
- assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ assertFalse(unnetworked.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
networked.setStandbyBucket(RARE_INDEX);
unnetworked.setStandbyBucket(RARE_INDEX);
controller.stopTrackingRestrictedJobLocked(networked);
controller.stopTrackingRestrictedJobLocked(unnetworked);
- assertTrue(networked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ assertTrue(networked.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
// Unnetworked shouldn't be affected by ConnectivityController since it doesn't have a
// connectivity constraint.
- assertFalse(unnetworked.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY));
+ assertFalse(unnetworked.isConstraintSatisfied(CONSTRAINT_CONNECTIVITY));
}
@Test
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 ee68b6d..0659f7e 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
@@ -19,19 +19,25 @@
import static android.app.job.JobInfo.BIAS_FOREGROUND_SERVICE;
import static android.app.job.JobInfo.BIAS_TOP_APP;
import static android.app.job.JobInfo.NETWORK_TYPE_ANY;
+import static android.app.job.JobInfo.NETWORK_TYPE_CELLULAR;
+import static android.app.job.JobInfo.NETWORK_TYPE_NONE;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.text.format.DateUtils.HOUR_IN_MILLIS;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS;
+import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_DEADLINE_PROXIMITY_LIMIT;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FLEXIBILITY_ENABLED;
import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS;
import static com.android.server.job.controllers.FlexibilityController.NUM_FLEXIBLE_CONSTRAINTS;
+import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING;
+import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CONNECTIVITY;
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;
@@ -54,6 +60,7 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
+import android.net.NetworkRequest;
import android.os.Looper;
import android.provider.DeviceConfig;
import android.util.ArraySet;
@@ -693,15 +700,59 @@
@Test
public void testTransportAffinity() {
- JobInfo.Builder jb = createJob(0).setRequiredNetworkType(NETWORK_TYPE_ANY);
- JobStatus js = createJobStatus("testTopAppBypass", jb);
+ JobStatus jsAny = createJobStatus("testTransportAffinity",
+ createJob(0).setRequiredNetworkType(NETWORK_TYPE_ANY));
+ JobStatus jsCell = createJobStatus("testTransportAffinity",
+ createJob(0).setRequiredNetworkType(NETWORK_TYPE_CELLULAR));
+ JobStatus jsWifi = createJobStatus("testTransportAffinity",
+ createJob(0).setRequiredNetwork(
+ new NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .build()));
+ // Disable the unseen constraint logic.
+ mFlexibilityController.setConstraintSatisfied(
+ SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS, true, FROZEN_TIME);
+ mFlexibilityController.setConstraintSatisfied(
+ SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS, false, FROZEN_TIME);
+ // Require only a single constraint
+ jsAny.adjustNumRequiredFlexibleConstraints(-3);
+ jsCell.adjustNumRequiredFlexibleConstraints(-2);
+ jsWifi.adjustNumRequiredFlexibleConstraints(-2);
synchronized (mFlexibilityController.mLock) {
- js.setTransportAffinitiesSatisfied(false);
- assertEquals(0, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js));
- js.setTransportAffinitiesSatisfied(true);
- assertEquals(1, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js));
- js.setTransportAffinitiesSatisfied(false);
- assertEquals(0, mFlexibilityController.getNumSatisfiedRequiredConstraintsLocked(js));
+ jsAny.setTransportAffinitiesSatisfied(false);
+ jsCell.setTransportAffinitiesSatisfied(false);
+ jsWifi.setTransportAffinitiesSatisfied(false);
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_CONNECTIVITY, false, FROZEN_TIME);
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsAny));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsCell));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsWifi));
+
+ // A good network exists, but the network hasn't been assigned to any of the jobs
+ jsAny.setTransportAffinitiesSatisfied(false);
+ jsCell.setTransportAffinitiesSatisfied(false);
+ jsWifi.setTransportAffinitiesSatisfied(false);
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_CONNECTIVITY, true, FROZEN_TIME);
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsAny));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsCell));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsWifi));
+
+ // The good network has been assigned to the relevant jobs
+ jsAny.setTransportAffinitiesSatisfied(true);
+ jsCell.setTransportAffinitiesSatisfied(false);
+ jsWifi.setTransportAffinitiesSatisfied(true);
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsAny));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsCell));
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsWifi));
+
+ // One job loses access to the network.
+ jsAny.setTransportAffinitiesSatisfied(true);
+ jsCell.setTransportAffinitiesSatisfied(false);
+ jsWifi.setTransportAffinitiesSatisfied(false);
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsAny));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsCell));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(jsWifi));
}
}
@@ -768,6 +819,131 @@
}
@Test
+ public void testHasEnoughSatisfiedConstraints_unseenConstraints_soonAfterBoot() {
+ // Add connectivity to require 4 constraints
+ JobStatus js = createJobStatus("testHasEnoughSatisfiedConstraints",
+ createJob(0).setRequiredNetworkType(NETWORK_TYPE_ANY));
+
+ // Too soon after boot
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(Instant.ofEpochMilli(100 - 1), ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(js));
+ }
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(Instant.ofEpochMilli(DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS - 1),
+ ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(js));
+ }
+
+ // Long after boot
+
+ // No constraints ever seen. Don't bother waiting
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(Instant.ofEpochMilli(DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS),
+ ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(js));
+ }
+ }
+
+ @Test
+ public void testHasEnoughSatisfiedConstraints_unseenConstraints_longAfterBoot() {
+ // Add connectivity to require 4 constraints
+ JobStatus connJs = createJobStatus("testHasEnoughSatisfiedConstraints",
+ createJob(0).setRequiredNetworkType(NETWORK_TYPE_ANY));
+ JobStatus nonConnJs = createJobStatus("testHasEnoughSatisfiedConstraints",
+ createJob(0).setRequiredNetworkType(NETWORK_TYPE_NONE));
+
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_BATTERY_NOT_LOW, true,
+ 2 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_CHARGING, true,
+ 3 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_IDLE, true,
+ 4 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_CONNECTIVITY, true,
+ 5 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+
+ // Long after boot
+ // All constraints satisfied right now
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(Instant.ofEpochMilli(DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS),
+ ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(connJs));
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(nonConnJs));
+ }
+
+ // Go down to 2 satisfied
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_CONNECTIVITY, false,
+ 6 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_IDLE, false,
+ 7 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+ // 3 & 4 constraints were seen recently enough, so the job should wait
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(connJs));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(nonConnJs));
+ }
+
+ // 4 constraints still in the grace period. Wait.
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(
+ Instant.ofEpochMilli(16 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10),
+ ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(connJs));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(nonConnJs));
+ }
+
+ // 3 constraints still in the grace period. Wait.
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(
+ Instant.ofEpochMilli(17 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10),
+ ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(connJs));
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(nonConnJs));
+ }
+
+ // 3 constraints haven't been seen recently. Don't wait.
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(
+ Instant.ofEpochMilli(
+ 17 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10 + 1),
+ ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(connJs));
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(nonConnJs));
+ }
+
+ // Add then remove connectivity. Resets expectation of 3 constraints for connectivity jobs.
+ // Connectivity job should wait while the non-connectivity job can run.
+ // of getting back to 4 constraints.
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_CONNECTIVITY, true,
+ 18 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+ mFlexibilityController.setConstraintSatisfied(
+ CONSTRAINT_CONNECTIVITY, false,
+ 19 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10);
+ JobSchedulerService.sElapsedRealtimeClock =
+ Clock.fixed(
+ Instant.ofEpochMilli(
+ 19 * DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS / 10 + 1),
+ ZoneOffset.UTC);
+ synchronized (mFlexibilityController.mLock) {
+ assertFalse(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(connJs));
+ assertTrue(mFlexibilityController.hasEnoughSatisfiedConstraintsLocked(nonConnJs));
+ }
+ }
+
+ @Test
public void testResetJobNumDroppedConstraints() {
JobInfo.Builder jb = createJob(22);
JobStatus js = createJobStatus("testResetJobNumDroppedConstraints", jb);
diff --git a/services/tests/mockingservicestests/src/com/android/server/power/OWNERS b/services/tests/mockingservicestests/src/com/android/server/power/OWNERS
index fb62520..37396f3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/power/OWNERS
+++ b/services/tests/mockingservicestests/src/com/android/server/power/OWNERS
@@ -1,3 +1,3 @@
include /services/core/java/com/android/server/power/OWNERS
-per-file ThermalManagerServiceMockingTest.java=wvw@google.com,xwxw@google.com
+per-file ThermalManagerServiceMockingTest.java=file:/THERMAL_OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
index 7e6883b..ccbbaa5 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java
@@ -100,7 +100,7 @@
}
@Test
- public void registerInputDevice_deviceCreation_hasDeviceId() {
+ public void registerInputDevice_deviceCreation_hasDeviceId() throws Exception {
final IBinder device1Token = new Binder("device1");
mInputController.createMouse("mouse", /*vendorId= */ 1, /*productId= */ 1, device1Token,
/* displayId= */ 1);
@@ -124,7 +124,7 @@
}
@Test
- public void unregisterInputDevice_allMiceUnregistered_clearPointerDisplayId() {
+ public void unregisterInputDevice_allMiceUnregistered_clearPointerDisplayId() throws Exception {
final IBinder deviceToken = new Binder();
mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
/* displayId= */ 1);
@@ -137,7 +137,8 @@
}
@Test
- public void unregisterInputDevice_anotherMouseExists_setPointerDisplayIdOverride() {
+ public void unregisterInputDevice_anotherMouseExists_setPointerDisplayIdOverride()
+ throws Exception {
final IBinder deviceToken = new Binder();
mInputController.createMouse("mouse1", /*vendorId= */ 1, /*productId= */ 1, deviceToken,
/* displayId= */ 1);
@@ -153,7 +154,7 @@
}
@Test
- public void createNavigationTouchpad_hasDeviceId() {
+ public void createNavigationTouchpad_hasDeviceId() throws Exception {
final IBinder deviceToken = new Binder();
mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
@@ -166,7 +167,7 @@
}
@Test
- public void createNavigationTouchpad_setsTypeAssociation() {
+ public void createNavigationTouchpad_setsTypeAssociation() throws Exception {
final IBinder deviceToken = new Binder();
mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
@@ -176,7 +177,7 @@
}
@Test
- public void createAndUnregisterNavigationTouchpad_unsetsTypeAssociation() {
+ public void createAndUnregisterNavigationTouchpad_unsetsTypeAssociation() throws Exception {
final IBinder deviceToken = new Binder();
mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1,
deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50);
@@ -188,7 +189,7 @@
}
@Test
- public void createKeyboard_addAndRemoveKeyboardLayoutAssociation() {
+ public void createKeyboard_addAndRemoveKeyboardLayoutAssociation() throws Exception {
final IBinder deviceToken = new Binder("device");
mInputController.createKeyboard("keyboard", /*vendorId= */2, /*productId= */ 2, deviceToken,
@@ -201,56 +202,7 @@
}
@Test
- public void createInputDevice_tooLongNameRaisesException() {
- final IBinder deviceToken = new Binder("device");
- // The underlying uinput implementation only supports device names up to 80 bytes. This
- // string is all ASCII characters, therefore if we have more than 80 ASCII characters we
- // will have more than 80 bytes.
- String deviceName =
- "This.is.a.very.long.device.name.that.exceeds.the.maximum.length.of.80.bytes"
- + ".by.a.couple.bytes";
-
- assertThrows(RuntimeException.class, () -> {
- mInputController.createDpad(deviceName, /*vendorId= */3, /*productId=*/3, deviceToken,
- 1);
- });
- }
-
- @Test
- public void createInputDevice_tooLongDeviceNameRaisesException() {
- final IBinder deviceToken = new Binder("device");
- // The underlying uinput implementation only supports device names up to 80 bytes (including
- // a 0-byte terminator).
- // This string is 79 characters and 80 bytes (including the 0-byte terminator)
- String deviceName =
- "This.is.a.very.long.device.name.that.exceeds.the.maximum.length01234567890123456";
-
- assertThrows(RuntimeException.class, () -> {
- mInputController.createDpad(deviceName, /*vendorId= */3, /*productId=*/3, deviceToken,
- 1);
- });
- }
-
- @Test
- public void createInputDevice_stringWithLessThanMaxCharsButMoreThanMaxBytesRaisesException() {
- final IBinder deviceToken = new Binder("device1");
-
- // Has only 39 characters but is 109 bytes as utf-8
- String device_name =
- "â–‘â–„â–„â–„â–„â–‘\n" +
- "▀▀▄██►\n" +
- "▀▀███►\n" +
- "░▀███►░█►\n" +
- "▒▄████▀▀";
-
- assertThrows(RuntimeException.class, () -> {
- mInputController.createDpad(device_name, /*vendorId= */5, /*productId=*/5,
- deviceToken, 1);
- });
- }
-
- @Test
- public void createInputDevice_duplicateNamesAreNotAllowed() {
+ public void createInputDevice_duplicateNamesAreNotAllowed() throws Exception {
final IBinder deviceToken1 = new Binder("deviceToken1");
final IBinder deviceToken2 = new Binder("deviceToken2");
@@ -258,9 +210,9 @@
mInputController.createDpad(sharedDeviceName, /*vendorId= */4, /*productId=*/4,
deviceToken1, 1);
- assertThrows("Device names need to be unique", RuntimeException.class, () -> {
- mInputController.createDpad(sharedDeviceName, /*vendorId= */5, /*productId=*/5,
- deviceToken2, 2);
- });
+ assertThrows("Device names need to be unique",
+ InputController.DeviceCreationException.class,
+ () -> mInputController.createDpad(
+ sharedDeviceName, /*vendorId= */5, /*productId=*/5, deviceToken2, 2));
}
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index 30300ec..d87b8d1 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -33,6 +33,7 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.nullable;
@@ -368,6 +369,18 @@
new Handler(TestableLooper.get(this).getLooper()));
when(mContext.getSystemService(Context.POWER_SERVICE)).thenReturn(powerManager);
+ when(mNativeWrapperMock.writeButtonEvent(anyLong(), anyInt(), anyInt(), anyLong()))
+ .thenReturn(true);
+ when(mNativeWrapperMock.writeRelativeEvent(anyLong(), anyFloat(), anyFloat(), anyLong()))
+ .thenReturn(true);
+ when(mNativeWrapperMock.writeScrollEvent(anyLong(), anyFloat(), anyFloat(), anyLong()))
+ .thenReturn(true);
+ when(mNativeWrapperMock.writeKeyEvent(anyLong(), anyInt(), anyInt(), anyLong()))
+ .thenReturn(true);
+ when(mNativeWrapperMock.writeTouchEvent(anyLong(), anyInt(), anyInt(), anyInt(),
+ anyFloat(), anyFloat(), anyFloat(), anyFloat(), anyLong()))
+ .thenReturn(true);
+
mInputManagerMockHelper = new InputManagerMockHelper(
TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock);
// Allow virtual devices to be created on the looper thread for testing.
@@ -1183,12 +1196,12 @@
@Test
public void sendKeyEvent_noFd() {
- assertThrows(
- IllegalArgumentException.class,
- () ->
- mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder()
- .setKeyCode(KeyEvent.KEYCODE_A)
- .setAction(VirtualKeyEvent.ACTION_DOWN).build()));
+ assertThat(mDeviceImpl.sendKeyEvent(BINDER,
+ new VirtualKeyEvent.Builder()
+ .setKeyCode(KeyEvent.KEYCODE_A)
+ .setAction(VirtualKeyEvent.ACTION_DOWN)
+ .build()))
+ .isFalse();
}
@Test
@@ -1201,24 +1214,24 @@
InputController.InputDeviceDescriptor.TYPE_KEYBOARD, DISPLAY_ID_1, PHYS,
DEVICE_NAME_1, INPUT_DEVICE_ID);
- mDeviceImpl.sendKeyEvent(BINDER, new VirtualKeyEvent.Builder()
- .setKeyCode(keyCode)
- .setAction(action)
- .setEventTimeNanos(eventTimeNanos)
- .build());
+ assertThat(mDeviceImpl.sendKeyEvent(BINDER,
+ new VirtualKeyEvent.Builder()
+ .setKeyCode(keyCode)
+ .setAction(action)
+ .setEventTimeNanos(eventTimeNanos)
+ .build()))
+ .isTrue();
verify(mNativeWrapperMock).writeKeyEvent(fd, keyCode, action, eventTimeNanos);
}
@Test
public void sendButtonEvent_noFd() {
- assertThrows(
- IllegalArgumentException.class,
- () ->
- mDeviceImpl.sendButtonEvent(BINDER,
- new VirtualMouseButtonEvent.Builder()
- .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK)
- .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_PRESS)
- .build()));
+ assertThat(mDeviceImpl.sendButtonEvent(BINDER,
+ new VirtualMouseButtonEvent.Builder()
+ .setButtonCode(VirtualMouseButtonEvent.BUTTON_BACK)
+ .setAction(VirtualMouseButtonEvent.ACTION_BUTTON_PRESS)
+ .build()))
+ .isFalse();
}
@Test
@@ -1231,38 +1244,24 @@
InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS,
DEVICE_NAME_1, INPUT_DEVICE_ID);
doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
- mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder()
- .setButtonCode(buttonCode)
- .setAction(action)
- .setEventTimeNanos(eventTimeNanos)
- .build());
+ assertThat(mDeviceImpl.sendButtonEvent(BINDER,
+ new VirtualMouseButtonEvent.Builder()
+ .setButtonCode(buttonCode)
+ .setAction(action)
+ .setEventTimeNanos(eventTimeNanos)
+ .build()))
+ .isTrue();
verify(mNativeWrapperMock).writeButtonEvent(fd, buttonCode, action, eventTimeNanos);
}
@Test
- public void sendButtonEvent_hasFd_wrongDisplay_throwsIllegalStateException() {
- final int fd = 1;
- final int buttonCode = VirtualMouseButtonEvent.BUTTON_BACK;
- final int action = VirtualMouseButtonEvent.ACTION_BUTTON_PRESS;
- mInputController.addDeviceForTesting(BINDER, fd,
- InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1,
- INPUT_DEVICE_ID);
- assertThrows(
- IllegalStateException.class,
- () ->
- mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder()
- .setButtonCode(buttonCode)
- .setAction(action).build()));
- }
-
- @Test
public void sendRelativeEvent_noFd() {
- assertThrows(
- IllegalArgumentException.class,
- () ->
- mDeviceImpl.sendRelativeEvent(BINDER,
- new VirtualMouseRelativeEvent.Builder().setRelativeX(
- 0.0f).setRelativeY(0.0f).build()));
+ assertThat(mDeviceImpl.sendRelativeEvent(BINDER,
+ new VirtualMouseRelativeEvent.Builder()
+ .setRelativeX(0.0f)
+ .setRelativeY(0.0f)
+ .build()))
+ .isFalse();
}
@Test
@@ -1275,39 +1274,25 @@
InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1,
INPUT_DEVICE_ID);
doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
- mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder()
- .setRelativeX(x)
- .setRelativeY(y)
- .setEventTimeNanos(eventTimeNanos)
- .build());
+ assertThat(mDeviceImpl.sendRelativeEvent(BINDER,
+ new VirtualMouseRelativeEvent.Builder()
+ .setRelativeX(x)
+ .setRelativeY(y)
+ .setEventTimeNanos(eventTimeNanos)
+ .build()))
+ .isTrue();
verify(mNativeWrapperMock).writeRelativeEvent(fd, x, y, eventTimeNanos);
}
- @Test
- public void sendRelativeEvent_hasFd_wrongDisplay_throwsIllegalStateException() {
- final int fd = 1;
- final float x = -0.2f;
- final float y = 0.7f;
- mInputController.addDeviceForTesting(BINDER, fd,
- InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1,
- INPUT_DEVICE_ID);
- assertThrows(
- IllegalStateException.class,
- () ->
- mDeviceImpl.sendRelativeEvent(BINDER,
- new VirtualMouseRelativeEvent.Builder()
- .setRelativeX(x).setRelativeY(y).build()));
- }
@Test
public void sendScrollEvent_noFd() {
- assertThrows(
- IllegalArgumentException.class,
- () ->
- mDeviceImpl.sendScrollEvent(BINDER,
- new VirtualMouseScrollEvent.Builder()
- .setXAxisMovement(-1f)
- .setYAxisMovement(1f).build()));
+ assertThat(mDeviceImpl.sendScrollEvent(BINDER,
+ new VirtualMouseScrollEvent.Builder()
+ .setXAxisMovement(-1f)
+ .setYAxisMovement(1f)
+ .build()))
+ .isFalse();
}
@Test
@@ -1320,42 +1305,28 @@
InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1,
INPUT_DEVICE_ID);
doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId();
- mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder()
- .setXAxisMovement(x)
- .setYAxisMovement(y)
- .setEventTimeNanos(eventTimeNanos)
- .build());
+ assertThat(mDeviceImpl.sendScrollEvent(BINDER,
+ new VirtualMouseScrollEvent.Builder()
+ .setXAxisMovement(x)
+ .setYAxisMovement(y)
+ .setEventTimeNanos(eventTimeNanos)
+ .build()))
+ .isTrue();
verify(mNativeWrapperMock).writeScrollEvent(fd, x, y, eventTimeNanos);
}
- @Test
- public void sendScrollEvent_hasFd_wrongDisplay_throwsIllegalStateException() {
- final int fd = 1;
- final float x = 0.5f;
- final float y = 1f;
- mInputController.addDeviceForTesting(BINDER, fd,
- InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1,
- INPUT_DEVICE_ID);
- assertThrows(
- IllegalStateException.class,
- () ->
- mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder()
- .setXAxisMovement(x)
- .setYAxisMovement(y).build()));
- }
@Test
public void sendTouchEvent_noFd() {
- assertThrows(
- IllegalArgumentException.class,
- () ->
- mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder()
- .setX(0.0f)
- .setY(0.0f)
- .setAction(VirtualTouchEvent.ACTION_UP)
- .setPointerId(1)
- .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
- .build()));
+ assertThat(mDeviceImpl.sendTouchEvent(BINDER,
+ new VirtualTouchEvent.Builder()
+ .setX(0.0f)
+ .setY(0.0f)
+ .setAction(VirtualTouchEvent.ACTION_UP)
+ .setPointerId(1)
+ .setToolType(VirtualTouchEvent.TOOL_TYPE_FINGER)
+ .build()))
+ .isFalse();
}
@Test
@@ -1370,14 +1341,16 @@
mInputController.addDeviceForTesting(BINDER, fd,
InputController.InputDeviceDescriptor.TYPE_TOUCHSCREEN, DISPLAY_ID_1, PHYS,
DEVICE_NAME_1, INPUT_DEVICE_ID);
- mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder()
- .setX(x)
- .setY(y)
- .setAction(action)
- .setPointerId(pointerId)
- .setToolType(toolType)
- .setEventTimeNanos(eventTimeNanos)
- .build());
+ assertThat(mDeviceImpl.sendTouchEvent(BINDER,
+ new VirtualTouchEvent.Builder()
+ .setX(x)
+ .setY(y)
+ .setAction(action)
+ .setPointerId(pointerId)
+ .setToolType(toolType)
+ .setEventTimeNanos(eventTimeNanos)
+ .build()))
+ .isTrue();
verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, Float.NaN,
Float.NaN, eventTimeNanos);
}
@@ -1396,16 +1369,18 @@
mInputController.addDeviceForTesting(BINDER, fd,
InputController.InputDeviceDescriptor.TYPE_TOUCHSCREEN, DISPLAY_ID_1, PHYS,
DEVICE_NAME_1, INPUT_DEVICE_ID);
- mDeviceImpl.sendTouchEvent(BINDER, new VirtualTouchEvent.Builder()
- .setX(x)
- .setY(y)
- .setAction(action)
- .setPointerId(pointerId)
- .setToolType(toolType)
- .setPressure(pressure)
- .setMajorAxisSize(majorAxisSize)
- .setEventTimeNanos(eventTimeNanos)
- .build());
+ assertThat(mDeviceImpl.sendTouchEvent(BINDER,
+ new VirtualTouchEvent.Builder()
+ .setX(x)
+ .setY(y)
+ .setAction(action)
+ .setPointerId(pointerId)
+ .setToolType(toolType)
+ .setPressure(pressure)
+ .setMajorAxisSize(majorAxisSize)
+ .setEventTimeNanos(eventTimeNanos)
+ .build()))
+ .isTrue();
verify(mNativeWrapperMock).writeTouchEvent(fd, pointerId, toolType, action, x, y, pressure,
majorAxisSize, eventTimeNanos);
}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
index 2583023..01922e0 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -57,12 +57,11 @@
private static final int CAMERA_DISPLAY_NAME_RES_ID_1 = 10;
private static final int CAMERA_WIDTH_1 = 100;
private static final int CAMERA_HEIGHT_1 = 200;
- private static final int CAMERA_FORMAT_1 = ImageFormat.RGB_565;
private static final int CAMERA_DISPLAY_NAME_RES_ID_2 = 11;
private static final int CAMERA_WIDTH_2 = 400;
private static final int CAMERA_HEIGHT_2 = 600;
- private static final int CAMERA_FORMAT_2 = ImageFormat.YUY2;
+ private static final int CAMERA_FORMAT = ImageFormat.YUV_420_888;
@Mock
private IVirtualCameraService mVirtualCameraServiceMock;
@@ -81,7 +80,7 @@
@Test
public void registerCamera_registersCamera() throws Exception {
mVirtualCameraController.registerCamera(createVirtualCameraConfig(
- CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1));
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_1));
ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
@@ -89,13 +88,13 @@
VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1,
- CAMERA_HEIGHT_1, CAMERA_FORMAT_1);
+ CAMERA_HEIGHT_1, CAMERA_FORMAT);
}
@Test
public void unregisterCamera_unregistersCamera() throws Exception {
VirtualCameraConfig config = createVirtualCameraConfig(
- CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1);
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_1);
mVirtualCameraController.unregisterCamera(config);
verify(mVirtualCameraServiceMock).unregisterCamera(any());
@@ -104,9 +103,9 @@
@Test
public void close_unregistersAllCameras() throws Exception {
mVirtualCameraController.registerCamera(createVirtualCameraConfig(
- CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1));
+ CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_1));
mVirtualCameraController.registerCamera(createVirtualCameraConfig(
- CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_DISPLAY_NAME_RES_ID_2));
+ CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT, CAMERA_DISPLAY_NAME_RES_ID_2));
ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
@@ -117,9 +116,9 @@
configurationCaptor.getAllValues();
assertThat(virtualCameraConfigurations).hasSize(2);
assertVirtualCameraConfiguration(virtualCameraConfigurations.get(0), CAMERA_WIDTH_1,
- CAMERA_HEIGHT_1, CAMERA_FORMAT_1);
+ CAMERA_HEIGHT_1, CAMERA_FORMAT);
assertVirtualCameraConfiguration(virtualCameraConfigurations.get(1), CAMERA_WIDTH_2,
- CAMERA_HEIGHT_2, CAMERA_FORMAT_2);
+ CAMERA_HEIGHT_2, CAMERA_FORMAT);
}
private VirtualCameraConfig createVirtualCameraConfig(
diff --git a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
index 2db46e6..46ead85 100644
--- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
@@ -571,6 +571,29 @@
}
@Test
+ public void testDebugTagsPersisted() throws Exception {
+ JobInfo ji = new Builder(53, mComponent)
+ .setPersisted(true)
+ .addDebugTag("a")
+ .addDebugTag("b")
+ .addDebugTag("c")
+ .addDebugTag("d")
+ .removeDebugTag("d")
+ .build();
+ final JobStatus js = JobStatus.createFromJobInfo(ji, SOME_UID, null, -1, null, null);
+ mTaskStoreUnderTest.add(js);
+ waitForPendingIo();
+
+ Set<String> expectedTags = Set.of("a", "b", "c");
+
+ final JobSet jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+ assertEquals("Debug tags not correctly persisted",
+ expectedTags, loaded.getJob().getDebugTags());
+ }
+
+ @Test
public void testNamespacePersisted() throws Exception {
final String namespace = "my.test.namespace";
JobInfo.Builder b = new Builder(93, mComponent)
@@ -675,6 +698,22 @@
}
@Test
+ public void testTraceTagPersisted() throws Exception {
+ JobInfo ji = new Builder(53, mComponent)
+ .setPersisted(true)
+ .setTraceTag("tag")
+ .build();
+ final JobStatus js = JobStatus.createFromJobInfo(ji, SOME_UID, null, -1, null, null);
+ mTaskStoreUnderTest.add(js);
+ waitForPendingIo();
+
+ final JobSet jobStatusSet = new JobSet();
+ mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true);
+ JobStatus loaded = jobStatusSet.getAllJobs().iterator().next();
+ assertEquals("Trace tag not correctly persisted", "tag", loaded.getJob().getTraceTag());
+ }
+
+ @Test
public void testEstimatedNetworkBytes() throws Exception {
assertPersistedEquals(new JobInfo.Builder(0, mComponent)
.setPersisted(true)
diff --git a/services/tests/servicestests/src/com/android/server/power/OWNERS b/services/tests/servicestests/src/com/android/server/power/OWNERS
index ef4c0bf..fe93ebb 100644
--- a/services/tests/servicestests/src/com/android/server/power/OWNERS
+++ b/services/tests/servicestests/src/com/android/server/power/OWNERS
@@ -1,3 +1,3 @@
include /services/core/java/com/android/server/power/OWNERS
-per-file ThermalManagerServiceTest.java=wvw@google.com, xwxw@google.com
\ No newline at end of file
+per-file ThermalManagerServiceTest.java=file:/THERMAL_OWNERS
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
index 8dcf89b..999e33c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java
@@ -102,4 +102,18 @@
assertThat(copy.shouldSuppressAmbientDisplay()).isTrue();
assertThat(copy.shouldDisplayGrayscale()).isFalse();
}
+
+ @Test
+ public void hasEffects_none_returnsFalse() {
+ ZenDeviceEffects effects = new ZenDeviceEffects.Builder().build();
+ assertThat(effects.hasEffects()).isFalse();
+ }
+
+ @Test
+ public void hasEffects_some_returnsTrue() {
+ ZenDeviceEffects effects = new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build();
+ assertThat(effects.hasEffects()).isTrue();
+ }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 261b5d3..d466107 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -33,6 +33,7 @@
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import android.service.notification.Condition;
+import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.EventInfo;
import android.service.notification.ZenPolicy;
@@ -327,7 +328,6 @@
rule.conditionId = CONDITION_ID;
rule.condition = CONDITION;
rule.enabled = ENABLED;
- rule.creationTime = 123;
rule.id = "id";
rule.zenMode = INTERRUPTION_FILTER;
rule.modified = true;
@@ -335,6 +335,18 @@
rule.snoozing = true;
rule.pkg = OWNER.getPackageName();
rule.zenPolicy = POLICY;
+ rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
+ .setShouldDisplayGrayscale(false)
+ .setShouldSuppressAmbientDisplay(true)
+ .setShouldDimWallpaper(false)
+ .setShouldUseNightMode(true)
+ .setShouldDisableAutoBrightness(false)
+ .setShouldDisableTapToWake(true)
+ .setShouldDisableTiltToWake(false)
+ .setShouldDisableTouch(true)
+ .setShouldMinimizeRadioUsage(false)
+ .setShouldMaximizeDoze(true)
+ .build();
rule.creationTime = CREATION_TIME;
rule.allowManualInvocation = ALLOW_MANUAL;
@@ -362,6 +374,8 @@
assertEquals(rule.name, fromXml.name);
assertEquals(rule.zenMode, fromXml.zenMode);
assertEquals(rule.creationTime, fromXml.creationTime);
+ assertEquals(rule.zenPolicy, fromXml.zenPolicy);
+ assertEquals(rule.zenDeviceEffects, fromXml.zenDeviceEffects);
assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation);
assertEquals(rule.type, fromXml.type);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index fd3d5e9b..ed7e8ae5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -16,6 +16,8 @@
package com.android.server.notification;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNotNull;
@@ -28,8 +30,10 @@
import android.net.Uri;
import android.provider.Settings;
import android.service.notification.Condition;
+import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeDiff;
+import android.service.notification.ZenModeDiff.RuleDiff;
import android.service.notification.ZenPolicy;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -42,10 +46,14 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
@SmallTest
@@ -56,6 +64,15 @@
public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS =
Set.of("version", "manualRule", "automaticRules");
+ // Differences for flagged fields are only generated if the flag is enabled.
+ // TODO: b/310620812 - Remove this exempt list when flag is inlined.
+ private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
+ android.app.Flags.modesApi()
+ ? Set.of()
+ : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
+ RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
+ RuleDiff.FIELD_ZEN_DEVICE_EFFECTS);
+
@Test
public void testRuleDiff_addRemoveSame() {
// Test add, remove, and both sides same
@@ -86,7 +103,7 @@
ArrayMap<String, Object> expectedFrom = new ArrayMap<>();
ArrayMap<String, Object> expectedTo = new ArrayMap<>();
List<Field> fieldsForDiff = getFieldsForDiffCheck(
- ZenModeConfig.ZenRule.class, Set.of()); // actually no exempt fields for ZenRule
+ ZenModeConfig.ZenRule.class, ZEN_RULE_EXEMPT_FIELDS);
generateFieldDiffs(r1, r2, fieldsForDiff, expectedFrom, expectedTo);
ZenModeDiff.RuleDiff d = new ZenModeDiff.RuleDiff(r1, r2);
@@ -230,16 +247,21 @@
rule.name = "name";
rule.snoozing = true;
rule.pkg = "a";
- rule.allowManualInvocation = true;
- rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
- rule.iconResId = 123;
- rule.triggerDescription = "At night";
+ if (android.app.Flags.modesApi()) {
+ rule.allowManualInvocation = true;
+ rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
+ rule.iconResId = 123;
+ rule.triggerDescription = "At night";
+ rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
+ .setShouldDimWallpaper(true)
+ .build();
+ }
return rule;
}
// Get the fields on which we would want to check a diff. The requirements are: not final or/
// static (as these should/can never change), and not in a specific list that's exempted.
- private List<Field> getFieldsForDiffCheck(Class c, Set<String> exemptNames)
+ private List<Field> getFieldsForDiffCheck(Class<?> c, Set<String> exemptNames)
throws SecurityException {
Field[] fields = c.getDeclaredFields();
ArrayList<Field> out = new ArrayList<>();
@@ -272,7 +294,7 @@
f.setAccessible(true);
// Just double-check also that the fields actually are for the class declared
assertEquals(f.getDeclaringClass(), a.getClass());
- Class t = f.getType();
+ Class<?> t = f.getType();
// handle the full set of primitive types first
if (boolean.class.equals(t)) {
f.setBoolean(a, true);
@@ -305,8 +327,8 @@
f.set(a, null);
expectedA.put(f.getName(), null);
try {
- f.set(b, t.getDeclaredConstructor().newInstance());
- expectedB.put(f.getName(), t.getDeclaredConstructor().newInstance());
+ f.set(b, newInstanceOf(t));
+ expectedB.put(f.getName(), newInstanceOf(t));
} catch (Exception e) {
// No default constructor, or blithely attempting to construct something doesn't
// work for some reason. If the default value isn't null, then keep it.
@@ -321,4 +343,34 @@
}
}
}
+
+ private static Object newInstanceOf(Class<?> clazz) throws ReflectiveOperationException {
+ try {
+ Constructor<?> defaultConstructor = clazz.getDeclaredConstructor();
+ return defaultConstructor.newInstance();
+ } catch (Exception e) {
+ // No default constructor, continue below.
+ }
+
+ // Look for a suitable builder.
+ Optional<Class<?>> clazzBuilder =
+ Arrays.stream(clazz.getDeclaredClasses())
+ .filter(maybeBuilder -> maybeBuilder.getSimpleName().equals("Builder"))
+ .filter(maybeBuilder ->
+ Arrays.stream(maybeBuilder.getMethods()).anyMatch(
+ m -> m.getName().equals("build")
+ && m.getParameterCount() == 0
+ && m.getReturnType().equals(clazz)))
+ .findFirst();
+ if (clazzBuilder.isPresent()) {
+ Object builder = newInstanceOf(clazzBuilder.get());
+ Method buildMethod = builder.getClass().getMethod("build");
+ Object built = buildMethod.invoke(builder);
+ assertThat(built).isInstanceOf(clazz);
+ return built;
+ }
+
+ throw new ReflectiveOperationException(
+ "Sorry! Couldn't figure out how to create an instance of " + clazz.getName());
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 5b88c8c..c6fa8a1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -643,6 +643,21 @@
}
@Test
+ public void testDisplayHasContent() {
+ final WindowState window = createWindow(null, TYPE_APPLICATION_OVERLAY, "window");
+ setDrawnState(WindowStateAnimator.COMMIT_DRAW_PENDING, window);
+ assertFalse(mDisplayContent.getLastHasContent());
+ // The pending draw state should be committed and the has-content state is also updated.
+ mDisplayContent.applySurfaceChangesTransaction();
+ assertTrue(window.isDrawn());
+ assertTrue(mDisplayContent.getLastHasContent());
+ // If the only window is no longer visible, has-content will be false.
+ setDrawnState(WindowStateAnimator.NO_SURFACE, window);
+ mDisplayContent.applySurfaceChangesTransaction();
+ assertFalse(mDisplayContent.getLastHasContent());
+ }
+
+ @Test
public void testImeIsAttachedToDisplayForLetterboxedApp() {
final DisplayContent dc = mDisplayContent;
final WindowState ws = createWindow(null, TYPE_APPLICATION, dc, "app window");
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index d08ab51..f99b489 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -273,7 +273,6 @@
assertFalse(win.mHasSurface);
assertNull(win.mWinAnimator.mSurfaceController);
- doReturn(mSystemServicesTestRule.mTransaction).when(SurfaceControl::getGlobalTransaction);
// Invisible requested activity should not get the last config even if its view is visible.
mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.VISIBLE, 0, 0, 0,
outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle);
diff --git a/telephony/java/android/service/euicc/EuiccProfileInfo.java b/telephony/java/android/service/euicc/EuiccProfileInfo.java
index f7c8237..cc8a992 100644
--- a/telephony/java/android/service/euicc/EuiccProfileInfo.java
+++ b/telephony/java/android/service/euicc/EuiccProfileInfo.java
@@ -43,7 +43,11 @@
@SystemApi
public final class EuiccProfileInfo implements Parcelable {
- /** Profile policy rules (bit mask) */
+ /**
+ * Profile policy rules (bit mask)
+ *
+ * @removed mistakenly exposed previously
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = { "POLICY_RULE_" }, value = {
POLICY_RULE_DO_NOT_DISABLE,
@@ -58,7 +62,11 @@
/** This profile should be deleted after being disabled. */
public static final int POLICY_RULE_DELETE_AFTER_DISABLING = 1 << 2;
- /** Class of the profile */
+ /**
+ * Class of the profile
+ *
+ * @removed mistakenly exposed previously
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "PROFILE_CLASS_" }, value = {
PROFILE_CLASS_TESTING,
@@ -79,7 +87,11 @@
*/
public static final int PROFILE_CLASS_UNSET = -1;
- /** State of the profile */
+ /**
+ * State of the profile
+ *
+ * @removed mistakenly exposed previously
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "PROFILE_STATE_" }, value = {
PROFILE_STATE_DISABLED,
diff --git a/telephony/java/android/telephony/euicc/EuiccCardManager.java b/telephony/java/android/telephony/euicc/EuiccCardManager.java
index 611f97b..e981e1f 100644
--- a/telephony/java/android/telephony/euicc/EuiccCardManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccCardManager.java
@@ -67,7 +67,11 @@
public class EuiccCardManager {
private static final String TAG = "EuiccCardManager";
- /** Reason for canceling a profile download session */
+ /**
+ * Reason for canceling a profile download session
+ *
+ * @removed mistakenly exposed previously
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"CANCEL_REASON_"}, value = {
CANCEL_REASON_END_USER_REJECTED,
@@ -97,7 +101,11 @@
*/
public static final int CANCEL_REASON_PPR_NOT_ALLOWED = 3;
- /** Options for resetting eUICC memory */
+ /**
+ * Options for resetting eUICC memory
+ *
+ * @removed mistakenly exposed previously
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = {"RESET_OPTION_"}, value = {
RESET_OPTION_DELETE_OPERATIONAL_PROFILES,
diff --git a/telephony/java/android/telephony/euicc/EuiccManager.java b/telephony/java/android/telephony/euicc/EuiccManager.java
index b9a7d43..86fbb04 100644
--- a/telephony/java/android/telephony/euicc/EuiccManager.java
+++ b/telephony/java/android/telephony/euicc/EuiccManager.java
@@ -552,9 +552,8 @@
/**
* Euicc OTA update status which can be got by {@link #getOtaStatus}
- * @hide
+ * @removed mistakenly exposed as system-api previously
*/
- @SystemApi
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"EUICC_OTA_"}, value = {
EUICC_OTA_IN_PROGRESS,
diff --git a/telephony/java/android/telephony/euicc/EuiccNotification.java b/telephony/java/android/telephony/euicc/EuiccNotification.java
index be0048f..fcc0b6a 100644
--- a/telephony/java/android/telephony/euicc/EuiccNotification.java
+++ b/telephony/java/android/telephony/euicc/EuiccNotification.java
@@ -36,7 +36,11 @@
*/
@SystemApi
public final class EuiccNotification implements Parcelable {
- /** Event */
+ /**
+ * Event
+ *
+ * @removed mistakenly exposed previously
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = { "EVENT_" }, value = {
EVENT_INSTALL,
diff --git a/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java b/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java
index 1c6b6b6..c35242d 100644
--- a/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java
+++ b/telephony/java/android/telephony/euicc/EuiccRulesAuthTable.java
@@ -37,7 +37,11 @@
*/
@SystemApi
public final class EuiccRulesAuthTable implements Parcelable {
- /** Profile policy rule flags */
+ /**
+ * Profile policy rule flags
+ *
+ * @removed mistakenly exposed previously
+ */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, prefix = { "POLICY_RULE_FLAG_" }, value = {
POLICY_RULE_FLAG_CONSENT_REQUIRED
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 93a5582..c1784f3 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -149,7 +149,9 @@
verify(native).setMotionClassifierEnabled(anyBoolean())
verify(native).setMaximumObscuringOpacityForTouch(anyFloat())
verify(native).setStylusPointerIconEnabled(anyBoolean())
- verify(native).setKeyRepeatConfiguration(anyInt(), anyInt())
+ // Called twice at boot, since there are individual callbacks to update the
+ // key repeat timeout and the key repeat delay.
+ verify(native, times(2)).setKeyRepeatConfiguration(anyInt(), anyInt())
}
@Test
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
index 1bcf364..d7aa0af 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt
@@ -119,8 +119,12 @@
* Write bytecode to push all the method arguments to the stack.
* The number of arguments and their type are taken from [methodDescriptor].
*/
-fun writeByteCodeToPushArguments(methodDescriptor: String, writer: MethodVisitor) {
- var i = -1
+fun writeByteCodeToPushArguments(
+ methodDescriptor: String,
+ writer: MethodVisitor,
+ argOffset: Int = 0,
+ ) {
+ var i = argOffset - 1
Type.getArgumentTypes(methodDescriptor).forEach { type ->
i++
@@ -159,6 +163,18 @@
}
/**
+ * Given a method descriptor, insert an [argType] as the first argument to it.
+ */
+fun prependArgTypeToMethodDescriptor(methodDescriptor: String, argType: Type): String {
+ val returnType = Type.getReturnType(methodDescriptor)
+ val argTypes = Type.getArgumentTypes(methodDescriptor).toMutableList()
+
+ argTypes.add(0, argType)
+
+ return Type.getMethodDescriptor(returnType, *argTypes.toTypedArray())
+}
+
+/**
* Return the "visibility" modifier from an `access` integer.
*
* (see https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.1-200-E.1)
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
index e63efd0..88db15b 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/visitors/ImplGeneratingAdapter.kt
@@ -19,6 +19,7 @@
import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.isVisibilityPrivateOrPackagePrivate
+import com.android.hoststubgen.asm.prependArgTypeToMethodDescriptor
import com.android.hoststubgen.asm.writeByteCodeToPushArguments
import com.android.hoststubgen.asm.writeByteCodeToReturn
import com.android.hoststubgen.filters.FilterPolicy
@@ -285,7 +286,7 @@
* class.
*/
private inner class NativeSubstitutingMethodAdapter(
- access: Int,
+ val access: Int,
private val name: String,
private val descriptor: String,
signature: String?,
@@ -300,12 +301,33 @@
}
override fun visitEnd() {
- writeByteCodeToPushArguments(descriptor, this)
+ var targetDescriptor = descriptor
+ var argOffset = 0
+
+ // For non-static native method, we need to tweak it a bit.
+ if ((access and Opcodes.ACC_STATIC) == 0) {
+ // Push `this` as the first argument.
+ this.visitVarInsn(Opcodes.ALOAD, 0)
+
+ // Update the descriptor -- add this class's type as the first argument
+ // to the method descriptor.
+ val thisType = Type.getType("L" + currentClassName + ";")
+
+ targetDescriptor = prependArgTypeToMethodDescriptor(
+ descriptor,
+ thisType,
+ )
+
+ // Shift the original arguments by one.
+ argOffset = 1
+ }
+
+ writeByteCodeToPushArguments(descriptor, this, argOffset)
visitMethodInsn(Opcodes.INVOKESTATIC,
nativeSubstitutionClass,
name,
- descriptor,
+ targetDescriptor,
false)
writeByteCodeToReturn(descriptor, this)
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
index 3474ae4..673d3e8 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/01-hoststubgen-test-tiny-framework-orig-dump.txt
@@ -1718,7 +1718,11 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 5, attributes: 2
+ interfaces: 0, fields: 1, methods: 8, attributes: 2
+ int value;
+ descriptor: I
+ flags: (0x0000)
+
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -1767,6 +1771,40 @@
Start Length Slot Name Signature
0 6 0 arg1 J
0 6 2 arg2 J
+
+ public void setValue(int);
+ descriptor: (I)V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=2, args_size=2
+ x: aload_0
+ x: iload_1
+ x: putfield #x // Field value:I
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 6 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 0 6 1 v I
+
+ public native int nativeNonStaticAddToValue(int);
+ descriptor: (I)I
+ flags: (0x0101) ACC_PUBLIC, ACC_NATIVE
+
+ public int nativeNonStaticAddToValue_should_be_like_this(int);
+ descriptor: (I)I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=2, args_size=2
+ x: aload_0
+ x: iload_1
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeNonStaticAddToValue:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: ireturn
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 6 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 0 6 1 arg I
}
SourceFile: "TinyFrameworkNative.java"
RuntimeInvisibleAnnotations:
@@ -1782,9 +1820,9 @@
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
- this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+ this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 3, attributes: 2
+ interfaces: 0, fields: 0, methods: 4, attributes: 2
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -1826,6 +1864,22 @@
Start Length Slot Name Signature
0 4 0 arg1 J
0 4 2 arg2 J
+
+ public static int nativeNonStaticAddToValue(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative, int);
+ descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=2, locals=2, args_size=2
+ x: aload_0
+ x: getfield #x // Field com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.value:I
+ x: iload_1
+ x: iadd
+ x: ireturn
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 7 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 0 7 1 arg I
}
SourceFile: "TinyFrameworkNative_host.java"
RuntimeInvisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
index a1aae8a..d12588a 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/02-hoststubgen-test-tiny-framework-host-stub-dump.txt
@@ -1039,7 +1039,11 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 5, attributes: 3
+ interfaces: 0, fields: 1, methods: 8, attributes: 3
+ int value;
+ descriptor: I
+ flags: (0x0000)
+
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -1080,6 +1084,32 @@
x: ldc #x // String Stub!
x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
x: athrow
+
+ public void setValue(int);
+ descriptor: (I)V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=3, locals=2, args_size=2
+ x: new #x // class java/lang/RuntimeException
+ x: dup
+ x: ldc #x // String Stub!
+ x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+ x: athrow
+
+ public native int nativeNonStaticAddToValue(int);
+ descriptor: (I)I
+ flags: (0x0101) ACC_PUBLIC, ACC_NATIVE
+
+ public int nativeNonStaticAddToValue_should_be_like_this(int);
+ descriptor: (I)I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=3, locals=2, args_size=2
+ x: new #x // class java/lang/RuntimeException
+ x: dup
+ x: ldc #x // String Stub!
+ x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+ x: athrow
}
SourceFile: "TinyFrameworkNative.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
index 29626f2..97fb64f 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/03-hoststubgen-test-tiny-framework-host-impl-dump.txt
@@ -1650,7 +1650,11 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 5, attributes: 3
+ interfaces: 0, fields: 1, methods: 8, attributes: 3
+ int value;
+ descriptor: I
+ flags: (0x0000)
+
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -1710,6 +1714,46 @@
Start Length Slot Name Signature
0 6 0 arg1 J
0 6 2 arg2 J
+
+ public void setValue(int);
+ descriptor: (I)V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=2, args_size=2
+ x: aload_0
+ x: iload_1
+ x: putfield #x // Field value:I
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 6 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 0 6 1 v I
+
+ public int nativeNonStaticAddToValue(int);
+ descriptor: (I)I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=2, args_size=2
+ x: aload_0
+ x: iload_1
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeNonStaticAddToValue:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: ireturn
+
+ public int nativeNonStaticAddToValue_should_be_like_this(int);
+ descriptor: (I)I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=2, args_size=2
+ x: aload_0
+ x: iload_1
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeNonStaticAddToValue:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: ireturn
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 0 6 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 0 6 1 arg I
}
SourceFile: "TinyFrameworkNative.java"
RuntimeVisibleAnnotations:
@@ -1732,7 +1776,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 3, attributes: 3
+ interfaces: 0, fields: 0, methods: 4, attributes: 3
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative_host();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -1792,6 +1836,28 @@
Start Length Slot Name Signature
15 4 0 arg1 J
15 4 2 arg2 J
+
+ public static int nativeNonStaticAddToValue(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative, int);
+ descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=4, locals=2, args_size=2
+ x: ldc #x // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+ x: ldc #x // String nativeNonStaticAddToValue
+ x: ldc #x // String (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+ x: invokevirtual #x // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+ x: aload_0
+ x: getfield #x // Field com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.value:I
+ x: iload_1
+ x: iadd
+ x: ireturn
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 15 7 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 15 7 1 arg I
}
SourceFile: "TinyFrameworkNative_host.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
index a1aae8a..d12588a 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/12-hoststubgen-test-tiny-framework-host-ext-stub-dump.txt
@@ -1039,7 +1039,11 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 5, attributes: 3
+ interfaces: 0, fields: 1, methods: 8, attributes: 3
+ int value;
+ descriptor: I
+ flags: (0x0000)
+
public com.android.hoststubgen.test.tinyframework.TinyFrameworkNative();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
@@ -1080,6 +1084,32 @@
x: ldc #x // String Stub!
x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
x: athrow
+
+ public void setValue(int);
+ descriptor: (I)V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=3, locals=2, args_size=2
+ x: new #x // class java/lang/RuntimeException
+ x: dup
+ x: ldc #x // String Stub!
+ x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+ x: athrow
+
+ public native int nativeNonStaticAddToValue(int);
+ descriptor: (I)I
+ flags: (0x0101) ACC_PUBLIC, ACC_NATIVE
+
+ public int nativeNonStaticAddToValue_should_be_like_this(int);
+ descriptor: (I)I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=3, locals=2, args_size=2
+ x: new #x // class java/lang/RuntimeException
+ x: dup
+ x: ldc #x // String Stub!
+ x: invokespecial #x // Method java/lang/RuntimeException."<init>":(Ljava/lang/String;)V
+ x: athrow
}
SourceFile: "TinyFrameworkNative.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
index ed7e7d3..8035189 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/golden-output/13-hoststubgen-test-tiny-framework-host-ext-impl-dump.txt
@@ -2108,7 +2108,11 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 6, attributes: 3
+ interfaces: 0, fields: 1, methods: 9, attributes: 3
+ int value;
+ descriptor: I
+ flags: (0x0000)
+
private static {};
descriptor: ()V
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -2193,6 +2197,56 @@
Start Length Slot Name Signature
11 6 0 arg1 J
11 6 2 arg2 J
+
+ public void setValue(int);
+ descriptor: (I)V
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=4, locals=2, args_size=2
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
+ x: ldc #x // String setValue
+ x: ldc #x // String (I)V
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: aload_0
+ x: iload_1
+ x: putfield #x // Field value:I
+ x: return
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 11 6 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 11 6 1 v I
+
+ public int nativeNonStaticAddToValue(int);
+ descriptor: (I)I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=2, locals=2, args_size=2
+ x: aload_0
+ x: iload_1
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeNonStaticAddToValue:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: ireturn
+
+ public int nativeNonStaticAddToValue_should_be_like_this(int);
+ descriptor: (I)I
+ flags: (0x0001) ACC_PUBLIC
+ Code:
+ stack=4, locals=2, args_size=2
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative
+ x: ldc #x // String nativeNonStaticAddToValue_should_be_like_this
+ x: ldc #x // String (I)I
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: aload_0
+ x: iload_1
+ x: invokestatic #x // Method com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.nativeNonStaticAddToValue:(Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: ireturn
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 11 6 0 this Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 11 6 1 arg I
}
SourceFile: "TinyFrameworkNative.java"
RuntimeVisibleAnnotations:
@@ -2215,7 +2269,7 @@
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #x // com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
super_class: #x // java/lang/Object
- interfaces: 0, fields: 0, methods: 4, attributes: 3
+ interfaces: 0, fields: 0, methods: 5, attributes: 3
private static {};
descriptor: ()V
flags: (0x000a) ACC_PRIVATE, ACC_STATIC
@@ -2300,6 +2354,33 @@
Start Length Slot Name Signature
26 4 0 arg1 J
26 4 2 arg2 J
+
+ public static int nativeNonStaticAddToValue(com.android.hoststubgen.test.tinyframework.TinyFrameworkNative, int);
+ descriptor: (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ flags: (0x0009) ACC_PUBLIC, ACC_STATIC
+ Code:
+ stack=4, locals=2, args_size=2
+ x: ldc #x // class com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+ x: ldc #x // String nativeNonStaticAddToValue
+ x: ldc #x // String (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: ldc #x // String com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.callMethodCallHook:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
+ x: ldc #x // String com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host
+ x: ldc #x // String nativeNonStaticAddToValue
+ x: ldc #x // String (Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;I)I
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.getStackWalker:()Ljava/lang/StackWalker;
+ x: invokevirtual #x // Method java/lang/StackWalker.getCallerClass:()Ljava/lang/Class;
+ x: invokestatic #x // Method com/android/hoststubgen/hosthelper/HostTestUtils.onNonStubMethodCalled:(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Class;)V
+ x: aload_0
+ x: getfield #x // Field com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.value:I
+ x: iload_1
+ x: iadd
+ x: ireturn
+ LineNumberTable:
+ LocalVariableTable:
+ Start Length Slot Name Signature
+ 26 7 0 source Lcom/android/hoststubgen/test/tinyframework/TinyFrameworkNative;
+ 26 7 1 arg I
}
SourceFile: "TinyFrameworkNative_host.java"
RuntimeVisibleAnnotations:
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
index c151dcc..e7b5d9f 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative.java
@@ -32,4 +32,16 @@
public static long nativeLongPlus_should_be_like_this(long arg1, long arg2) {
return TinyFrameworkNative_host.nativeLongPlus(arg1, arg2);
}
+
+ int value;
+
+ public void setValue(int v) {
+ this.value = v;
+ }
+
+ public native int nativeNonStaticAddToValue(int arg);
+
+ public int nativeNonStaticAddToValue_should_be_like_this(int arg) {
+ return TinyFrameworkNative_host.nativeNonStaticAddToValue(this, arg);
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
index 48f7dea..749ebaa 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-framework/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkNative_host.java
@@ -28,4 +28,10 @@
public static long nativeLongPlus(long arg1, long arg2) {
return arg1 + arg2;
}
+
+ // Note, the method must be static even for a non-static native method, but instead it
+ // must take the "source" instance as the first argument.
+ public static int nativeNonStaticAddToValue(TinyFrameworkNative source, int arg) {
+ return source.value + arg;
+ }
}
diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
index b015661..d04ca52 100644
--- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
+++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java
@@ -152,6 +152,13 @@
}
@Test
+ public void testNativeSubstitutionClass_nonStatic() {
+ TinyFrameworkNative instance = new TinyFrameworkNative();
+ instance.setValue(5);
+ assertThat(instance.nativeNonStaticAddToValue(3)).isEqualTo(8);
+ }
+
+ @Test
public void testExitLog() {
thrown.expect(RuntimeException.class);
thrown.expectMessage("Outer exception");