diff options
462 files changed, 17675 insertions, 3913 deletions
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 3bbbb15ee32e..e0fffb4c152e 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -924,7 +924,8 @@ public class JobInfo implements Parcelable { @SuppressWarnings("UnsafeParcelApi") private JobInfo(Parcel in) { jobId = in.readInt(); - extras = in.readPersistableBundle(); + final PersistableBundle persistableExtras = in.readPersistableBundle(); + extras = persistableExtras != null ? persistableExtras : PersistableBundle.EMPTY; transientExtras = in.readBundle(); if (in.readInt() != 0) { clipData = ClipData.CREATOR.createFromParcel(in); diff --git a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java index 37642497f29e..33668c76c152 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobScheduler.java @@ -337,7 +337,7 @@ public abstract class JobScheduler { * but there are situations where it may get this wrong and count the JobInfo as changing. * (That said, you should be relatively safe with a simple set of consistent data in these * fields.) You should never use {@link JobInfo.Builder#setClipData(ClipData, int)} with - * work you are enqueue, since currently this will always be treated as a different JobInfo, + * work you are enqueuing, since currently this will always be treated as a different JobInfo, * even if the ClipData contents are exactly the same.</p> * * <p class="caution"><strong>Note:</strong> Scheduling a job can have a high cost, even if it's @@ -345,6 +345,16 @@ public abstract class JobScheduler { * version {@link android.os.Build.VERSION_CODES#Q}. As such, the system may throttle calls to * this API if calls are made too frequently in a short amount of time. * + * <p class="caution"><strong>Note:</strong> Prior to Android version + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems could not be persisted. + * Apps were not allowed to enqueue JobWorkItems with persisted jobs and the system would throw + * an {@link IllegalArgumentException} if they attempted to do so. Starting with + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * JobWorkItems can be persisted alongside the hosting job. + * However, Intents cannot be persisted. Set a {@link PersistableBundle} using + * {@link JobWorkItem.Builder#setExtras(PersistableBundle)} for any information that needs + * to be persisted. + * * <p>Note: The JobService component needs to be enabled in order to successfully schedule a * job. * diff --git a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java index 32945e00c8e3..18167e2a67dc 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobWorkItem.java @@ -19,20 +19,33 @@ package android.app.job; import static android.app.job.JobInfo.NETWORK_BYTES_UNKNOWN; import android.annotation.BytesLong; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.compat.Compatibility; import android.compat.annotation.UnsupportedAppUsage; import android.content.Intent; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; /** * A unit of work that can be enqueued for a job using * {@link JobScheduler#enqueue JobScheduler.enqueue}. See * {@link JobParameters#dequeueWork() JobParameters.dequeueWork} for more details. + * + * <p class="caution"><strong>Note:</strong> Prior to Android version + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems could not be persisted. + * Apps were not allowed to enqueue JobWorkItems with persisted jobs and the system would throw + * an {@link IllegalArgumentException} if they attempted to do so. Starting with + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, JobWorkItems can be persisted alongside + * the hosting job. However, Intents cannot be persisted. Set a {@link PersistableBundle} using + * {@link Builder#setExtras(PersistableBundle)} for any information that needs to be persisted. */ final public class JobWorkItem implements Parcelable { + @NonNull + private final PersistableBundle mExtras; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) final Intent mIntent; private final long mNetworkDownloadBytes; @@ -49,6 +62,10 @@ final public class JobWorkItem implements Parcelable { * Create a new piece of work, which can be submitted to * {@link JobScheduler#enqueue JobScheduler.enqueue}. * + * <p> + * Intents cannot be used for persisted JobWorkItems. + * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems. + * * @param intent The general Intent describing this work. */ public JobWorkItem(Intent intent) { @@ -62,6 +79,10 @@ final public class JobWorkItem implements Parcelable { * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for * details about how to estimate network traffic. * + * <p> + * Intents cannot be used for persisted JobWorkItems. + * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems. + * * @param intent The general Intent describing this work. * @param downloadBytes The estimated size of network traffic that will be * downloaded by this job work item, in bytes. @@ -79,6 +100,10 @@ final public class JobWorkItem implements Parcelable { * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for * details about how to estimate network traffic. * + * <p> + * Intents cannot be used for persisted JobWorkItems. + * Use {@link Builder#setExtras(PersistableBundle)} instead for persisted JobWorkItems. + * * @param intent The general Intent describing this work. * @param downloadBytes The estimated size of network traffic that will be * downloaded by this job work item, in bytes. @@ -89,6 +114,7 @@ final public class JobWorkItem implements Parcelable { */ public JobWorkItem(@Nullable Intent intent, @BytesLong long downloadBytes, @BytesLong long uploadBytes, @BytesLong long minimumChunkBytes) { + mExtras = PersistableBundle.EMPTY; mIntent = intent; mNetworkDownloadBytes = downloadBytes; mNetworkUploadBytes = uploadBytes; @@ -96,6 +122,25 @@ final public class JobWorkItem implements Parcelable { enforceValidity(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES)); } + private JobWorkItem(@NonNull Builder builder) { + mDeliveryCount = builder.mDeliveryCount; + mExtras = builder.mExtras.deepCopy(); + mIntent = builder.mIntent; + mNetworkDownloadBytes = builder.mNetworkDownloadBytes; + mNetworkUploadBytes = builder.mNetworkUploadBytes; + mMinimumChunkBytes = builder.mMinimumNetworkChunkBytes; + } + + /** + * Return the extras associated with this work. + * + * @see Builder#setExtras(PersistableBundle) + */ + @NonNull + public PersistableBundle getExtras() { + return mExtras; + } + /** * Return the Intent associated with this work. */ @@ -176,6 +221,7 @@ final public class JobWorkItem implements Parcelable { /** * @hide */ + @Nullable public Object getGrants() { return mGrants; } @@ -186,6 +232,8 @@ final public class JobWorkItem implements Parcelable { sb.append(mWorkId); sb.append(" intent="); sb.append(mIntent); + sb.append(" extras="); + sb.append(mExtras); if (mNetworkDownloadBytes != NETWORK_BYTES_UNKNOWN) { sb.append(" downloadBytes="); sb.append(mNetworkDownloadBytes); @@ -207,6 +255,140 @@ final public class JobWorkItem implements Parcelable { } /** + * Builder class for constructing {@link JobWorkItem} objects. + */ + public static final class Builder { + private int mDeliveryCount; + private PersistableBundle mExtras = PersistableBundle.EMPTY; + private Intent mIntent; + private long mNetworkDownloadBytes = NETWORK_BYTES_UNKNOWN; + private long mNetworkUploadBytes = NETWORK_BYTES_UNKNOWN; + private long mMinimumNetworkChunkBytes = NETWORK_BYTES_UNKNOWN; + + /** + * Initialize a new Builder to construct a {@link JobWorkItem} object. + */ + public Builder() { + } + + /** + * @see JobWorkItem#getDeliveryCount() + * @return This object for method chaining + * @hide + */ + @NonNull + public Builder setDeliveryCount(int deliveryCount) { + mDeliveryCount = deliveryCount; + return this; + } + + /** + * Set optional extras. This can be persisted, so we only allow primitive types. + * @param extras Bundle containing extras you want the scheduler to hold on to for you. + * @return This object for method chaining + * @see JobWorkItem#getExtras() + */ + @NonNull + public Builder setExtras(@NonNull PersistableBundle extras) { + if (extras == null) { + throw new IllegalArgumentException("extras cannot be null"); + } + mExtras = extras; + return this; + } + + /** + * Set an intent with information relevant to this work item. + * + * <p> + * Intents cannot be used for persisted JobWorkItems. + * Use {@link #setExtras(PersistableBundle)} instead for persisted JobWorkItems. + * + * @return This object for method chaining + * @see JobWorkItem#getIntent() + */ + @NonNull + public Builder setIntent(@NonNull Intent intent) { + mIntent = intent; + return this; + } + + /** + * Set the estimated size of network traffic that will be performed for this work item, + * in bytes. + * + * See {@link JobInfo.Builder#setEstimatedNetworkBytes(long, long)} for + * details about how to estimate network traffic. + * + * @param downloadBytes The estimated size of network traffic that will be + * downloaded for this work item, in bytes. + * @param uploadBytes The estimated size of network traffic that will be + * uploaded for this work item, in bytes. + * @return This object for method chaining + * @see JobInfo.Builder#setEstimatedNetworkBytes(long, long) + * @see JobWorkItem#getEstimatedNetworkDownloadBytes() + * @see JobWorkItem#getEstimatedNetworkUploadBytes() + */ + @NonNull + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setEstimatedNetworkBytes(@BytesLong long downloadBytes, + @BytesLong long uploadBytes) { + if (downloadBytes != NETWORK_BYTES_UNKNOWN && downloadBytes < 0) { + throw new IllegalArgumentException( + "Invalid network download bytes: " + downloadBytes); + } + if (uploadBytes != NETWORK_BYTES_UNKNOWN && uploadBytes < 0) { + throw new IllegalArgumentException("Invalid network upload bytes: " + uploadBytes); + } + mNetworkDownloadBytes = downloadBytes; + mNetworkUploadBytes = uploadBytes; + return this; + } + + /** + * Set the minimum size of non-resumable network traffic this work item requires, in bytes. + * When the upload or download can be easily paused and resumed, use this to set the + * smallest size that must be transmitted between start and stop events to be considered + * successful. If the transfer cannot be paused and resumed, then this should be the sum + * of the values provided to {@link #setEstimatedNetworkBytes(long, long)}. + * + * See {@link JobInfo.Builder#setMinimumNetworkChunkBytes(long)} for + * details about how to set the minimum chunk. + * + * @param chunkSizeBytes The smallest piece of data that cannot be easily paused and + * resumed, in bytes. + * @return This object for method chaining + * @see JobInfo.Builder#setMinimumNetworkChunkBytes(long) + * @see JobWorkItem#getMinimumNetworkChunkBytes() + * @see JobWorkItem#JobWorkItem(android.content.Intent, long, long, long) + */ + @NonNull + public Builder setMinimumNetworkChunkBytes(@BytesLong long chunkSizeBytes) { + if (chunkSizeBytes != NETWORK_BYTES_UNKNOWN && chunkSizeBytes <= 0) { + throw new IllegalArgumentException("Minimum chunk size must be positive"); + } + mMinimumNetworkChunkBytes = chunkSizeBytes; + return this; + } + + /** + * @return The JobWorkItem object to hand to the JobScheduler. This object is immutable. + */ + @NonNull + public JobWorkItem build() { + return build(Compatibility.isChangeEnabled(JobInfo.REJECT_NEGATIVE_NETWORK_ESTIMATES)); + } + + /** @hide */ + @NonNull + public JobWorkItem build(boolean rejectNegativeNetworkEstimates) { + JobWorkItem jobWorkItem = new JobWorkItem(this); + jobWorkItem.enforceValidity(rejectNegativeNetworkEstimates); + return jobWorkItem; + } + } + + /** * @hide */ public void enforceValidity(boolean rejectNegativeNetworkEstimates) { @@ -249,6 +431,7 @@ final public class JobWorkItem implements Parcelable { } else { out.writeInt(0); } + out.writePersistableBundle(mExtras); out.writeLong(mNetworkDownloadBytes); out.writeLong(mNetworkUploadBytes); out.writeLong(mMinimumChunkBytes); @@ -274,6 +457,8 @@ final public class JobWorkItem implements Parcelable { } else { mIntent = null; } + final PersistableBundle extras = in.readPersistableBundle(); + mExtras = extras != null ? extras : PersistableBundle.EMPTY; mNetworkDownloadBytes = in.readLong(); mNetworkUploadBytes = in.readLong(); mMinimumChunkBytes = in.readLong(); diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index e2d302f46187..0fa5764354e8 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -318,6 +318,10 @@ public class DeviceIdleController extends SystemService private Bundle mIdleIntentOptions; private Intent mLightIdleIntent; private Bundle mLightIdleIntentOptions; + private Intent mPowerSaveWhitelistChangedIntent; + private Bundle mPowerSaveWhitelistChangedOptions; + private Intent mPowerSaveTempWhitelistChangedIntent; + private Bundle mPowerSaveTempWhilelistChangedOptions; private AnyMotionDetector mAnyMotionDetector; private final AppStateTrackerImpl mAppStateTracker; @GuardedBy("this") @@ -2532,15 +2536,26 @@ public class DeviceIdleController extends SystemService mAppStateTracker.onSystemServicesReady(); + final Bundle mostRecentDeliveryOptions = BroadcastOptions.makeBasic() + .setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT) + .toBundle(); + mIdleIntent = new Intent(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); mIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); mLightIdleIntent = new Intent(PowerManager.ACTION_LIGHT_DEVICE_IDLE_MODE_CHANGED); mLightIdleIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND); - final BroadcastOptions options = BroadcastOptions.makeBasic(); - options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); - mIdleIntentOptions = mLightIdleIntentOptions = options.toBundle(); + mIdleIntentOptions = mLightIdleIntentOptions = mostRecentDeliveryOptions; + + mPowerSaveWhitelistChangedIntent = new Intent( + PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + mPowerSaveWhitelistChangedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mPowerSaveTempWhitelistChangedIntent = new Intent( + PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED); + mPowerSaveTempWhitelistChangedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + mPowerSaveWhitelistChangedOptions = mostRecentDeliveryOptions; + mPowerSaveTempWhilelistChangedOptions = mostRecentDeliveryOptions; IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_BATTERY_CHANGED); @@ -4350,17 +4365,17 @@ public class DeviceIdleController extends SystemService } private void reportPowerSaveWhitelistChangedLocked() { - Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM); + getContext().sendBroadcastAsUser(mPowerSaveWhitelistChangedIntent, UserHandle.SYSTEM, + null /* receiverPermission */, + mPowerSaveWhitelistChangedOptions); } private void reportTempWhitelistChangedLocked(final int uid, final boolean added) { mHandler.obtainMessage(MSG_REPORT_TEMP_APP_WHITELIST_CHANGED, uid, added ? 1 : 0) .sendToTarget(); - Intent intent = new Intent(PowerManager.ACTION_POWER_SAVE_TEMP_WHITELIST_CHANGED); - intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - getContext().sendBroadcastAsUser(intent, UserHandle.SYSTEM); + getContext().sendBroadcastAsUser(mPowerSaveTempWhitelistChangedIntent, UserHandle.SYSTEM, + null /* receiverPermission */, + mPowerSaveTempWhilelistChangedOptions); } private void passWhiteListsToForceAppStandbyTrackerLocked() { diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index c7a2997d8fcc..8defa162b96e 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -1406,6 +1406,7 @@ public class JobSchedulerService extends com.android.server.SystemService if (toCancel.getJob().equals(job)) { toCancel.enqueueWorkLocked(work); + mJobs.touchJob(toCancel); // If any of work item is enqueued when the source is in the foreground, // exempt the entire job. @@ -3775,6 +3776,14 @@ public class JobSchedulerService extends com.android.server.SystemService } } } + if (job.isPersisted()) { + // Intent.saveToXml() doesn't persist everything, so just reject all + // JobWorkItems with Intents to be safe/predictable. + if (jobWorkItem.getIntent() != null) { + throw new IllegalArgumentException( + "Cannot persist JobWorkItems with Intents"); + } + } } return JobScheduler.RESULT_SUCCESS; } @@ -3837,9 +3846,6 @@ public class JobSchedulerService extends com.android.server.SystemService final int userId = UserHandle.getUserId(uid); enforceValidJobRequest(uid, job); - if (job.isPersisted()) { - throw new IllegalArgumentException("Can't enqueue work for persisted jobs"); - } if (work == null) { throw new NullPointerException("work is null"); } 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 285b9825d25b..ce7da8607497 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -615,6 +615,9 @@ public final class JobServiceContext implements ServiceConnection { "last work dequeued"); // This will finish the job. doCallbackLocked(false, "last work dequeued"); + } else { + // Delivery count has been updated, so persist JobWorkItem change. + mService.mJobs.touchJob(mRunningJob); } return work; } @@ -632,6 +635,7 @@ public final class JobServiceContext implements ServiceConnection { // Exception-throwing-can down the road to JobParameters.completeWork >:( return true; } + mService.mJobs.touchJob(mRunningJob); return mRunningJob.completeWorkLocked(workId); } } finally { 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 5f5f447933f5..882704949707 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobStore.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobStore.java @@ -25,6 +25,7 @@ import static com.android.server.job.JobSchedulerService.sSystemClock; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.JobInfo; +import android.app.job.JobWorkItem; import android.content.ComponentName; import android.content.Context; import android.net.NetworkRequest; @@ -310,6 +311,15 @@ public final class JobStore { mJobSet.removeJobsOfUnlistedUsers(keepUserIds); } + /** Note a change in the specified JobStatus that necessitates writing job state to disk. */ + void touchJob(@NonNull JobStatus jobStatus) { + if (!jobStatus.isPersisted()) { + return; + } + mPendingJobWriteUids.put(jobStatus.getUid(), true); + maybeWriteStatusToDiskAsync(); + } + @VisibleForTesting public void clear() { mJobSet.clear(); @@ -430,6 +440,7 @@ public final class JobStore { private static final String XML_TAG_PERIODIC = "periodic"; 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 void migrateJobFilesAsync() { synchronized (mLock) { @@ -724,6 +735,7 @@ public final class JobStore { writeConstraintsToXml(out, jobStatus); writeExecutionCriteriaToXml(out, jobStatus); writeBundleToXml(jobStatus.getJob().getExtras(), out); + writeJobWorkItemsToXml(out, jobStatus); out.endTag(null, XML_TAG_JOB); numJobs++; @@ -906,6 +918,53 @@ public final class JobStore { out.endTag(null, XML_TAG_ONEOFF); } } + + 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. + writeJobWorkItemListToXml(out, jobStatus.executingWork); + writeJobWorkItemListToXml(out, jobStatus.pendingWork); + } + + private void writeJobWorkItemListToXml(@NonNull TypedXmlSerializer out, + @Nullable List<JobWorkItem> jobWorkItems) + throws IOException, XmlPullParserException { + if (jobWorkItems == null) { + return; + } + // Write the items in list order to maintain the enqueue order. + final int size = jobWorkItems.size(); + for (int i = 0; i < size; ++i) { + final JobWorkItem item = jobWorkItems.get(i); + if (item.getGrants() != null) { + // We currently don't allow persisting jobs when grants are involved. + // TODO(256618122): allow persisting JobWorkItems with grant flags + continue; + } + if (item.getIntent() != null) { + // Intent.saveToXml() doesn't persist everything, so we shouldn't attempt to + // persist these JobWorkItems at all. + Slog.wtf(TAG, "Encountered JobWorkItem with Intent in persisting list"); + continue; + } + out.startTag(null, XML_TAG_JOB_WORK_ITEM); + out.attributeInt(null, "delivery-count", item.getDeliveryCount()); + if (item.getEstimatedNetworkDownloadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) { + out.attributeLong(null, "estimated-download-bytes", + item.getEstimatedNetworkDownloadBytes()); + } + if (item.getEstimatedNetworkUploadBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) { + out.attributeLong(null, "estimated-upload-bytes", + item.getEstimatedNetworkUploadBytes()); + } + if (item.getMinimumNetworkChunkBytes() != JobInfo.NETWORK_BYTES_UNKNOWN) { + out.attributeLong(null, "minimum-network-chunk-bytes", + item.getMinimumNetworkChunkBytes()); + } + writeBundleToXml(item.getExtras(), out); + out.endTag(null, XML_TAG_JOB_WORK_ITEM); + } + } }; /** @@ -1262,7 +1321,13 @@ public final class JobStore { Slog.e(TAG, "Persisted extras contained invalid data", e); return null; } - parser.nextTag(); // Consume </extras> + eventType = parser.nextTag(); // Consume </extras> + + List<JobWorkItem> jobWorkItems = null; + if (eventType == XmlPullParser.START_TAG + && XML_TAG_JOB_WORK_ITEM.equals(parser.getName())) { + jobWorkItems = readJobWorkItemsFromXml(parser); + } final JobInfo builtJob; try { @@ -1301,6 +1366,11 @@ public final class JobStore { elapsedRuntimes.first, elapsedRuntimes.second, lastSuccessfulRunTime, lastFailedRunTime, (rtcIsGood) ? null : rtcRuntimes, internalFlags, /* dynamicConstraints */ 0); + if (jobWorkItems != null) { + for (int i = 0; i < jobWorkItems.size(); ++i) { + js.enqueueWorkLocked(jobWorkItems.get(i)); + } + } return js; } @@ -1473,6 +1543,64 @@ public final class JobStore { parser.getAttributeLong(null, "deadline", JobStatus.NO_LATEST_RUNTIME); return Pair.create(earliestRunTimeRtc, latestRunTimeRtc); } + + @NonNull + private List<JobWorkItem> readJobWorkItemsFromXml(TypedXmlPullParser parser) + throws IOException, XmlPullParserException { + List<JobWorkItem> jobWorkItems = new ArrayList<>(); + + for (int eventType = parser.getEventType(); eventType != XmlPullParser.END_DOCUMENT; + eventType = parser.next()) { + final String tagName = parser.getName(); + if (!XML_TAG_JOB_WORK_ITEM.equals(tagName)) { + // We're no longer operating with work items. + break; + } + try { + JobWorkItem jwi = readJobWorkItemFromXml(parser); + if (jwi != null) { + jobWorkItems.add(jwi); + } + } catch (Exception e) { + // If there's an issue with one JobWorkItem, drop only the one item and not the + // whole job. + Slog.e(TAG, "Problem with persisted JobWorkItem", e); + } + } + + return jobWorkItems; + } + + @Nullable + private JobWorkItem readJobWorkItemFromXml(TypedXmlPullParser parser) + throws IOException, XmlPullParserException { + JobWorkItem.Builder jwiBuilder = new JobWorkItem.Builder(); + + jwiBuilder + .setDeliveryCount(parser.getAttributeInt(null, "delivery-count")) + .setEstimatedNetworkBytes( + parser.getAttributeLong(null, + "estimated-download-bytes", JobInfo.NETWORK_BYTES_UNKNOWN), + parser.getAttributeLong(null, + "estimated-upload-bytes", JobInfo.NETWORK_BYTES_UNKNOWN)) + .setMinimumNetworkChunkBytes(parser.getAttributeLong(null, + "minimum-network-chunk-bytes", JobInfo.NETWORK_BYTES_UNKNOWN)); + parser.next(); + try { + final PersistableBundle extras = PersistableBundle.restoreFromXml(parser); + jwiBuilder.setExtras(extras); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Persisted extras contained invalid data", e); + return null; + } + + try { + return jwiBuilder.build(); + } catch (Exception e) { + Slog.e(TAG, "Invalid JobWorkItem", e); + return null; + } + } } /** Set of all tracked jobs. */ 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 571259943972..b0b6a01f6281 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 @@ -676,6 +676,12 @@ public final class JobStatus { Slog.i(TAG, "Cloning job with persisted run times", new RuntimeException("here")); } } + if (jobStatus.executingWork != null && jobStatus.executingWork.size() > 0) { + executingWork = new ArrayList<>(jobStatus.executingWork); + } + if (jobStatus.pendingWork != null && jobStatus.pendingWork.size() > 0) { + pendingWork = new ArrayList<>(jobStatus.pendingWork); + } } /** diff --git a/core/api/current.txt b/core/api/current.txt index 5b2b37f33fd4..2f916f1c501c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -935,6 +935,8 @@ package android { field @Deprecated public static final int keyTextSize = 16843316; // 0x1010234 field @Deprecated public static final int keyWidth = 16843325; // 0x101023d field public static final int keyboardLayout = 16843691; // 0x10103ab + field public static final int keyboardLayoutType; + field public static final int keyboardLocale; field @Deprecated public static final int keyboardMode = 16843341; // 0x101024d field public static final int keyboardNavigationCluster = 16844096; // 0x1010540 field public static final int keycode = 16842949; // 0x10100c5 @@ -5650,6 +5652,11 @@ package android.app { field public static final int MODE_UNKNOWN = 0; // 0x0 } + public class GrammaticalInflectionManager { + method public int getApplicationGrammaticalGender(); + method public void setRequestedApplicationGrammaticalGender(int); + } + public class Instrumentation { ctor public Instrumentation(); method public android.os.TestLooperManager acquireLooperManager(android.os.Looper); @@ -8588,12 +8595,22 @@ package android.app.job { method public int getDeliveryCount(); method public long getEstimatedNetworkDownloadBytes(); method public long getEstimatedNetworkUploadBytes(); + method @NonNull public android.os.PersistableBundle getExtras(); method public android.content.Intent getIntent(); method public long getMinimumNetworkChunkBytes(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.job.JobWorkItem> CREATOR; } + public static final class JobWorkItem.Builder { + ctor public JobWorkItem.Builder(); + method @NonNull public android.app.job.JobWorkItem build(); + method @NonNull public android.app.job.JobWorkItem.Builder setEstimatedNetworkBytes(long, long); + method @NonNull public android.app.job.JobWorkItem.Builder setExtras(@NonNull android.os.PersistableBundle); + method @NonNull public android.app.job.JobWorkItem.Builder setIntent(@NonNull android.content.Intent); + method @NonNull public android.app.job.JobWorkItem.Builder setMinimumNetworkChunkBytes(long); + } + } package android.app.people { @@ -10005,6 +10022,7 @@ package android.content { field public static final String FILE_INTEGRITY_SERVICE = "file_integrity"; field public static final String FINGERPRINT_SERVICE = "fingerprint"; field public static final String GAME_SERVICE = "game"; + field public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection"; field public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties"; field public static final String HEALTHCONNECT_SERVICE = "healthconnect"; field public static final String INPUT_METHOD_SERVICE = "input_method"; @@ -11261,6 +11279,7 @@ package android.content.pm { field public static final int CONFIG_DENSITY = 4096; // 0x1000 field public static final int CONFIG_FONT_SCALE = 1073741824; // 0x40000000 field public static final int CONFIG_FONT_WEIGHT_ADJUSTMENT = 268435456; // 0x10000000 + field public static final int CONFIG_GRAMMATICAL_GENDER = 32768; // 0x8000 field public static final int CONFIG_KEYBOARD = 16; // 0x10 field public static final int CONFIG_KEYBOARD_HIDDEN = 32; // 0x20 field public static final int CONFIG_LAYOUT_DIRECTION = 8192; // 0x2000 @@ -11809,6 +11828,7 @@ package android.content.pm { method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback, @NonNull android.os.Handler); method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull String, @NonNull android.content.IntentSender); method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull android.content.pm.VersionedPackage, @NonNull android.content.IntentSender); + method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull android.content.pm.VersionedPackage, int, @NonNull android.content.IntentSender); method @RequiresPermission(android.Manifest.permission.DELETE_PACKAGES) public void uninstallExistingPackage(@NonNull String, @Nullable android.content.IntentSender); method public void unregisterSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback); method public void updateSessionAppIcon(int, @Nullable android.graphics.Bitmap); @@ -11937,6 +11957,8 @@ package android.content.pm { method public int getInstallReason(); method @Nullable public String getInstallerAttributionTag(); method @Nullable public String getInstallerPackageName(); + method public int getInstallerUid(); + method @NonNull public boolean getIsPreApprovalRequested(); method public int getMode(); method public int getOriginatingUid(); method @Nullable public android.net.Uri getOriginatingUri(); @@ -11987,6 +12009,7 @@ package android.content.pm { method public void setInstallLocation(int); method public void setInstallReason(int); method public void setInstallScenario(int); + method public void setInstallerPackageName(@Nullable String); method public void setKeepApplicationEnabledSetting(); method public void setMultiPackage(); method public void setOriginatingUid(int); @@ -12247,6 +12270,7 @@ package android.content.pm { field public static final String FEATURE_IDENTITY_CREDENTIAL_HARDWARE_DIRECT_ACCESS = "android.hardware.identity_credential_direct_access"; field public static final String FEATURE_INPUT_METHODS = "android.software.input_methods"; field public static final String FEATURE_IPSEC_TUNNELS = "android.software.ipsec_tunnels"; + field public static final String FEATURE_IPSEC_TUNNEL_MIGRATION = "android.software.ipsec_tunnel_migration"; field public static final String FEATURE_IRIS = "android.hardware.biometrics.iris"; field public static final String FEATURE_KEYSTORE_APP_ATTEST_KEY = "android.hardware.keystore.app_attest_key"; field public static final String FEATURE_KEYSTORE_LIMITED_USE_KEY = "android.hardware.keystore.limited_use_key"; @@ -12875,6 +12899,7 @@ package android.content.res { method public int diff(android.content.res.Configuration); method public boolean equals(android.content.res.Configuration); method @NonNull public static android.content.res.Configuration generateDelta(@NonNull android.content.res.Configuration, @NonNull android.content.res.Configuration); + method public int getGrammaticalGender(); method public int getLayoutDirection(); method @NonNull public android.os.LocaleList getLocales(); method public boolean isLayoutSizeAtLeast(int); @@ -12904,6 +12929,10 @@ package android.content.res { field @NonNull public static final android.os.Parcelable.Creator<android.content.res.Configuration> CREATOR; field public static final int DENSITY_DPI_UNDEFINED = 0; // 0x0 field public static final int FONT_WEIGHT_ADJUSTMENT_UNDEFINED = 2147483647; // 0x7fffffff + field public static final int GRAMMATICAL_GENDER_FEMININE = 3; // 0x3 + field public static final int GRAMMATICAL_GENDER_MASCULINE = 4; // 0x4 + field public static final int GRAMMATICAL_GENDER_NEUTRAL = 2; // 0x2 + field public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0; // 0x0 field public static final int HARDKEYBOARDHIDDEN_NO = 1; // 0x1 field public static final int HARDKEYBOARDHIDDEN_UNDEFINED = 0; // 0x0 field public static final int HARDKEYBOARDHIDDEN_YES = 2; // 0x2 @@ -14694,6 +14723,7 @@ package android.graphics { method public void drawLine(float, float, float, float, @NonNull android.graphics.Paint); method public void drawLines(@NonNull @Size(multiple=4) float[], int, int, @NonNull android.graphics.Paint); method public void drawLines(@NonNull @Size(multiple=4) float[], @NonNull android.graphics.Paint); + method public void drawMesh(@NonNull android.graphics.Mesh, android.graphics.BlendMode, @NonNull android.graphics.Paint); method public void drawOval(@NonNull android.graphics.RectF, @NonNull android.graphics.Paint); method public void drawOval(float, float, float, float, @NonNull android.graphics.Paint); method public void drawPaint(@NonNull android.graphics.Paint); @@ -14926,6 +14956,8 @@ package android.graphics { enum_constant public static final android.graphics.ColorSpace.Named ACESCG; enum_constant public static final android.graphics.ColorSpace.Named ADOBE_RGB; enum_constant public static final android.graphics.ColorSpace.Named BT2020; + enum_constant public static final android.graphics.ColorSpace.Named BT2020_HLG; + enum_constant public static final android.graphics.ColorSpace.Named BT2020_PQ; enum_constant public static final android.graphics.ColorSpace.Named BT709; enum_constant public static final android.graphics.ColorSpace.Named CIE_LAB; enum_constant public static final android.graphics.ColorSpace.Named CIE_XYZ; @@ -15269,6 +15301,49 @@ package android.graphics { enum_constant public static final android.graphics.Matrix.ScaleToFit START; } + public class Mesh { + method @NonNull public static android.graphics.Mesh make(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull android.graphics.Rect); + method @NonNull public static android.graphics.Mesh makeIndexed(@NonNull android.graphics.MeshSpecification, int, @NonNull java.nio.Buffer, int, @NonNull java.nio.ShortBuffer, @NonNull android.graphics.Rect); + method public void setColorUniform(@NonNull String, int); + method public void setColorUniform(@NonNull String, long); + method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color); + method public void setFloatUniform(@NonNull String, float); + method public void setFloatUniform(@NonNull String, float, float); + method public void setFloatUniform(@NonNull String, float, float, float); + method public void setFloatUniform(@NonNull String, float, float, float, float); + method public void setFloatUniform(@NonNull String, @NonNull float[]); + method public void setIntUniform(@NonNull String, int); + method public void setIntUniform(@NonNull String, int, int); + method public void setIntUniform(@NonNull String, int, int, int); + method public void setIntUniform(@NonNull String, int, int, int, int); + method public void setIntUniform(@NonNull String, @NonNull int[]); + field public static final int TRIANGLES = 0; // 0x0 + field public static final int TRIANGLE_STRIP = 1; // 0x1 + } + + public class MeshSpecification { + method @NonNull public static android.graphics.MeshSpecification make(@NonNull java.util.List<android.graphics.MeshSpecification.Attribute>, int, @NonNull java.util.List<android.graphics.MeshSpecification.Varying>, @NonNull String, @NonNull String); + method @NonNull public static android.graphics.MeshSpecification make(@NonNull java.util.List<android.graphics.MeshSpecification.Attribute>, int, @NonNull java.util.List<android.graphics.MeshSpecification.Varying>, @NonNull String, @NonNull String, @NonNull android.graphics.ColorSpace); + method @NonNull public static android.graphics.MeshSpecification make(@NonNull java.util.List<android.graphics.MeshSpecification.Attribute>, int, @NonNull java.util.List<android.graphics.MeshSpecification.Varying>, @NonNull String, @NonNull String, @NonNull android.graphics.ColorSpace, int); + field public static final int FLOAT = 0; // 0x0 + field public static final int FLOAT2 = 1; // 0x1 + field public static final int FLOAT3 = 2; // 0x2 + field public static final int FLOAT4 = 3; // 0x3 + field public static final int OPAQUE = 1; // 0x1 + field public static final int PREMUL = 2; // 0x2 + field public static final int UBYTE4 = 4; // 0x4 + field public static final int UNKNOWN = 0; // 0x0 + field public static final int UNPREMULT = 3; // 0x3 + } + + public static class MeshSpecification.Attribute { + ctor public MeshSpecification.Attribute(int, int, @NonNull String); + } + + public static class MeshSpecification.Varying { + ctor public MeshSpecification.Varying(int, @NonNull String); + } + @Deprecated public class Movie { method @Deprecated public static android.graphics.Movie decodeByteArray(byte[], int, int); method @Deprecated public static android.graphics.Movie decodeFile(String); @@ -15784,6 +15859,7 @@ package android.graphics { } public final class RecordingCanvas extends android.graphics.Canvas { + method public final void drawMesh(@NonNull android.graphics.Mesh, android.graphics.BlendMode, @NonNull android.graphics.Paint); } public final class Rect implements android.os.Parcelable { @@ -19962,8 +20038,9 @@ package android.location { method public int describeContents(); method @NonNull public android.location.GnssClock getClock(); method @NonNull public java.util.Collection<android.location.GnssAutomaticGainControl> getGnssAutomaticGainControls(); - method public boolean getIsFullTracking(); method @NonNull public java.util.Collection<android.location.GnssMeasurement> getMeasurements(); + method public boolean hasFullTracking(); + method public boolean isFullTracking(); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.location.GnssMeasurementsEvent> CREATOR; } @@ -19972,9 +20049,10 @@ package android.location { ctor public GnssMeasurementsEvent.Builder(); ctor public GnssMeasurementsEvent.Builder(@NonNull android.location.GnssMeasurementsEvent); method @NonNull public android.location.GnssMeasurementsEvent build(); + method @NonNull public android.location.GnssMeasurementsEvent.Builder clearFullTracking(); method @NonNull public android.location.GnssMeasurementsEvent.Builder setClock(@NonNull android.location.GnssClock); + method @NonNull public android.location.GnssMeasurementsEvent.Builder setFullTracking(boolean); method @NonNull public android.location.GnssMeasurementsEvent.Builder setGnssAutomaticGainControls(@NonNull java.util.Collection<android.location.GnssAutomaticGainControl>); - method @NonNull public android.location.GnssMeasurementsEvent.Builder setIsFullTracking(boolean); method @NonNull public android.location.GnssMeasurementsEvent.Builder setMeasurements(@NonNull java.util.Collection<android.location.GnssMeasurement>); } @@ -24191,6 +24269,7 @@ package android.media { method public int getDisableReason(); method public int getFlags(); method @NonNull public String getRouteId(); + method public int getSessionParticipantCount(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.media.RouteListingPreference.Item> CREATOR; field public static final int DISABLE_REASON_AD = 3; // 0x3 @@ -24206,6 +24285,7 @@ package android.media { method @NonNull public android.media.RouteListingPreference.Item build(); method @NonNull public android.media.RouteListingPreference.Item.Builder setDisableReason(int); method @NonNull public android.media.RouteListingPreference.Item.Builder setFlags(int); + method @NonNull public android.media.RouteListingPreference.Item.Builder setSessionParticipantCount(@IntRange(from=0) int); } public final class RoutingSessionInfo implements android.os.Parcelable { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bd64f4db3a7b..5f4c19beb3b5 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1496,11 +1496,13 @@ package android.app.backup { method @RequiresPermission(android.Manifest.permission.BACKUP) public android.content.Intent getDataManagementIntent(String); method @Nullable @RequiresPermission(android.Manifest.permission.BACKUP) public CharSequence getDataManagementIntentLabel(@NonNull String); method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.BACKUP) public String getDataManagementLabel(@NonNull String); + method @NonNull public android.app.backup.BackupRestoreEventLogger getDelayedRestoreLogger(); method @RequiresPermission(android.Manifest.permission.BACKUP) public String getDestinationString(String); method @RequiresPermission(android.Manifest.permission.BACKUP) public boolean isAppEligibleForBackup(String); method @RequiresPermission(android.Manifest.permission.BACKUP) public boolean isBackupEnabled(); method @RequiresPermission(android.Manifest.permission.BACKUP) public boolean isBackupServiceActive(android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.BACKUP) public String[] listAllTransports(); + method @NonNull public void reportDelayedRestoreResult(@NonNull android.app.backup.BackupRestoreEventLogger); method @RequiresPermission(android.Manifest.permission.BACKUP) public int requestBackup(String[], android.app.backup.BackupObserver); method @RequiresPermission(android.Manifest.permission.BACKUP) public int requestBackup(String[], android.app.backup.BackupObserver, android.app.backup.BackupManagerMonitor, int); method @Deprecated public int requestRestore(android.app.backup.RestoreObserver, android.app.backup.BackupManagerMonitor); @@ -3331,6 +3333,7 @@ package android.content { field public static final String CATEGORY_LEANBACK_SETTINGS = "android.intent.category.LEANBACK_SETTINGS"; field public static final String EXTRA_CALLING_PACKAGE = "android.intent.extra.CALLING_PACKAGE"; field public static final String EXTRA_FORCE_FACTORY_RESET = "android.intent.extra.FORCE_FACTORY_RESET"; + field public static final String EXTRA_INSTALL_RESULT = "android.intent.extra.INSTALL_RESULT"; field public static final String EXTRA_INSTANT_APP_ACTION = "android.intent.extra.INSTANT_APP_ACTION"; field public static final String EXTRA_INSTANT_APP_BUNDLES = "android.intent.extra.INSTANT_APP_BUNDLES"; field public static final String EXTRA_INSTANT_APP_EXTRAS = "android.intent.extra.INSTANT_APP_EXTRAS"; @@ -3346,6 +3349,7 @@ package android.content { field public static final String EXTRA_RESULT_NEEDED = "android.intent.extra.RESULT_NEEDED"; field public static final String EXTRA_ROLE_NAME = "android.intent.extra.ROLE_NAME"; field public static final String EXTRA_SHOWING_ATTRIBUTION = "android.intent.extra.SHOWING_ATTRIBUTION"; + field public static final String EXTRA_UNINSTALL_ALL_USERS = "android.intent.extra.UNINSTALL_ALL_USERS"; field public static final String EXTRA_UNKNOWN_INSTANT_APP = "android.intent.extra.UNKNOWN_INSTANT_APP"; field public static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle"; field public static final String EXTRA_VERIFICATION_BUNDLE = "android.intent.extra.VERIFICATION_BUNDLE"; @@ -3447,9 +3451,11 @@ package android.content.om { package android.content.pm { public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable { + method @RequiresPermission(android.Manifest.permission.DELETE_PACKAGES) public boolean hasFragileUserData(); method public boolean isEncryptionAware(); method public boolean isInstantApp(); method public boolean isOem(); + method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public boolean isPrivilegedApp(); method public boolean isProduct(); method public boolean isVendor(); field public String credentialProtectedDataDir; @@ -3567,16 +3573,32 @@ package android.content.pm { } public class PackageInstaller { + method @NonNull public android.content.pm.PackageInstaller.InstallInfo getInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException; method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setPermissionsResult(int, boolean); + field public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL"; + field public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL"; field public static final int DATA_LOADER_TYPE_INCREMENTAL = 2; // 0x2 field public static final int DATA_LOADER_TYPE_NONE = 0; // 0x0 field public static final int DATA_LOADER_TYPE_STREAMING = 1; // 0x1 + field public static final String EXTRA_CALLBACK = "android.content.pm.extra.CALLBACK"; field public static final String EXTRA_DATA_LOADER_TYPE = "android.content.pm.extra.DATA_LOADER_TYPE"; + field public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS"; + field public static final String EXTRA_RESOLVED_BASE_PATH = "android.content.pm.extra.RESOLVED_BASE_PATH"; field public static final int LOCATION_DATA_APP = 0; // 0x0 field public static final int LOCATION_MEDIA_DATA = 2; // 0x2 field public static final int LOCATION_MEDIA_OBB = 1; // 0x1 } + public static class PackageInstaller.InstallInfo { + method public long calculateInstalledSize(@NonNull android.content.pm.PackageInstaller.SessionParams) throws java.io.IOException; + method public int getInstallLocation(); + method @NonNull public String getPackageName(); + } + + public static class PackageInstaller.PackageParsingException extends java.lang.Exception { + method public int getErrorCode(); + } + public static class PackageInstaller.Session implements java.io.Closeable { method @RequiresPermission("com.android.permission.USE_INSTALLER_V2") public void addFile(int, @NonNull String, long, @NonNull byte[], @Nullable byte[]); method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void commitTransferred(@NonNull android.content.IntentSender); @@ -3623,6 +3645,7 @@ package android.content.pm { public abstract class PackageManager { method @RequiresPermission("android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS") public abstract void addOnPermissionsChangeListener(@NonNull android.content.pm.PackageManager.OnPermissionsChangedListener); method public abstract boolean arePermissionsIndividuallyControlled(); + method @NonNull public boolean canUserUninstall(@NonNull String, @NonNull android.os.UserHandle); method @NonNull public abstract java.util.List<android.content.IntentFilter> getAllIntentFilters(@NonNull String); method @NonNull @RequiresPermission("android.permission.GET_APP_METADATA") public android.os.PersistableBundle getAppMetadata(@NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; @@ -3641,6 +3664,7 @@ package android.content.pm { method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_INSTANT_APPS) public abstract java.util.List<android.content.pm.InstantAppInfo> getInstantApps(); method @Deprecated @NonNull public abstract java.util.List<android.content.pm.IntentFilterVerificationInfo> getIntentFilterVerifications(@NonNull String); 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 @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 @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); @@ -3668,11 +3692,19 @@ package android.content.pm { method @RequiresPermission(value=android.Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE, conditional=true) public void setSyntheticAppDetailsActivityEnabled(@NonNull String, boolean); method public void setSystemAppState(@NonNull String, int); 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 @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"; + field public static final int DELETE_ALL_USERS = 2; // 0x2 + field public static final int DELETE_FAILED_ABORTED = -5; // 0xfffffffb + field public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; // 0xfffffffe + field public static final int DELETE_FAILED_INTERNAL_ERROR = -1; // 0xffffffff + field public static final int DELETE_FAILED_OWNER_BLOCKED = -4; // 0xfffffffc + field public static final int DELETE_KEEP_DATA = 1; // 0x1 + field public static final int DELETE_SUCCEEDED = 1; // 0x1 field public static final String EXTRA_REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES = "android.content.pm.extra.REQUEST_PERMISSIONS_LEGACY_ACCESS_PERMISSION_NAMES"; field public static final String EXTRA_REQUEST_PERMISSIONS_NAMES = "android.content.pm.extra.REQUEST_PERMISSIONS_NAMES"; field public static final String EXTRA_REQUEST_PERMISSIONS_RESULTS = "android.content.pm.extra.REQUEST_PERMISSIONS_RESULTS"; @@ -3780,6 +3812,11 @@ package android.content.pm { @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 { } + public static final class PackageManager.UninstallCompleteCallback implements android.os.Parcelable { + method public void onUninstallComplete(@NonNull String, int, @Nullable String); + field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.PackageManager.UninstallCompleteCallback> CREATOR; + } + public class PermissionGroupInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable { field @StringRes public final int backgroundRequestDetailResourceId; field @StringRes public final int backgroundRequestResourceId; @@ -4865,17 +4902,15 @@ package android.hardware.input { public final class VirtualTouchscreenConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable { method public int describeContents(); - method public int getHeightInPixels(); - method public int getWidthInPixels(); + method public int getHeight(); + method public int getWidth(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualTouchscreenConfig> CREATOR; } public static final class VirtualTouchscreenConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualTouchscreenConfig.Builder> { - ctor public VirtualTouchscreenConfig.Builder(); + ctor public VirtualTouchscreenConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int); method @NonNull public android.hardware.input.VirtualTouchscreenConfig build(); - method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setHeightInPixels(int); - method @NonNull public android.hardware.input.VirtualTouchscreenConfig.Builder setWidthInPixels(int); } } @@ -6569,6 +6604,7 @@ package android.media { public class AudioManager { method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addAssistantServicesUids(@NonNull int[]); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnNonDefaultDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener) throws java.lang.SecurityException; method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener) throws java.lang.SecurityException; @@ -6590,6 +6626,7 @@ package android.media { method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMaxVolumeIndexForAttributes(@NonNull android.media.AudioAttributes); method @IntRange(from=0) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int getMinVolumeIndexForAttributes(@NonNull android.media.AudioAttributes); method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getMutingExpectedDevice(); + method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getNonDefaultDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy); method @Nullable @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public android.media.AudioDeviceAttributes getPreferredDeviceForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForCapturePreset(int); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List<android.media.AudioDeviceAttributes> getPreferredDevicesForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy); @@ -6605,6 +6642,8 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void registerMuteAwaitConnectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.MuteAwaitConnectionCallback); method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnNonDefaultDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForStrategyChangedListener); @@ -6616,6 +6655,7 @@ package android.media { method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setAdditionalOutputDeviceDelay(@NonNull android.media.AudioDeviceInfo, @IntRange(from=0) long); method public void setAudioServerStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.AudioServerStateCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setBluetoothVariableLatencyEnabled(boolean); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setDeviceVolumeBehavior(@NonNull android.media.AudioDeviceAttributes, int); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void setFocusRequestResult(@NonNull android.media.AudioFocusInfo, int, @NonNull android.media.audiopolicy.AudioPolicy); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean setPreferredDeviceForCapturePreset(int, @NonNull android.media.AudioDeviceAttributes); @@ -6660,6 +6700,10 @@ package android.media { field public static final int EVENT_TIMEOUT = 2; // 0x2 } + public static interface AudioManager.OnNonDefaultDevicesForStrategyChangedListener { + method public void onNonDefaultDevicesForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>); + } + @Deprecated public static interface AudioManager.OnPreferredDeviceForStrategyChangedListener { method @Deprecated public void onPreferredDeviceForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @Nullable android.media.AudioDeviceAttributes); } @@ -7744,6 +7788,7 @@ package android.media.tv.tuner.filter { field public static final int STATUS_DATA_READY = 1; // 0x1 field public static final int STATUS_HIGH_WATER = 4; // 0x4 field public static final int STATUS_LOW_WATER = 2; // 0x2 + field public static final int STATUS_NO_DATA = 16; // 0x10 field public static final int STATUS_OVERFLOW = 8; // 0x8 field public static final int SUBTYPE_AUDIO = 3; // 0x3 field public static final int SUBTYPE_DOWNLOAD = 5; // 0x5 @@ -10923,6 +10968,7 @@ package android.provider { field public static final String ACTION_TETHER_PROVISIONING_UI = "android.settings.TETHER_PROVISIONING_UI"; field public static final String ACTION_TETHER_SETTINGS = "android.settings.TETHER_SETTINGS"; field public static final String ACTION_TETHER_UNSUPPORTED_CARRIER_UI = "android.settings.TETHER_UNSUPPORTED_CARRIER_UI"; + field public static final String ACTION_USER_SETTINGS = "android.settings.USER_SETTINGS"; } public static final class Settings.Global extends android.provider.Settings.NameValueTable { @@ -11834,6 +11880,7 @@ package android.service.notification { field @NonNull public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR; field public static final String KEY_CONTEXTUAL_ACTIONS = "key_contextual_actions"; field public static final String KEY_IMPORTANCE = "key_importance"; + field public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal"; field public static final String KEY_NOT_CONVERSATION = "key_not_conversation"; field public static final String KEY_PEOPLE = "key_people"; field public static final String KEY_RANKING_SCORE = "key_ranking_score"; @@ -11874,6 +11921,10 @@ package android.service.notification { method public void onNotificationRemoved(@NonNull android.service.notification.StatusBarNotification, @NonNull android.service.notification.NotificationListenerService.RankingMap, @NonNull android.service.notification.NotificationStats, int); } + public static class NotificationListenerService.Ranking { + method public int getProposedImportance(); + } + public final class NotificationStats implements android.os.Parcelable { ctor public NotificationStats(); ctor protected NotificationStats(android.os.Parcel); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index c1cbeef0bba9..233dee9f98e4 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -814,7 +814,7 @@ package android.content.pm { public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable { method public boolean hasRequestForegroundServiceExemption(); method public boolean isOnBackInvokedCallbackEnabled(); - method public boolean isPrivilegedApp(); + method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public boolean isPrivilegedApp(); method public boolean isSystemApp(); method public void setEnableOnBackInvokedCallback(boolean); field public static final int PRIVATE_FLAG_PRIVILEGED = 8; // 0x8 @@ -831,7 +831,6 @@ package android.content.pm { public static class PackageInstaller.SessionParams implements android.os.Parcelable { method public void setInstallFlagAllowTest(); - method public void setInstallerPackageName(@Nullable String); } public abstract class PackageManager { @@ -1299,6 +1298,7 @@ package android.hardware.input { method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void clearAllModifierKeyRemappings(); method @Nullable public String getCurrentKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier); method @NonNull public java.util.List<java.lang.String> getKeyboardLayoutDescriptorsForInputDevice(@NonNull android.view.InputDevice); + method @NonNull public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public java.util.Map<java.lang.Integer,java.lang.Integer> getModifierKeyRemapping(); method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int); method @RequiresPermission(android.Manifest.permission.SET_KEYBOARD_LAYOUT) public void removeKeyboardLayoutForInputDevice(@NonNull android.hardware.input.InputDeviceIdentifier, @NonNull String); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 9ce196d85b56..afd8a52d43ce 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -62,6 +62,7 @@ import android.app.servertransaction.ResumeActivityItem; import android.app.servertransaction.TransactionExecutor; import android.app.servertransaction.TransactionExecutorHelper; import android.bluetooth.BluetoothFrameworkInitializer; +import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; import android.content.AutofillOptions; @@ -361,6 +362,8 @@ public final class ActivityThread extends ClientTransactionHandler private int mLastProcessState = PROCESS_STATE_UNKNOWN; ArrayList<WeakReference<AssistStructure>> mLastAssistStructures = new ArrayList<>(); private int mLastSessionId; + // Holds the value of the last reported device ID value from the server for the top activity. + int mLastReportedDeviceId; final ArrayMap<IBinder, CreateServiceData> mServicesData = new ArrayMap<>(); @UnsupportedAppUsage final ArrayMap<IBinder, Service> mServices = new ArrayMap<>(); @@ -546,6 +549,9 @@ public final class ActivityThread extends ClientTransactionHandler boolean hideForNow; Configuration createdConfig; Configuration overrideConfig; + // TODO(b/263402465): pass deviceId directly in LaunchActivityItem#execute + // The deviceId assigned by the server when this activity was first started. + int mDeviceId; // Used for consolidating configs before sending on to Activity. private Configuration tmpConfig = new Configuration(); // Callback used for updating activity override config and camera compat control state. @@ -608,7 +614,7 @@ public final class ActivityThread extends ClientTransactionHandler } public ActivityClientRecord(IBinder token, Intent intent, int ident, - ActivityInfo info, Configuration overrideConfig, + ActivityInfo info, Configuration overrideConfig, int deviceId, String referrer, IVoiceInteractor voiceInteractor, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, ActivityOptions activityOptions, @@ -630,6 +636,7 @@ public final class ActivityThread extends ClientTransactionHandler this.isForward = isForward; this.profilerInfo = profilerInfo; this.overrideConfig = overrideConfig; + this.mDeviceId = deviceId; this.packageInfo = client.getPackageInfoNoCheck(activityInfo.applicationInfo); mActivityOptions = activityOptions; mLaunchedFromBubble = launchedFromBubble; @@ -3816,6 +3823,7 @@ public final class ActivityThread extends ClientTransactionHandler // Make sure we are running with the most recent config. mConfigurationController.handleConfigurationChanged(null, null); + updateDeviceIdForNonUIContexts(r.mDeviceId); if (localLOGV) Slog.v( TAG, "Handling launch of " + r); @@ -4219,7 +4227,8 @@ public final class ActivityThread extends ClientTransactionHandler private void scheduleResume(ActivityClientRecord r) { final ClientTransaction transaction = ClientTransaction.obtain(this.mAppThread, r.token); - transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false)); + transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false, + /* shouldSendCompatFakeFocus */ false)); executeTransaction(transaction); } @@ -4548,6 +4557,9 @@ public final class ActivityThread extends ClientTransactionHandler context.setOuterContext(service); service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService()); + if (!service.isUiContext()) { // WindowProviderService is a UI Context. + service.updateDeviceId(mLastReportedDeviceId); + } service.onCreate(); mServicesData.put(data.token, data); mServices.put(data.token, service); @@ -4884,7 +4896,7 @@ public final class ActivityThread extends ClientTransactionHandler @Override public void handleResumeActivity(ActivityClientRecord r, boolean finalStateRequest, - boolean isForward, String reason) { + boolean isForward, boolean shouldSendCompatFakeFocus, String reason) { // If we are getting ready to gc after going to the background, well // we are back active so skip it. unscheduleGcIdler(); @@ -4991,6 +5003,16 @@ public final class ActivityThread extends ClientTransactionHandler if (r.activity.mVisibleFromClient) { r.activity.makeVisible(); } + + if (shouldSendCompatFakeFocus) { + // Attaching to a window is asynchronous with the activity being resumed, + // so it's possible we will need to send a fake focus event after attaching + if (impl != null) { + impl.dispatchCompatFakeFocus(); + } else { + r.window.getDecorView().fakeFocusAfterAttachingToWindow(); + } + } } r.nextIdle = mNewActivities; @@ -6066,9 +6088,48 @@ public final class ActivityThread extends ClientTransactionHandler } } + private void updateDeviceIdForNonUIContexts(int deviceId) { + // Invalid device id is treated as a no-op. + if (deviceId == VirtualDeviceManager.DEVICE_ID_INVALID) { + return; + } + if (deviceId == mLastReportedDeviceId) { + return; + } + mLastReportedDeviceId = deviceId; + ArrayList<Context> nonUIContexts = new ArrayList<>(); + // Update Application and Service contexts with implicit device association. + // UI Contexts are able to derived their device Id association from the display. + synchronized (mResourcesManager) { + final int numApps = mAllApplications.size(); + for (int i = 0; i < numApps; i++) { + nonUIContexts.add(mAllApplications.get(i)); + } + final int numServices = mServices.size(); + for (int i = 0; i < numServices; i++) { + final Service service = mServices.valueAt(i); + // WindowProviderService is a UI Context. + if (!service.isUiContext()) { + nonUIContexts.add(service); + } + } + } + for (Context context : nonUIContexts) { + try { + context.updateDeviceId(deviceId); + } catch (IllegalArgumentException e) { + // It can happen that the system already closed/removed a virtual device + // and the passed deviceId is no longer valid. + // TODO(b/263355088): check for validity of deviceId before updating + // instead of catching this exception once VDM add an API to validate ids. + } + } + } + @Override - public void handleConfigurationChanged(Configuration config) { + public void handleConfigurationChanged(Configuration config, int deviceId) { mConfigurationController.handleConfigurationChanged(config); + updateDeviceIdForNonUIContexts(deviceId); // These are only done to maintain @UnsupportedAppUsage and should be removed someday. mCurDefaultDisplayDpi = mConfigurationController.getCurDefaultDisplayDpi(); diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 309b2535798e..a7a4b356c8d5 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -3875,4 +3875,19 @@ public class ApplicationPackageManager extends PackageManager { throw e.rethrowAsRuntimeException(); } } + + @Override + public boolean canUserUninstall(String packageName, UserHandle user) { + try { + return mPM.getBlockUninstallForUser(packageName, user.getIdentifier()); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @Override + public boolean shouldShowNewAppInstalledNotification() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED, 0) == 1; + } } diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index bd9bab3ec77b..419ffac230f7 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -108,7 +108,8 @@ public abstract class ClientTransactionHandler { * @param reason Reason for performing this operation. */ public abstract void handleResumeActivity(@NonNull ActivityClientRecord r, - boolean finalStateRequest, boolean isForward, String reason); + boolean finalStateRequest, boolean isForward, boolean shouldSendCompatFakeFocus, + String reason); /** * Notify the activity about top resumed state change. @@ -184,8 +185,8 @@ public abstract class ClientTransactionHandler { /** Get package info. */ public abstract LoadedApk getPackageInfoNoCheck(ApplicationInfo ai); - /** Deliver app configuration change notification. */ - public abstract void handleConfigurationChanged(Configuration config); + /** Deliver app configuration change notification and device association. */ + public abstract void handleConfigurationChanged(Configuration config, int deviceId); /** * Get {@link android.app.ActivityThread.ActivityClientRecord} instance that corresponds to the diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index a832b9a2100d..1120257142dc 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2710,7 +2710,7 @@ class ContextImpl extends Context { context.setResources(createResources(mToken, mPackageInfo, mSplitName, displayId, overrideConfig, display.getDisplayAdjustments().getCompatibilityInfo(), mResources.getLoaders())); - context.mDisplay = display; + context.setDisplay(display); // Inherit context type if the container is from System or System UI context to bypass // UI context check. context.mContextType = mContextType == CONTEXT_TYPE_SYSTEM_OR_SYSTEM_UI @@ -2726,6 +2726,13 @@ class ContextImpl extends Context { return context; } + private void setDisplay(Display display) { + mDisplay = display; + if (display != null) { + updateDeviceIdIfChanged(display.getDisplayId()); + } + } + @Override public @NonNull Context createDeviceContext(int deviceId) { if (!isValidDeviceId(deviceId)) { @@ -2863,8 +2870,8 @@ class ContextImpl extends Context { baseContext.setResources(windowContextResources); // Associate the display with window context resources so that configuration update from // the server side will also apply to the display's metrics. - baseContext.mDisplay = ResourcesManager.getInstance().getAdjustedDisplay(displayId, - windowContextResources); + baseContext.setDisplay(ResourcesManager.getInstance().getAdjustedDisplay( + displayId, windowContextResources)); return baseContext; } @@ -3008,11 +3015,24 @@ class ContextImpl extends Context { @Override public void updateDisplay(int displayId) { - mDisplay = mResourcesManager.getAdjustedDisplay(displayId, mResources); + setDisplay(mResourcesManager.getAdjustedDisplay(displayId, mResources)); if (mContextType == CONTEXT_TYPE_NON_UI) { mContextType = CONTEXT_TYPE_DISPLAY_CONTEXT; } - // TODO(b/253201821): Update deviceId when display is updated. + } + + private void updateDeviceIdIfChanged(int displayId) { + if (mIsExplicitDeviceId) { + return; + } + VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); + if (vdm != null) { + int deviceId = vdm.getDeviceIdForDisplayId(displayId); + if (deviceId != mDeviceId) { + mDeviceId = deviceId; + notifyOnDeviceChangedListeners(mDeviceId); + } + } } @Override @@ -3307,8 +3327,8 @@ class ContextImpl extends Context { classLoader, packageInfo.getApplication() == null ? null : packageInfo.getApplication().getResources().getLoaders())); - context.mDisplay = resourcesManager.getAdjustedDisplay(displayId, - context.getResources()); + context.setDisplay(resourcesManager.getAdjustedDisplay( + displayId, context.getResources())); return context; } diff --git a/core/java/android/app/GrammaticalInflectionManager.java b/core/java/android/app/GrammaticalInflectionManager.java new file mode 100644 index 000000000000..1905b6a46d7e --- /dev/null +++ b/core/java/android/app/GrammaticalInflectionManager.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.annotation.SystemService; +import android.content.Context; +import android.content.res.Configuration; +import android.os.RemoteException; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * This class allow applications to control granular grammatical inflection settings (such as + * per-app grammatical gender). + */ +@SystemService(Context.GRAMMATICAL_INFLECTION_SERVICE) +public class GrammaticalInflectionManager { + private static final Set<Integer> VALID_GENDER_VALUES = new HashSet<>(Arrays.asList( + Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED, + Configuration.GRAMMATICAL_GENDER_NEUTRAL, + Configuration.GRAMMATICAL_GENDER_FEMININE, + Configuration.GRAMMATICAL_GENDER_MASCULINE)); + + private final Context mContext; + private final IGrammaticalInflectionManager mService; + + /** @hide Instantiated by ContextImpl */ + public GrammaticalInflectionManager(Context context, IGrammaticalInflectionManager service) { + mContext = context; + mService = service; + } + + /** + * Returns the current grammatical gender for the calling app. A new value can be requested via + * {@link #setRequestedApplicationGrammaticalGender(int)} and will be updated with a new + * configuration change. The method always returns the value received with the last received + * configuration change. + * + * @return the value of grammatical gender + * @see Configuration#getGrammaticalGender + */ + @Configuration.GrammaticalGender + public int getApplicationGrammaticalGender() { + return mContext.getApplicationContext() + .getResources() + .getConfiguration() + .getGrammaticalGender(); + } + + /** + * Sets the current grammatical gender for the calling app (keyed by package name and user ID + * retrieved from the calling pid). + * + * <p><b>Note:</b> Changes to app grammatical gender will result in a configuration change (and + * potentially an Activity re-creation) being applied to the specified application. For more + * information, see the <a + * href="https://developer.android.com/guide/topics/resources/runtime-changes">section on + * handling configuration changes</a>. The set grammatical gender are persisted across + * application restarts; they are backed up if the user has enabled Backup & Restore.` + * + * @param grammaticalGender the terms of address the user preferred in an application. + * @see Configuration#getGrammaticalGender + */ + public void setRequestedApplicationGrammaticalGender( + @Configuration.GrammaticalGender int grammaticalGender) { + if (!VALID_GENDER_VALUES.contains(grammaticalGender)) { + throw new IllegalArgumentException("Unknown grammatical gender"); + } + + try { + mService.setRequestedApplicationGrammaticalGender( + mContext.getPackageName(), mContext.getUserId(), grammaticalGender); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index f20503cef705..461aa3c69482 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -198,9 +198,8 @@ interface IActivityTaskManager { * @param taskId The id of the task to set the bounds for. * @param bounds The new bounds. * @param resizeMode Resize mode defined as {@code ActivityTaskManager#RESIZE_MODE_*} constants. - * @return Return true on success. Otherwise false. */ - boolean resizeTask(int taskId, in Rect bounds, int resizeMode); + void resizeTask(int taskId, in Rect bounds, int resizeMode); void moveRootTaskToDisplay(int taskId, int displayId); void moveTaskToRootTask(int taskId, int rootTaskId, boolean toTop); diff --git a/core/java/android/app/IGrammaticalInflectionManager.aidl b/core/java/android/app/IGrammaticalInflectionManager.aidl new file mode 100644 index 000000000000..9366a45551da --- /dev/null +++ b/core/java/android/app/IGrammaticalInflectionManager.aidl @@ -0,0 +1,19 @@ +package android.app; + + +/** + * Internal interface used to control app-specific gender. + * + * <p>Use the {@link android.app.GrammarInflectionManager} class rather than going through + * this Binder interface directly. See {@link android.app.GrammarInflectionManager} for + * more complete documentation. + * + * @hide + */ + interface IGrammaticalInflectionManager { + + /** + * Sets a specified app’s app-specific grammatical gender. + */ + void setRequestedApplicationGrammaticalGender(String appPackageName, int userId, int gender); + }
\ No newline at end of file diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java index 5d50d29b46ae..50ba7dbbec85 100644 --- a/core/java/android/app/LocaleConfig.java +++ b/core/java/android/app/LocaleConfig.java @@ -64,7 +64,7 @@ public class LocaleConfig implements Parcelable { public static final String TAG_LOCALE_CONFIG = "locale-config"; public static final String TAG_LOCALE = "locale"; private LocaleList mLocales; - private int mStatus; + private int mStatus = STATUS_NOT_SPECIFIED; /** * succeeded reading the LocaleConfig structure stored in an XML file. @@ -119,6 +119,7 @@ public class LocaleConfig implements Parcelable { LocaleManager localeManager = context.getSystemService(LocaleManager.class); if (localeManager == null) { Slog.w(TAG, "LocaleManager is null, cannot get the override LocaleConfig"); + mStatus = STATUS_NOT_SPECIFIED; return; } LocaleConfig localeConfig = localeManager.getOverrideLocaleConfig(); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index df13a8788103..5b3b2a68a2b2 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -1547,6 +1547,7 @@ public final class SystemServiceRegistry { IAmbientContextManager.Stub.asInterface(iBinder); return new AmbientContextManager(ctx.getOuterContext(), manager); }}); + registerService(Context.WEARABLE_SENSING_SERVICE, WearableSensingManager.class, new CachedServiceFetcher<WearableSensingManager>() { @Override @@ -1559,6 +1560,18 @@ public final class SystemServiceRegistry { return new WearableSensingManager(ctx.getOuterContext(), manager); }}); + registerService(Context.GRAMMATICAL_INFLECTION_SERVICE, GrammaticalInflectionManager.class, + new CachedServiceFetcher<GrammaticalInflectionManager>() { + @Override + public GrammaticalInflectionManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + return new GrammaticalInflectionManager(ctx, + IGrammaticalInflectionManager.Stub.asInterface( + ServiceManager.getServiceOrThrow( + Context.GRAMMATICAL_INFLECTION_SERVICE))); + }}); + + sInitializing = true; try { // Note: the following functions need to be @SystemApis, once they become mainline diff --git a/core/java/android/app/backup/BackupManager.java b/core/java/android/app/backup/BackupManager.java index 7255c3e0813f..bad282ead3d3 100644 --- a/core/java/android/app/backup/BackupManager.java +++ b/core/java/android/app/backup/BackupManager.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.app.backup.BackupAnnotations.OperationType; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -1041,6 +1042,42 @@ public class BackupManager { return backupAgent.getBackupRestoreEventLogger(); } + /** + * Get an instance of {@link BackupRestoreEventLogger} to report B&R related events during a + * delayed restore operation. + * + * @return an instance of {@link BackupRestoreEventLogger}. + * + * @hide + */ + @NonNull + @SystemApi + public BackupRestoreEventLogger getDelayedRestoreLogger() { + return new BackupRestoreEventLogger(OperationType.RESTORE); + } + + /** + * Report B&R related events following a delayed restore operation. + * + * @param logger an instance of {@link BackupRestoreEventLogger} to which the corresponding + * events have been logged. + * + * @hide + */ + @NonNull + @SystemApi + public void reportDelayedRestoreResult(@NonNull BackupRestoreEventLogger logger) { + checkServiceBinder(); + if (sService != null) { + try { + sService.reportDelayedRestoreResult(mContext.getPackageName(), + logger.getLoggingResults()); + } catch (RemoteException e) { + Log.w(TAG, "reportDelayedRestoreResult() couldn't connect"); + } + } + } + /* * We wrap incoming binder calls with a private class implementation that * redirects them into main-thread actions. This serializes the backup diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index bf5be95c4ab0..aeb498721fb6 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -16,6 +16,7 @@ package android.app.backup; +import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupObserver; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IFullBackupRestoreObserver; @@ -722,4 +723,6 @@ interface IBackupManager { * that have been excluded will be passed to the agent to make it aware of the exclusions. */ void excludeKeysFromRestore(String packageName, in List<String> keys); + + void reportDelayedRestoreResult(in String packageName, in List<DataTypeResult> results); } diff --git a/core/java/android/app/servertransaction/ConfigurationChangeItem.java b/core/java/android/app/servertransaction/ConfigurationChangeItem.java index 49a1c669537c..a563bbc9c70d 100644 --- a/core/java/android/app/servertransaction/ConfigurationChangeItem.java +++ b/core/java/android/app/servertransaction/ConfigurationChangeItem.java @@ -32,6 +32,7 @@ import java.util.Objects; public class ConfigurationChangeItem extends ClientTransactionItem { private Configuration mConfiguration; + private int mDeviceId; @Override public void preExecute(android.app.ClientTransactionHandler client, IBinder token) { @@ -42,7 +43,7 @@ public class ConfigurationChangeItem extends ClientTransactionItem { @Override public void execute(ClientTransactionHandler client, IBinder token, PendingTransactionActions pendingActions) { - client.handleConfigurationChanged(mConfiguration); + client.handleConfigurationChanged(mConfiguration, mDeviceId); } @@ -51,12 +52,13 @@ public class ConfigurationChangeItem extends ClientTransactionItem { private ConfigurationChangeItem() {} /** Obtain an instance initialized with provided params. */ - public static ConfigurationChangeItem obtain(Configuration config) { + public static ConfigurationChangeItem obtain(Configuration config, int deviceId) { ConfigurationChangeItem instance = ObjectPool.obtain(ConfigurationChangeItem.class); if (instance == null) { instance = new ConfigurationChangeItem(); } instance.mConfiguration = config; + instance.mDeviceId = deviceId; return instance; } @@ -64,6 +66,7 @@ public class ConfigurationChangeItem extends ClientTransactionItem { @Override public void recycle() { mConfiguration = null; + mDeviceId = 0; ObjectPool.recycle(this); } @@ -74,11 +77,13 @@ public class ConfigurationChangeItem extends ClientTransactionItem { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeTypedObject(mConfiguration, flags); + dest.writeInt(mDeviceId); } /** Read from Parcel. */ private ConfigurationChangeItem(Parcel in) { mConfiguration = in.readTypedObject(Configuration.CREATOR); + mDeviceId = in.readInt(); } public static final @android.annotation.NonNull Creator<ConfigurationChangeItem> CREATOR = @@ -101,16 +106,20 @@ public class ConfigurationChangeItem extends ClientTransactionItem { return false; } final ConfigurationChangeItem other = (ConfigurationChangeItem) o; - return Objects.equals(mConfiguration, other.mConfiguration); + return Objects.equals(mConfiguration, other.mConfiguration) + && mDeviceId == other.mDeviceId; } @Override public int hashCode() { - return mConfiguration.hashCode(); + int result = 17; + result = 31 * result + mDeviceId; + result = 31 * result + mConfiguration.hashCode(); + return result; } @Override public String toString() { - return "ConfigurationChangeItem{config=" + mConfiguration + "}"; + return "ConfigurationChangeItem{deviceId=" + mDeviceId + ", config" + mConfiguration + "}"; } } diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java index 7e4db81b8d62..3d0aa2540068 100644 --- a/core/java/android/app/servertransaction/LaunchActivityItem.java +++ b/core/java/android/app/servertransaction/LaunchActivityItem.java @@ -58,6 +58,7 @@ public class LaunchActivityItem extends ClientTransactionItem { private ActivityInfo mInfo; private Configuration mCurConfig; private Configuration mOverrideConfig; + private int mDeviceId; private String mReferrer; private IVoiceInteractor mVoiceInteractor; private int mProcState; @@ -95,7 +96,7 @@ public class LaunchActivityItem extends ClientTransactionItem { PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo, - mOverrideConfig, mReferrer, mVoiceInteractor, mState, mPersistentState, + mOverrideConfig, mDeviceId, mReferrer, mVoiceInteractor, mState, mPersistentState, mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo, client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble, mTaskFragmentToken); @@ -116,7 +117,7 @@ public class LaunchActivityItem extends ClientTransactionItem { /** Obtain an instance initialized with provided params. */ public static LaunchActivityItem obtain(Intent intent, int ident, ActivityInfo info, - Configuration curConfig, Configuration overrideConfig, + Configuration curConfig, Configuration overrideConfig, int deviceId, String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, ActivityOptions activityOptions, @@ -127,7 +128,7 @@ public class LaunchActivityItem extends ClientTransactionItem { if (instance == null) { instance = new LaunchActivityItem(); } - setValues(instance, intent, ident, info, curConfig, overrideConfig, referrer, + setValues(instance, intent, ident, info, curConfig, overrideConfig, deviceId, referrer, voiceInteractor, procState, state, persistentState, pendingResults, pendingNewIntents, activityOptions, isForward, profilerInfo, assistToken, activityClientController, shareableActivityToken, @@ -138,7 +139,7 @@ public class LaunchActivityItem extends ClientTransactionItem { @Override public void recycle() { - setValues(this, null, 0, null, null, null, null, null, 0, null, null, null, null, + setValues(this, null, 0, null, null, null, 0, null, null, 0, null, null, null, null, null, false, null, null, null, null, false, null); ObjectPool.recycle(this); } @@ -154,6 +155,7 @@ public class LaunchActivityItem extends ClientTransactionItem { dest.writeTypedObject(mInfo, flags); dest.writeTypedObject(mCurConfig, flags); dest.writeTypedObject(mOverrideConfig, flags); + dest.writeInt(mDeviceId); dest.writeString(mReferrer); dest.writeStrongInterface(mVoiceInteractor); dest.writeInt(mProcState); @@ -175,7 +177,7 @@ public class LaunchActivityItem extends ClientTransactionItem { private LaunchActivityItem(Parcel in) { setValues(this, in.readTypedObject(Intent.CREATOR), in.readInt(), in.readTypedObject(ActivityInfo.CREATOR), in.readTypedObject(Configuration.CREATOR), - in.readTypedObject(Configuration.CREATOR), in.readString(), + in.readTypedObject(Configuration.CREATOR), in.readInt(), in.readString(), IVoiceInteractor.Stub.asInterface(in.readStrongBinder()), in.readInt(), in.readBundle(getClass().getClassLoader()), in.readPersistableBundle(getClass().getClassLoader()), @@ -215,6 +217,7 @@ public class LaunchActivityItem extends ClientTransactionItem { return intentsEqual && mIdent == other.mIdent && activityInfoEqual(other.mInfo) && Objects.equals(mCurConfig, other.mCurConfig) && Objects.equals(mOverrideConfig, other.mOverrideConfig) + && mDeviceId == other.mDeviceId && Objects.equals(mReferrer, other.mReferrer) && mProcState == other.mProcState && areBundlesEqualRoughly(mState, other.mState) && areBundlesEqualRoughly(mPersistentState, other.mPersistentState) @@ -235,6 +238,7 @@ public class LaunchActivityItem extends ClientTransactionItem { result = 31 * result + mIdent; result = 31 * result + Objects.hashCode(mCurConfig); result = 31 * result + Objects.hashCode(mOverrideConfig); + result = 31 * result + mDeviceId; result = 31 * result + Objects.hashCode(mReferrer); result = 31 * result + Objects.hashCode(mProcState); result = 31 * result + getRoughBundleHashCode(mState); @@ -279,16 +283,17 @@ public class LaunchActivityItem extends ClientTransactionItem { public String toString() { return "LaunchActivityItem{intent=" + mIntent + ",ident=" + mIdent + ",info=" + mInfo + ",curConfig=" + mCurConfig + ",overrideConfig=" + mOverrideConfig - + ",referrer=" + mReferrer + ",procState=" + mProcState + ",state=" + mState - + ",persistentState=" + mPersistentState + ",pendingResults=" + mPendingResults - + ",pendingNewIntents=" + mPendingNewIntents + ",options=" + mActivityOptions - + ",profilerInfo=" + mProfilerInfo + ",assistToken=" + mAssistToken - + ",shareableActivityToken=" + mShareableActivityToken + "}"; + + ",deviceId=" + mDeviceId + ",referrer=" + mReferrer + ",procState=" + mProcState + + ",state=" + mState + ",persistentState=" + mPersistentState + + ",pendingResults=" + mPendingResults + ",pendingNewIntents=" + mPendingNewIntents + + ",options=" + mActivityOptions + ",profilerInfo=" + mProfilerInfo + + ",assistToken=" + mAssistToken + ",shareableActivityToken=" + + mShareableActivityToken + "}"; } // Using the same method to set and clear values to make sure we don't forget anything private static void setValues(LaunchActivityItem instance, Intent intent, int ident, - ActivityInfo info, Configuration curConfig, Configuration overrideConfig, + ActivityInfo info, Configuration curConfig, Configuration overrideConfig, int deviceId, String referrer, IVoiceInteractor voiceInteractor, int procState, Bundle state, PersistableBundle persistentState, List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents, @@ -300,6 +305,7 @@ public class LaunchActivityItem extends ClientTransactionItem { instance.mInfo = info; instance.mCurConfig = curConfig; instance.mOverrideConfig = overrideConfig; + instance.mDeviceId = deviceId; instance.mReferrer = referrer; instance.mVoiceInteractor = voiceInteractor; instance.mProcState = procState; diff --git a/core/java/android/app/servertransaction/ResumeActivityItem.java b/core/java/android/app/servertransaction/ResumeActivityItem.java index e6fdc006615a..222f8ca61e09 100644 --- a/core/java/android/app/servertransaction/ResumeActivityItem.java +++ b/core/java/android/app/servertransaction/ResumeActivityItem.java @@ -39,6 +39,9 @@ public class ResumeActivityItem extends ActivityLifecycleItem { private int mProcState; private boolean mUpdateProcState; private boolean mIsForward; + // Whether we should send compat fake focus when the activity is resumed. This is needed + // because some game engines wait to get focus before drawing the content of the app. + private boolean mShouldSendCompatFakeFocus; @Override public void preExecute(ClientTransactionHandler client, IBinder token) { @@ -52,7 +55,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); client.handleResumeActivity(r, true /* finalStateRequest */, mIsForward, - "RESUME_ACTIVITY"); + mShouldSendCompatFakeFocus, "RESUME_ACTIVITY"); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } @@ -74,7 +77,8 @@ public class ResumeActivityItem extends ActivityLifecycleItem { private ResumeActivityItem() {} /** Obtain an instance initialized with provided params. */ - public static ResumeActivityItem obtain(int procState, boolean isForward) { + public static ResumeActivityItem obtain(int procState, boolean isForward, + boolean shouldSendCompatFakeFocus) { ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); if (instance == null) { instance = new ResumeActivityItem(); @@ -82,12 +86,13 @@ public class ResumeActivityItem extends ActivityLifecycleItem { instance.mProcState = procState; instance.mUpdateProcState = true; instance.mIsForward = isForward; + instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus; return instance; } /** Obtain an instance initialized with provided params. */ - public static ResumeActivityItem obtain(boolean isForward) { + public static ResumeActivityItem obtain(boolean isForward, boolean shouldSendCompatFakeFocus) { ResumeActivityItem instance = ObjectPool.obtain(ResumeActivityItem.class); if (instance == null) { instance = new ResumeActivityItem(); @@ -95,6 +100,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { instance.mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; instance.mUpdateProcState = false; instance.mIsForward = isForward; + instance.mShouldSendCompatFakeFocus = shouldSendCompatFakeFocus; return instance; } @@ -105,6 +111,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { mProcState = ActivityManager.PROCESS_STATE_UNKNOWN; mUpdateProcState = false; mIsForward = false; + mShouldSendCompatFakeFocus = false; ObjectPool.recycle(this); } @@ -117,6 +124,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { dest.writeInt(mProcState); dest.writeBoolean(mUpdateProcState); dest.writeBoolean(mIsForward); + dest.writeBoolean(mShouldSendCompatFakeFocus); } /** Read from Parcel. */ @@ -124,6 +132,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem { mProcState = in.readInt(); mUpdateProcState = in.readBoolean(); mIsForward = in.readBoolean(); + mShouldSendCompatFakeFocus = in.readBoolean(); } public static final @NonNull Creator<ResumeActivityItem> CREATOR = @@ -147,7 +156,8 @@ public class ResumeActivityItem extends ActivityLifecycleItem { } final ResumeActivityItem other = (ResumeActivityItem) o; return mProcState == other.mProcState && mUpdateProcState == other.mUpdateProcState - && mIsForward == other.mIsForward; + && mIsForward == other.mIsForward + && mShouldSendCompatFakeFocus == other.mShouldSendCompatFakeFocus; } @Override @@ -156,12 +166,14 @@ public class ResumeActivityItem extends ActivityLifecycleItem { result = 31 * result + mProcState; result = 31 * result + (mUpdateProcState ? 1 : 0); result = 31 * result + (mIsForward ? 1 : 0); + result = 31 * result + (mShouldSendCompatFakeFocus ? 1 : 0); return result; } @Override public String toString() { return "ResumeActivityItem{procState=" + mProcState - + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + "}"; + + ",updateProcState=" + mUpdateProcState + ",isForward=" + mIsForward + + ",shouldSendCompatFakeFocus=" + mShouldSendCompatFakeFocus + "}"; } } diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java index 1ff0b796fb1e..c8f7d100a398 100644 --- a/core/java/android/app/servertransaction/TransactionExecutor.java +++ b/core/java/android/app/servertransaction/TransactionExecutor.java @@ -226,7 +226,8 @@ public class TransactionExecutor { break; case ON_RESUME: mTransactionHandler.handleResumeActivity(r, false /* finalStateRequest */, - r.isForward, "LIFECYCLER_RESUME_ACTIVITY"); + r.isForward, false /* shouldSendCompatFakeFocus */, + "LIFECYCLER_RESUME_ACTIVITY"); break; case ON_PAUSE: mTransactionHandler.handlePauseActivity(r, false /* finished */, diff --git a/core/java/android/app/servertransaction/TransactionExecutorHelper.java b/core/java/android/app/servertransaction/TransactionExecutorHelper.java index cb6aa09cc6db..5311b09e609d 100644 --- a/core/java/android/app/servertransaction/TransactionExecutorHelper.java +++ b/core/java/android/app/servertransaction/TransactionExecutorHelper.java @@ -202,7 +202,8 @@ public class TransactionExecutorHelper { lifecycleItem = StopActivityItem.obtain(0 /* configChanges */); break; default: - lifecycleItem = ResumeActivityItem.obtain(false /* isForward */); + lifecycleItem = ResumeActivityItem.obtain(false /* isForward */, + false /* shouldSendCompatFakeFocus */); break; } diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 22ea9f2094cb..9ab7cf9a8fc6 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -60,62 +60,73 @@ interface IVirtualDevice { /** * Closes the virtual device and frees all associated resources. */ + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void close(); /** * Notifies of an audio session being started. */ + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void onAudioSessionStarting( int displayId, IAudioRoutingCallback routingCallback, IAudioConfigChangedCallback configChangedCallback); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void onAudioSessionEnded(); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void createVirtualDpad( in VirtualDpadConfig config, IBinder token); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void createVirtualKeyboard( in VirtualKeyboardConfig config, IBinder token); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void createVirtualMouse( in VirtualMouseConfig config, IBinder token); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void createVirtualTouchscreen( in VirtualTouchscreenConfig config, IBinder token); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void createVirtualNavigationTouchpad( in VirtualNavigationTouchpadConfig config, IBinder token); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void unregisterInputDevice(IBinder token); int getInputDeviceId(IBinder token); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event); + @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event); /** * Creates a virtual sensor, capable of injecting sensor events into the system. */ - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void createVirtualSensor(IBinder tokenm, in VirtualSensorConfig config); /** * Removes the sensor corresponding to the given token from the system. */ - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void unregisterSensor(IBinder token); /** * Sends an event to the virtual sensor corresponding to the given token. */ - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event); /** @@ -126,6 +137,7 @@ interface IVirtualDevice { PointF getCursorPosition(IBinder token); /** Sets whether to show or hide the cursor while this virtual device is active. */ + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void setShowPointerIcon(boolean showPointerIcon); /** @@ -133,9 +145,9 @@ interface IVirtualDevice { * when matching the provided IntentFilter and calls the callback with the intercepted * intent. */ - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void registerIntentInterceptor( in IVirtualDeviceIntentInterceptor intentInterceptor, in IntentFilter filter); - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + @EnforcePermission("CREATE_VIRTUAL_DEVICE") void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor); } diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 856101875b04..adf59fbc3776 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -775,13 +775,11 @@ public final class VirtualDeviceManager { final Point size = new Point(); display.getDisplay().getSize(size); VirtualTouchscreenConfig touchscreenConfig = - new VirtualTouchscreenConfig.Builder() + new VirtualTouchscreenConfig.Builder(size.x, size.y) .setVendorId(vendorId) .setProductId(productId) .setInputDeviceName(inputDeviceName) .setAssociatedDisplayId(display.getDisplay().getDisplayId()) - .setWidthInPixels(size.x) - .setHeightInPixels(size.y) .build(); return createVirtualTouchscreen(touchscreenConfig); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 9c25c326190e..7d7232e36d72 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -40,6 +40,7 @@ import android.app.Activity; import android.app.ActivityManager; import android.app.BroadcastOptions; import android.app.GameManager; +import android.app.GrammaticalInflectionManager; import android.app.IApplicationThread; import android.app.IServiceConnection; import android.app.VrManager; @@ -3973,6 +3974,8 @@ public abstract class Context { CREDENTIAL_SERVICE, DEVICE_LOCK_SERVICE, VIRTUALIZATION_SERVICE, + GRAMMATICAL_INFLECTION_SERVICE, + }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -6169,6 +6172,14 @@ public abstract class Context { public static final String VIRTUALIZATION_SERVICE = "virtualization"; /** + * Use with {@link #getSystemService(String)} to retrieve a + * {@link GrammaticalInflectionManager}. + * + * @see #getSystemService(String) + */ + public static final String GRAMMATICAL_INFLECTION_SERVICE = "grammatical_inflection"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * @@ -7280,7 +7291,9 @@ public abstract class Context { * Updates the device ID association of this Context. Since a Context created with * {@link #createDeviceContext} cannot change its device association, this method must * not be called for instances created with {@link #createDeviceContext}. - * + *<p> + * Note that updating the deviceId of the Context will not update its associated display. + *</p> * @param deviceId The new device ID to assign to this Context. * @throws UnsupportedOperationException if the method is called on an instance that was * created with {@link Context#createDeviceContext(int)} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 8aa0454102f3..7ee8f604dd4f 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -66,6 +66,7 @@ import android.provider.DocumentsContract; import android.provider.DocumentsProvider; import android.provider.MediaStore; import android.provider.OpenableColumns; +import android.service.chooser.ChooserAction; import android.telecom.PhoneAccount; import android.telecom.TelecomManager; import android.text.TextUtils; @@ -1814,8 +1815,8 @@ public class Intent implements Parcelable, Cloneable { * Package manager install result code. @hide because result codes are not * yet ready to be exposed. */ - public static final String EXTRA_INSTALL_RESULT - = "android.intent.extra.INSTALL_RESULT"; + @SystemApi + public static final String EXTRA_INSTALL_RESULT = "android.intent.extra.INSTALL_RESULT"; /** * Activity Action: Launch application uninstaller. @@ -1841,6 +1842,7 @@ public class Intent implements Parcelable, Cloneable { * Specify whether the package should be uninstalled for all users. * @hide because these should not be part of normal application flow. */ + @SystemApi public static final String EXTRA_UNINSTALL_ALL_USERS = "android.intent.extra.UNINSTALL_ALL_USERS"; @@ -5826,6 +5828,25 @@ public class Intent implements Parcelable, Cloneable { = "android.intent.extra.CHOOSER_REFINEMENT_INTENT_SENDER"; /** + * A Parcelable[] of {@link ChooserAction} objects to provide the Android Sharesheet with + * app-specific actions to be presented to the user when invoking {@link #ACTION_CHOOSER}. + * @hide + */ + public static final String EXTRA_CHOOSER_CUSTOM_ACTIONS = + "android.intent.extra.EXTRA_CHOOSER_CUSTOM_ACTIONS"; + + /** + * Optional argument to be used with {@link #ACTION_CHOOSER}. + * A {@link android.app.PendingIntent} to be sent when the user wants to do payload reselection + * in the sharesheet. + * A reselection action allows the user to return to the source app to change the content being + * shared. + * @hide + */ + public static final String EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION = + "android.intent.extra.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION"; + + /** * An {@code ArrayList} of {@code String} annotations describing content for * {@link #ACTION_CHOOSER}. * diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index dab57fda6106..68a84e830492 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -807,6 +807,7 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { CONFIG_LAYOUT_DIRECTION, CONFIG_COLOR_MODE, CONFIG_FONT_SCALE, + CONFIG_GRAMMATICAL_GENDER, }) @Retention(RetentionPolicy.SOURCE) public @interface Config {} @@ -917,6 +918,12 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final int CONFIG_COLOR_MODE = 0x4000; /** * Bit in {@link #configChanges} that indicates that the activity + * can itself handle the change to gender. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_GRAMMATICAL_GENDER = 0x8000; + /** + * Bit in {@link #configChanges} that indicates that the activity * can itself handle asset path changes. Set from the {@link android.R.attr#configChanges} * attribute. This is not a core resource configuration, but a higher-level value, so its * constant starts at the high bits. @@ -946,7 +953,6 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { * not a core resource configuration, but a higher-level value, so its * constant starts at the high bits. */ - public static final int CONFIG_FONT_WEIGHT_ADJUSTMENT = 0x10000000; /** @hide diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 84811ea304c6..94c5e25b25f9 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -18,9 +18,11 @@ package android.content.pm; import static android.os.Build.VERSION_CODES.DONUT; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; @@ -2253,6 +2255,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * @hide */ + @SystemApi + @RequiresPermission(Manifest.permission.DELETE_PACKAGES) public boolean hasFragileUserData() { return (privateFlags & PRIVATE_FLAG_HAS_FRAGILE_USER_DATA) != 0; } @@ -2487,8 +2491,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { return (privateFlags & ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY) != 0; } - /** @hide */ + /** + * @return {@code true} if the application is permitted to hold privileged permissions. + * + * @hide */ @TestApi + @SystemApi + @RequiresPermission(Manifest.permission.INSTALL_PACKAGES) public boolean isPrivilegedApp() { return (privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0; } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index febdaed276c6..703a92523cf5 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -26,6 +26,9 @@ import static android.content.pm.Checksum.TYPE_WHOLE_MERKLE_ROOT_4K_SHA256; import static android.content.pm.Checksum.TYPE_WHOLE_SHA1; import static android.content.pm.Checksum.TYPE_WHOLE_SHA256; import static android.content.pm.Checksum.TYPE_WHOLE_SHA512; +import static android.content.pm.PackageInfo.INSTALL_LOCATION_AUTO; +import static android.content.pm.PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; +import static android.content.pm.PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL; import android.Manifest; import android.annotation.CallbackExecutor; @@ -48,6 +51,10 @@ import android.content.IntentSender; import android.content.pm.PackageManager.DeleteFlags; import android.content.pm.PackageManager.InstallReason; import android.content.pm.PackageManager.InstallScenario; +import android.content.pm.parsing.ApkLiteParseUtils; +import android.content.pm.parsing.PackageLite; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; import android.graphics.Bitmap; import android.icu.util.ULocale; import android.net.Uri; @@ -70,12 +77,14 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.ExceptionUtils; +import com.android.internal.content.InstallLocationUtils; import com.android.internal.util.DataClass; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.internal.util.function.pooled.PooledLambda; import java.io.Closeable; +import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -172,10 +181,22 @@ public class PackageInstaller { public static final String ACTION_SESSION_UPDATED = "android.content.pm.action.SESSION_UPDATED"; - /** {@hide} */ + /** + * Intent action to indicate that user action is required for current install. This action can + * be used only by system apps. + * + * @hide + */ + @SystemApi public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL"; - /** @hide */ + /** + * Activity Action: Intent sent to the installer when a session for requesting + * user pre-approval, and user needs to confirm the installation. + * + * @hide + */ + @SystemApi public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL"; @@ -272,11 +293,23 @@ public class PackageInstaller { @Deprecated public static final String EXTRA_PACKAGE_NAMES = "android.content.pm.extra.PACKAGE_NAMES"; - /** {@hide} */ + /** + * The status as used internally in the package manager. Refer to {@link PackageManager} for + * a list of all valid legacy statuses. + * + * @hide + */ + @SystemApi public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS"; /** {@hide} */ public static final String EXTRA_LEGACY_BUNDLE = "android.content.pm.extra.LEGACY_BUNDLE"; - /** {@hide} */ + /** + * The callback to execute once an uninstall is completed (used for both successful and + * unsuccessful uninstalls). + * + * @hide + */ + @SystemApi public static final String EXTRA_CALLBACK = "android.content.pm.extra.CALLBACK"; /** @@ -293,6 +326,17 @@ public class PackageInstaller { public static final String EXTRA_DATA_LOADER_TYPE = "android.content.pm.extra.DATA_LOADER_TYPE"; /** + * Path to the validated base APK for this session, which may point at an + * APK inside the session (when the session defines the base), or it may + * point at the existing base APK (when adding splits to an existing app). + * + * @hide + */ + @SystemApi + public static final String EXTRA_RESOLVED_BASE_PATH = + "android.content.pm.extra.RESOLVED_BASE_PATH"; + + /** * Streaming installation pending. * Caller should make sure DataLoader is able to prepare image and reinitiate the operation. * @@ -796,8 +840,6 @@ public class PackageInstaller { * @param statusReceiver Where to deliver the result of the operation indicated by the extra * {@link #EXTRA_STATUS}. Refer to the individual status codes * on how to handle them. - * - * @hide */ @RequiresPermission(anyOf = { Manifest.permission.DELETE_PACKAGES, @@ -1871,6 +1913,101 @@ public class PackageInstaller { } /** + * Parse a single APK or a directory of APKs to get install relevant information about + * the package wrapped in {@link InstallInfo}. + * @throws PackageParsingException if the package source file(s) provided is(are) not valid, + * or the parser isn't able to parse the supplied source(s). + * @hide + */ + @SystemApi + @NonNull + public InstallInfo getInstallInfo(@NonNull File file, int flags) + throws PackageParsingException { + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite( + input.reset(), file, flags); + if (result.isError()) { + throw new PackageParsingException(result.getErrorCode(), result.getErrorMessage()); + } + return new InstallInfo(result); + } + + // (b/239722738) This class serves as a bridge between the PackageLite class, which + // is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java) + // This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or + // public APIs. + /** + * Install related details from an APK or a folder of APK(s). + * + * @hide + */ + @SystemApi + public static class InstallInfo { + + /** @hide */ + @IntDef(prefix = { "INSTALL_LOCATION_" }, value = { + INSTALL_LOCATION_AUTO, + INSTALL_LOCATION_INTERNAL_ONLY, + INSTALL_LOCATION_PREFER_EXTERNAL + }) + @Retention(RetentionPolicy.SOURCE) + public @interface InstallLocation{} + + private PackageLite mPkg; + + InstallInfo(ParseResult<PackageLite> result) { + mPkg = result.getResult(); + } + + /** + * See {@link PackageLite#getPackageName()} + */ + @NonNull + public String getPackageName() { + return mPkg.getPackageName(); + } + + /** + * @return The default install location defined by an application in + * {@link android.R.attr#installLocation} attribute. + */ + public @InstallLocation int getInstallLocation() { + return mPkg.getInstallLocation(); + } + + /** + * @param params {@link SessionParams} of the installation + * @return Total disk space occupied by an application after installation. + * Includes the size of the raw APKs, possibly unpacked resources, raw dex metadata files, + * and all relevant native code. + * @throws IOException when size of native binaries cannot be calculated. + */ + public long calculateInstalledSize(@NonNull SessionParams params) throws IOException { + return InstallLocationUtils.calculateInstalledSize(mPkg, params.abiOverride); + } + } + + /** + * Generic exception class for using with parsing operations. + * + * @hide + */ + @SystemApi + public static class PackageParsingException extends Exception { + private final int mErrorCode; + + /** {@hide} */ + public PackageParsingException(int errorCode, @Nullable String detailedMessage) { + super(detailedMessage); + mErrorCode = errorCode; + } + + public int getErrorCode() { + return mErrorCode; + } + } + + /** * Parameters for creating a new {@link PackageInstaller.Session}. */ public static class SessionParams implements Parcelable { @@ -2431,9 +2568,7 @@ public class PackageInstaller { * By default this is the app that created the {@link PackageInstaller} object. * * @param installerPackageName name of the installer package - * {@hide} */ - @TestApi public void setInstallerPackageName(@Nullable String installerPackageName) { this.installerPackageName = installerPackageName; } @@ -3430,8 +3565,6 @@ public class PackageInstaller { /** * Returns the Uid of the owner of the session. - * - * @hide */ public int getInstallerUid() { return installerUid; @@ -3445,6 +3578,13 @@ public class PackageInstaller { return keepApplicationEnabledSetting; } + /** + * Returns whether this session has requested user pre-approval. + */ + public @NonNull boolean getIsPreApprovalRequested() { + return isPreapprovalRequested; + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index cbdcc0226eac..4ad657e64fb3 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2250,6 +2250,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int DELETE_KEEP_DATA = 0x00000001; /** @@ -2258,6 +2259,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int DELETE_ALL_USERS = 0x00000002; /** @@ -2295,6 +2297,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int DELETE_SUCCEEDED = 1; /** @@ -2304,6 +2307,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int DELETE_FAILED_INTERNAL_ERROR = -1; /** @@ -2313,6 +2317,7 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int DELETE_FAILED_DEVICE_POLICY_MANAGER = -2; /** @@ -2332,9 +2337,11 @@ public abstract class PackageManager { * * @hide */ + @SystemApi public static final int DELETE_FAILED_OWNER_BLOCKED = -4; /** {@hide} */ + @SystemApi public static final int DELETE_FAILED_ABORTED = -5; /** @@ -4090,6 +4097,17 @@ public abstract class PackageManager { public static final String FEATURE_IPSEC_TUNNELS = "android.software.ipsec_tunnels"; /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: The device has + * the requisite kernel support for migrating IPsec tunnels to new source/destination addresses. + * + * <p>This feature implies that the device supports XFRM Migration (CONFIG_XFRM_MIGRATE) and has + * the kernel fixes to support cross-address-family IPsec tunnel migration + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_IPSEC_TUNNEL_MIGRATION = + "android.software.ipsec_tunnel_migration"; + + /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device supports a system interface for the user to select * and bind device control services provided by applications. @@ -5336,6 +5354,17 @@ public abstract class PackageManager { throws NameNotFoundException; /** + * See {@link #getPackageUidAsUser(String, PackageInfoFlags, int)}. + * @deprecated Use {@link #getPackageUidAsUser(String, PackageInfoFlags, int)} instead. + * @hide + */ + @Deprecated + @SuppressWarnings("HiddenAbstractMethod") + @UnsupportedAppUsage + public abstract int getPackageUidAsUser(@NonNull String packageName, + int flags, @UserIdInt int userId) throws NameNotFoundException; + + /** * Return the UID associated with the given package name. * <p> * Note that the same package will have different UIDs under different @@ -5343,23 +5372,15 @@ public abstract class PackageManager { * * @param packageName The full name (i.e. com.google.apps.contacts) of the * desired package. + * @param flags Additional option flags to modify the data returned. * @param userId The user handle identifier to look up the package under. * @return Returns an integer UID who owns the given package name. * @throws NameNotFoundException if no such package is available to the * caller. - * @deprecated Use {@link #getPackageUidAsUser(String, PackageInfoFlags, int)} instead. - * @hide - */ - @Deprecated - @SuppressWarnings("HiddenAbstractMethod") - @UnsupportedAppUsage - public abstract int getPackageUidAsUser(@NonNull String packageName, - int flags, @UserIdInt int userId) throws NameNotFoundException; - - /** - * See {@link #getPackageUidAsUser(String, int, int)}. * @hide */ + @SystemApi + @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public int getPackageUidAsUser(@NonNull String packageName, @NonNull PackageInfoFlags flags, @UserIdInt int userId) throws NameNotFoundException { throw new UnsupportedOperationException( @@ -9805,6 +9826,83 @@ public abstract class PackageManager { } /** + * A parcelable class to pass as an intent extra to the PackageInstaller. When an uninstall is + * completed (both successfully or unsuccessfully), the result is sent to the uninstall + * initiators. + * + * @hide + */ + @SystemApi + public static final class UninstallCompleteCallback implements Parcelable { + private IPackageDeleteObserver2 mBinder; + + /** @hide */ + @IntDef(prefix = { "DELETE_" }, value = { + DELETE_SUCCEEDED, + DELETE_FAILED_INTERNAL_ERROR, + DELETE_FAILED_DEVICE_POLICY_MANAGER, + DELETE_FAILED_USER_RESTRICTED, + DELETE_FAILED_OWNER_BLOCKED, + DELETE_FAILED_ABORTED, + DELETE_FAILED_USED_SHARED_LIBRARY, + DELETE_FAILED_APP_PINNED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DeleteStatus{} + + /** @hide */ + public UninstallCompleteCallback(@NonNull IBinder binder) { + mBinder = IPackageDeleteObserver2.Stub.asInterface(binder); + } + + /** @hide */ + private UninstallCompleteCallback(Parcel in) { + mBinder = IPackageDeleteObserver2.Stub.asInterface(in.readStrongBinder()); + } + + public static final @NonNull Parcelable.Creator<UninstallCompleteCallback> CREATOR = + new Parcelable.Creator<>() { + public UninstallCompleteCallback createFromParcel(Parcel source) { + return new UninstallCompleteCallback(source); + } + + public UninstallCompleteCallback[] newArray(int size) { + return new UninstallCompleteCallback[size]; + } + }; + + /** + * Called when an uninstallation is completed successfully or unsuccessfully. + * + * @param packageName The name of the package being uninstalled. + * @param resultCode Result code of the operation. + * @param errorMessage Error message if any. + * + * @hide */ + @SystemApi + public void onUninstallComplete(@NonNull String packageName, @DeleteStatus int resultCode, + @Nullable String errorMessage) { + try { + mBinder.onPackageDeleted(packageName, resultCode, errorMessage); + } catch (RemoteException e) { + // no-op + } + } + + /** @hide */ + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeStrongBinder(mBinder.asBinder()); + } + } + + /** * Return the install reason that was recorded when a package was first * installed for a specific user. Requesting the install reason for another * user will require the permission INTERACT_ACROSS_USERS_FULL. @@ -10723,4 +10821,33 @@ public abstract class PackageManager { throw e.rethrowFromSystemServer(); } } + + /** + * Checks if a package is blocked from uninstall for a particular user. A package can be + * blocked from being uninstalled by a device owner or profile owner. + * See {@link DevicePolicyManager#setUninstallBlocked(ComponentName, String, boolean)}. + * + * @param packageName Name of the package being uninstalled. + * @param user UserHandle who's ability to uninstall a package is being checked. + * + * @hide + */ + @SystemApi + @NonNull + public boolean canUserUninstall(@NonNull String packageName, @NonNull UserHandle user){ + throw new UnsupportedOperationException( + "canUserUninstall not implemented in subclass"); + } + + /** + * See {@link android.provider.Settings.Global#SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED}. + * + * @hide + */ + @SystemApi + @NonNull + public boolean shouldShowNewAppInstalledNotification() { + throw new UnsupportedOperationException( + "isShowNewAppInstalledNotificationEnabled not implemented in subclass"); + } } diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index f47c1e0723b5..96aa6249245c 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -46,6 +46,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; +import android.app.GrammaticalInflectionManager; import android.app.WindowConfiguration; import android.compat.annotation.UnsupportedAppUsage; import android.content.LocaleProto; @@ -141,6 +142,44 @@ public final class Configuration implements Parcelable, Comparable<Configuration @UnsupportedAppUsage public boolean userSetLocale; + /** + * Current user preference for the grammatical gender. + */ + @GrammaticalGender + private int mGrammaticalGender; + + /** @hide */ + @IntDef(prefix = { "GRAMMATICAL_GENDER_" }, value = { + GRAMMATICAL_GENDER_NOT_SPECIFIED, + GRAMMATICAL_GENDER_NEUTRAL, + GRAMMATICAL_GENDER_FEMININE, + GRAMMATICAL_GENDER_MASCULINE, + }) + public @interface GrammaticalGender {} + + /** + * Constant for grammatical gender: to indicate the user has not specified the terms + * of address for the application. + */ + public static final int GRAMMATICAL_GENDER_NOT_SPECIFIED = 0; + + /** + * Constant for grammatical gender: to indicate the terms of address the user + * preferred in an application is neuter. + */ + public static final int GRAMMATICAL_GENDER_NEUTRAL = 2; + + /** + * Constant for grammatical gender: to indicate the terms of address the user + * preferred in an application is feminine. + */ + public static final int GRAMMATICAL_GENDER_FEMININE = 3; + + /** + * Constant for grammatical gender: to indicate the terms of address the user + * preferred in an application is masculine. + */ + public static final int GRAMMATICAL_GENDER_MASCULINE = 4; /** Constant for {@link #colorMode}: bits that encode whether the screen is wide gamut. */ public static final int COLOR_MODE_WIDE_COLOR_GAMUT_MASK = 0x3; @@ -1024,6 +1063,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration } o.fixUpLocaleList(); mLocaleList = o.mLocaleList; + mGrammaticalGender = o.mGrammaticalGender; userSetLocale = o.userSetLocale; touchscreen = o.touchscreen; keyboard = o.keyboard; @@ -1510,6 +1550,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration seq = 0; windowConfiguration.setToDefaults(); fontWeightAdjustment = FONT_WEIGHT_ADJUSTMENT_UNDEFINED; + mGrammaticalGender = GRAMMATICAL_GENDER_NOT_SPECIFIED; } /** @@ -1712,6 +1753,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT; fontWeightAdjustment = delta.fontWeightAdjustment; } + if (delta.mGrammaticalGender != mGrammaticalGender) { + changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER; + mGrammaticalGender = delta.mGrammaticalGender; + } return changed; } @@ -1929,6 +1974,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration && fontWeightAdjustment != delta.fontWeightAdjustment) { changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT; } + + if (!publicOnly&& mGrammaticalGender != delta.mGrammaticalGender) { + changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER; + } return changed; } @@ -2023,6 +2072,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(assetsSeq); dest.writeInt(seq); dest.writeInt(fontWeightAdjustment); + dest.writeInt(mGrammaticalGender); } public void readFromParcel(Parcel source) { @@ -2055,6 +2105,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration assetsSeq = source.readInt(); seq = source.readInt(); fontWeightAdjustment = source.readInt(); + mGrammaticalGender = source.readInt(); } public static final @android.annotation.NonNull Parcelable.Creator<Configuration> CREATOR @@ -2155,6 +2206,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration if (n != 0) return n; n = this.fontWeightAdjustment - that.fontWeightAdjustment; if (n != 0) return n; + n = this.mGrammaticalGender - that.mGrammaticalGender; + if (n != 0) return n; // if (n != 0) return n; return n; @@ -2196,10 +2249,37 @@ public final class Configuration implements Parcelable, Comparable<Configuration result = 31 * result + densityDpi; result = 31 * result + assetsSeq; result = 31 * result + fontWeightAdjustment; + result = 31 * result + mGrammaticalGender; return result; } /** + * Returns the user preference for the grammatical gender. Will be + * {@link #GRAMMATICAL_GENDER_NOT_SPECIFIED} or + * {@link #GRAMMATICAL_GENDER_NEUTRAL} or + * {@link #GRAMMATICAL_GENDER_FEMININE} or + * {@link #GRAMMATICAL_GENDER_MASCULINE}. + * + * @return The preferred grammatical gender. + */ + @GrammaticalGender + public int getGrammaticalGender() { + return mGrammaticalGender; + } + + /** + * Sets the user preference for the grammatical gender. This is only for frameworks to easily + * override the gender in the configuration. To update the grammatical gender for an application + * use {@link GrammaticalInflectionManager#setRequestedApplicationGrammaticalGender(int)}. + * + * @param grammaticalGender The preferred grammatical gender. + * @hide + */ + public void setGrammaticalGender(@GrammaticalGender int grammaticalGender) { + mGrammaticalGender = grammaticalGender; + } + + /** * Get the locale list. This is the preferred way for getting the locales (instead of using * the direct accessor to {@link #locale}, which would only provide the primary locale). * diff --git a/core/java/android/hardware/DataSpace.java b/core/java/android/hardware/DataSpace.java index 0a145746d303..b8b1eaaa164c 100644 --- a/core/java/android/hardware/DataSpace.java +++ b/core/java/android/hardware/DataSpace.java @@ -521,9 +521,7 @@ public final class DataSpace { public static final int DATASPACE_BT2020_HLG = 168165376; /** - * ITU-R Recommendation 2020 (BT.2020) - * - * Ultra High-definition television. + * Perceptual Quantizer encoding. * * <p>Composed of the following -</p> * <pre> diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 9c4216032942..655e5981971f 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -166,6 +166,14 @@ public final class InputManager { * The <code>android:keyboardLayout</code> attribute refers to a * <a href="http://source.android.com/tech/input/key-character-map-files.html"> * key character map</a> resource that defines the keyboard layout. + * The <code>android:keyboardLocale</code> attribute specifies a comma separated list of BCP 47 + * language tags depicting the locales supported by the keyboard layout. This attribute is + * optional and will be used for auto layout selection for external physical keyboards. + * The <code>android:keyboardLayoutType</code> attribute specifies the layoutType for the + * keyboard layout. This can be either empty or one of the following supported layout types: + * qwerty, qwertz, azerty, dvorak, colemak, workman, extended, turkish_q, turkish_f. This + * attribute is optional and will be used for auto layout selection for external physical + * keyboards. * </p> */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) @@ -705,6 +713,30 @@ public final class InputManager { } /** + * Returns the layout type of the queried layout + * <p> + * The input manager consults the built-in keyboard layouts as well as all keyboard layouts + * advertised by applications using a {@link #ACTION_QUERY_KEYBOARD_LAYOUTS} broadcast receiver. + * </p> + * + * @param layoutDescriptor The layout descriptor of the queried layout + * @return layout type of the queried layout + * + * @hide + */ + @TestApi + @NonNull + public String getKeyboardLayoutTypeForLayoutDescriptor(@NonNull String layoutDescriptor) { + KeyboardLayout[] layouts = getKeyboardLayouts(); + for (KeyboardLayout kl : layouts) { + if (layoutDescriptor.equals(kl.getDescriptor())) { + return kl.getLayoutType(); + } + } + return ""; + } + + /** * Gets information about all supported keyboard layouts appropriate * for a specific input device. * <p> diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java index 52c15519907a..58f7759551e8 100644 --- a/core/java/android/hardware/input/KeyboardLayout.java +++ b/core/java/android/hardware/input/KeyboardLayout.java @@ -21,24 +21,74 @@ import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; +import java.util.HashMap; +import java.util.Map; + /** * Describes a keyboard layout. * * @hide */ -public final class KeyboardLayout implements Parcelable, - Comparable<KeyboardLayout> { +public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayout> { private final String mDescriptor; private final String mLabel; private final String mCollection; private final int mPriority; @NonNull private final LocaleList mLocales; + private final LayoutType mLayoutType; private final int mVendorId; private final int mProductId; - public static final @android.annotation.NonNull Parcelable.Creator<KeyboardLayout> CREATOR = - new Parcelable.Creator<KeyboardLayout>() { + /** Currently supported Layout types in the KCM files */ + private enum LayoutType { + UNDEFINED(0, "undefined"), + QWERTY(1, "qwerty"), + QWERTZ(2, "qwertz"), + AZERTY(3, "azerty"), + DVORAK(4, "dvorak"), + COLEMAK(5, "colemak"), + WORKMAN(6, "workman"), + TURKISH_F(7, "turkish_f"), + TURKISH_Q(8, "turkish_q"), + EXTENDED(9, "extended"); + + private final int mValue; + private final String mName; + private static final Map<Integer, LayoutType> VALUE_TO_ENUM_MAP = new HashMap<>(); + static { + VALUE_TO_ENUM_MAP.put(UNDEFINED.mValue, UNDEFINED); + VALUE_TO_ENUM_MAP.put(QWERTY.mValue, QWERTY); + VALUE_TO_ENUM_MAP.put(QWERTZ.mValue, QWERTZ); + VALUE_TO_ENUM_MAP.put(AZERTY.mValue, AZERTY); + VALUE_TO_ENUM_MAP.put(DVORAK.mValue, DVORAK); + VALUE_TO_ENUM_MAP.put(COLEMAK.mValue, COLEMAK); + VALUE_TO_ENUM_MAP.put(WORKMAN.mValue, WORKMAN); + VALUE_TO_ENUM_MAP.put(TURKISH_F.mValue, TURKISH_F); + VALUE_TO_ENUM_MAP.put(TURKISH_Q.mValue, TURKISH_Q); + VALUE_TO_ENUM_MAP.put(EXTENDED.mValue, EXTENDED); + } + + private static LayoutType of(int value) { + return VALUE_TO_ENUM_MAP.getOrDefault(value, UNDEFINED); + } + + LayoutType(int value, String name) { + this.mValue = value; + this.mName = name; + } + + private int getValue() { + return mValue; + } + + private String getName() { + return mName; + } + } + + @NonNull + public static final Parcelable.Creator<KeyboardLayout> CREATOR = new Parcelable.Creator<>() { public KeyboardLayout createFromParcel(Parcel source) { return new KeyboardLayout(source); } @@ -48,12 +98,13 @@ public final class KeyboardLayout implements Parcelable, }; public KeyboardLayout(String descriptor, String label, String collection, int priority, - LocaleList locales, int vid, int pid) { + LocaleList locales, int layoutValue, int vid, int pid) { mDescriptor = descriptor; mLabel = label; mCollection = collection; mPriority = priority; mLocales = locales; + mLayoutType = LayoutType.of(layoutValue); mVendorId = vid; mProductId = pid; } @@ -64,6 +115,7 @@ public final class KeyboardLayout implements Parcelable, mCollection = source.readString(); mPriority = source.readInt(); mLocales = LocaleList.CREATOR.createFromParcel(source); + mLayoutType = LayoutType.of(source.readInt()); mVendorId = source.readInt(); mProductId = source.readInt(); } @@ -106,6 +158,15 @@ public final class KeyboardLayout implements Parcelable, } /** + * Gets the layout type that this keyboard layout is intended for. + * This may be "undefined" if a layoutType has not been assigned to this keyboard layout. + * @return The keyboard layout's intended layout type. + */ + public String getLayoutType() { + return mLayoutType.getName(); + } + + /** * Gets the vendor ID of the hardware device this keyboard layout is intended for. * Returns -1 if this is not specific to any piece of hardware. * @return The hardware vendor ID of the keyboard layout's intended device. @@ -135,6 +196,7 @@ public final class KeyboardLayout implements Parcelable, dest.writeString(mCollection); dest.writeInt(mPriority); mLocales.writeToParcel(dest, 0); + dest.writeInt(mLayoutType.getValue()); dest.writeInt(mVendorId); dest.writeInt(mProductId); } @@ -160,6 +222,7 @@ public final class KeyboardLayout implements Parcelable, + ", descriptor: " + mDescriptor + ", priority: " + mPriority + ", locales: " + mLocales.toString() + + ", layout type: " + mLayoutType.getName() + ", vendorId: " + mVendorId + ", productId: " + mProductId; } diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java index f2805bb1029e..5ad5fd93fd0a 100644 --- a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java +++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java @@ -88,12 +88,17 @@ public final class VirtualNavigationTouchpadConfig extends VirtualInputDeviceCon * Builder for creating a {@link VirtualNavigationTouchpadConfig}. */ public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> { - private final int mHeight; private final int mWidth; - public Builder(@IntRange(from = 1) int touchpadHeight, - @IntRange(from = 1) int touchpadWidth) { + /** + * Creates a new instance for the given dimensions of the {@link VirtualNavigationTouchpad}. + * + * @param touchpadWidth The width of the touchpad. + * @param touchpadHeight The height of the touchpad. + */ + public Builder(@IntRange(from = 1) int touchpadWidth, + @IntRange(from = 1) int touchpadHeight) { if (touchpadHeight <= 0 || touchpadWidth <= 0) { throw new IllegalArgumentException( "Cannot create a virtual navigation touchpad, touchpad dimensions must be " diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java index e358619baf71..aac341ccb5ed 100644 --- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java +++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java @@ -16,6 +16,7 @@ package android.hardware.input; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; import android.os.Parcel; @@ -29,31 +30,31 @@ import android.os.Parcelable; @SystemApi public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable { - /** The touchscreen width in pixels. */ - private final int mWidthInPixels; - /** The touchscreen height in pixels. */ - private final int mHeightInPixels; + /** The touchscreen width. */ + private final int mWidth; + /** The touchscreen height. */ + private final int mHeight; private VirtualTouchscreenConfig(@NonNull Builder builder) { super(builder); - mWidthInPixels = builder.mWidthInPixels; - mHeightInPixels = builder.mHeightInPixels; + mWidth = builder.mWidth; + mHeight = builder.mHeight; } private VirtualTouchscreenConfig(@NonNull Parcel in) { super(in); - mWidthInPixels = in.readInt(); - mHeightInPixels = in.readInt(); + mWidth = in.readInt(); + mHeight = in.readInt(); } - /** Returns the touchscreen width in pixels. */ - public int getWidthInPixels() { - return mWidthInPixels; + /** Returns the touchscreen width. */ + public int getWidth() { + return mWidth; } - /** Returns the touchscreen height in pixels. */ - public int getHeightInPixels() { - return mHeightInPixels; + /** Returns the touchscreen height. */ + public int getHeight() { + return mHeight; } @Override @@ -64,8 +65,8 @@ public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig imp @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); - dest.writeInt(mWidthInPixels); - dest.writeInt(mHeightInPixels); + dest.writeInt(mWidth); + dest.writeInt(mHeight); } @NonNull @@ -86,25 +87,29 @@ public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig imp * Builder for creating a {@link VirtualTouchscreenConfig}. */ public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> { - private int mWidthInPixels; - private int mHeightInPixels; + private int mWidth; + private int mHeight; /** - * @see VirtualTouchscreenConfig#getWidthInPixels(). + * Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}. + * + * <p>The dimensions are not pixels but in the touchscreens raw coordinate space. They do + * not necessarily have to correspond to the display size or aspect ratio. In this case the + * framework will handle the scaling appropriately. + * + * @param touchscreenWidth The width of the touchscreen. + * @param touchscreenHeight The height of the touchscreen. */ - @NonNull - public Builder setWidthInPixels(int widthInPixels) { - mWidthInPixels = widthInPixels; - return this; - } - - /** - * @see VirtualTouchscreenConfig#getHeightInPixels(). - */ - @NonNull - public Builder setHeightInPixels(int heightInPixels) { - mHeightInPixels = heightInPixels; - return this; + public Builder(@IntRange(from = 1) int touchscreenWidth, + @IntRange(from = 1) int touchscreenHeight) { + if (touchscreenHeight <= 0 || touchscreenWidth <= 0) { + throw new IllegalArgumentException( + "Cannot create a virtual touchscreen, touchscreen dimensions must be " + + "positive. Got: (" + touchscreenHeight + ", " + + touchscreenWidth + ")"); + } + mHeight = touchscreenHeight; + mWidth = touchscreenWidth; } /** diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java index 7faa285a8fee..727716e67f28 100644 --- a/core/java/android/hardware/radio/ProgramSelector.java +++ b/core/java/android/hardware/radio/ProgramSelector.java @@ -65,12 +65,12 @@ public final class ProgramSelector implements Parcelable { */ @Deprecated public static final int PROGRAM_TYPE_INVALID = 0; - /** Analogue AM radio (with or without RDS). + /** Analog AM radio (with or without RDS). * @deprecated use {@link ProgramIdentifier} instead */ @Deprecated public static final int PROGRAM_TYPE_AM = 1; - /** analogue FM radio (with or without RDS). + /** analog FM radio (with or without RDS). * @deprecated use {@link ProgramIdentifier} instead */ @Deprecated @@ -125,25 +125,50 @@ public final class ProgramSelector implements Parcelable { public @interface ProgramType {} public static final int IDENTIFIER_TYPE_INVALID = 0; - /** kHz */ + /** + * Primary identifier for analog (without RDS) AM/FM stations: + * frequency in kHz. + * + * <p>This identifier also contains band information: + * <li> + * <ul><500kHz: AM LW. + * <ul>500kHz - 1705kHz: AM MW. + * <ul>1.71MHz - 30MHz: AM SW. + * <ul>>60MHz: FM. + * </li> + */ public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; - /** 16bit */ + /** + * 16bit primary identifier for FM RDS station. + */ public static final int IDENTIFIER_TYPE_RDS_PI = 2; /** * 64bit compound primary identifier for HD Radio. * - * Consists of (from the LSB): - * - 32bit: Station ID number; - * - 4bit: HD_SUBCHANNEL; - * - 18bit: AMFM_FREQUENCY. - * The remaining bits should be set to zeros when writing on the chip side + * <p>Consists of (from the LSB): + * <li> + * <ul>132bit: Station ID number. + * <ul>14bit: HD_SUBCHANNEL. + * <ul>18bit: AMFM_FREQUENCY. + * </li> + * + * <p>While station ID number should be unique globally, it sometimes gets + * abused by broadcasters (i.e. not being set at all). To ensure local + * uniqueness, AMFM_FREQUENCY_KHZ was added here. Global uniqueness is + * a best-effort - see {@link IDENTIFIER_TYPE_HD_STATION_NAME}. + * + * <p>HD Radio subchannel is a value in range of 0-7. + * This index is 0-based (where 0 is MPS and 1..7 are SPS), + * as opposed to HD Radio standard (where it's 1-based). + * + * <p>The remaining bits should be set to zeros when writing on the chip side * and ignored when read. */ public static final int IDENTIFIER_TYPE_HD_STATION_ID_EXT = 3; /** - * HD Radio subchannel - a value of range 0-7. + * HD Radio subchannel - a value in range of 0-7. * - * The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS), + * <p>The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS), * as opposed to HD Radio standard (where it's 1-based). * * @deprecated use IDENTIFIER_TYPE_HD_STATION_ID_EXT instead @@ -153,16 +178,16 @@ public final class ProgramSelector implements Parcelable { /** * 64bit additional identifier for HD Radio. * - * Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not + * <p>Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not * globally unique. To provide a best-effort solution, a short version of * station name may be carried as additional identifier and may be used * by the tuner hardware to double-check tuning. * - * The name is limited to the first 8 A-Z0-9 characters (lowercase letters - * must be converted to uppercase). Encoded in little-endian ASCII: - * the first character of the name is the LSB. + * <p>The name is limited to the first 8 A-Z0-9 characters (lowercase + * letters must be converted to uppercase). Encoded in little-endian + * ASCII: the first character of the name is the LSB. * - * For example: "Abc" is encoded as 0x434241. + * <p>For example: "Abc" is encoded as 0x434241. */ public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004; /** @@ -175,17 +200,19 @@ public final class ProgramSelector implements Parcelable { /** * 28bit compound primary identifier for Digital Audio Broadcasting. * - * Consists of (from the LSB): - * - 16bit: SId; - * - 8bit: ECC code; - * - 4bit: SCIdS. + * <p>Consists of (from the LSB): + * <li> + * <ul>16bit: SId. + * <ul>8bit: ECC code. + * <ul>4bit: SCIdS. + * </li> * - * SCIdS (Service Component Identifier within the Service) value + * <p>SCIdS (Service Component Identifier within the Service) value * of 0 represents the main service, while 1 and above represents * secondary services. * - * The remaining bits should be set to zeros when writing on the chip side - * and ignored when read. + * <p>The remaining bits should be set to zeros when writing on the chip + * side and ignored when read. * * @deprecated use {@link #IDENTIFIER_TYPE_DAB_DMB_SID_EXT} instead */ @@ -197,7 +224,9 @@ public final class ProgramSelector implements Parcelable { public static final int IDENTIFIER_TYPE_DAB_SCID = 7; /** kHz */ public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8; - /** 24bit */ + /** + * 24bit primary identifier for Digital Radio Mondiale. + */ public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; /** kHz */ public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; @@ -207,7 +236,9 @@ public final class ProgramSelector implements Parcelable { */ @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; - /** 32bit */ + /** + * 32bit primary identifier for SiriusXM Satellite Radio. + */ public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12; /** 0-999 range */ public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13; @@ -224,15 +255,15 @@ public final class ProgramSelector implements Parcelable { * of 0 represents the main service, while 1 and above represents * secondary services. * - * The remaining bits should be set to zeros when writing on the chip side - * and ignored when read. + * <p>The remaining bits should be set to zeros when writing on the chip + * side and ignored when read. */ public static final int IDENTIFIER_TYPE_DAB_DMB_SID_EXT = 14; /** * Primary identifier for vendor-specific radio technology. * The value format is determined by a vendor. * - * It must not be used in any other programType than corresponding VENDOR + * <p>It must not be used in any other programType than corresponding VENDOR * type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must * not be used in any program type other than 1015). */ diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index efe8238c8a62..b236d6632961 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -224,6 +224,7 @@ public final class Settings { * @hide */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + @SystemApi public static final String ACTION_USER_SETTINGS = "android.settings.USER_SETTINGS"; diff --git a/core/java/android/service/chooser/ChooserAction.java b/core/java/android/service/chooser/ChooserAction.java new file mode 100644 index 000000000000..3010049633d4 --- /dev/null +++ b/core/java/android/service/chooser/ChooserAction.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.chooser; + +import android.annotation.NonNull; +import android.app.PendingIntent; +import android.graphics.drawable.Icon; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.Objects; + +/** + * A ChooserAction is an app-defined action that can be provided to the Android Sharesheet to + * be shown to the user when {@link android.content.Intent.ACTION_CHOOSER} is invoked. + * + * @see android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS + * @see android.content.Intent.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION + * @hide + */ +public final class ChooserAction implements Parcelable { + private final Icon mIcon; + private final CharSequence mLabel; + private final PendingIntent mAction; + + private ChooserAction( + Icon icon, + CharSequence label, + PendingIntent action) { + mIcon = icon; + mLabel = label; + mAction = action; + } + + /** + * Return a user-readable label for this action. + */ + @NonNull + public CharSequence getLabel() { + return mLabel; + } + + /** + * Return an {@link Icon} representing this action. + */ + @NonNull + public Icon getIcon() { + return mIcon; + } + + /** + * Return the action intent. + */ + @NonNull + public PendingIntent getAction() { + return mAction; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + mIcon.writeToParcel(dest, flags); + TextUtils.writeToParcel(mLabel, dest, flags); + mAction.writeToParcel(dest, flags); + } + + @Override + public String toString() { + return "ChooserAction {" + "label=" + mLabel + ", intent=" + mAction + "}"; + } + + public static final Parcelable.Creator<ChooserAction> CREATOR = + new Creator<ChooserAction>() { + @Override + public ChooserAction createFromParcel(Parcel source) { + return new ChooserAction( + Icon.CREATOR.createFromParcel(source), + TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source), + PendingIntent.CREATOR.createFromParcel(source)); + } + + @Override + public ChooserAction[] newArray(int size) { + return new ChooserAction[size]; + } + }; + + /** + * Builder class for {@link ChooserAction} objects + */ + public static final class Builder { + private final Icon mIcon; + private final CharSequence mLabel; + private final PendingIntent mAction; + + /** + * Construct a new builder for {@link ChooserAction} object. + * + * @param icon an {@link Icon} representing this action, consisting of a white foreground + * atop a transparent background. + * @param label label the user-readable label for this action. + * @param action {@link PendingIntent} to be invoked when the action is selected. + */ + public Builder( + @NonNull Icon icon, + @NonNull CharSequence label, + @NonNull PendingIntent action) { + Objects.requireNonNull(icon, "icon can not be null"); + Objects.requireNonNull(label, "label can not be null"); + Objects.requireNonNull(action, "pending intent can not be null"); + mIcon = icon; + mLabel = label; + mAction = action; + } + + /** + * Combine all of the options that have been set and return a new {@link ChooserAction} + * object. + * @return the built action + */ + public ChooserAction build() { + return new ChooserAction(mIcon, mLabel, mAction); + } + } +} diff --git a/core/java/android/service/chooser/OWNERS b/core/java/android/service/chooser/OWNERS index a5acba58b52c..dcd4a7be08c3 100644 --- a/core/java/android/service/chooser/OWNERS +++ b/core/java/android/service/chooser/OWNERS @@ -1,4 +1,3 @@ -asc@google.com -mpietal@google.com -dsandler@android.com -dsandler@google.com +mrcasey@google.com +ayepin@google.com +joshtrask@google.com diff --git a/core/java/android/service/notification/Adjustment.java b/core/java/android/service/notification/Adjustment.java index 4b25c8832068..df185ee14e98 100644 --- a/core/java/android/service/notification/Adjustment.java +++ b/core/java/android/service/notification/Adjustment.java @@ -52,7 +52,7 @@ public final class Adjustment implements Parcelable { /** @hide */ @StringDef (prefix = { "KEY_" }, value = { KEY_CONTEXTUAL_ACTIONS, KEY_GROUP_KEY, KEY_IMPORTANCE, KEY_PEOPLE, KEY_SNOOZE_CRITERIA, - KEY_TEXT_REPLIES, KEY_USER_SENTIMENT + KEY_TEXT_REPLIES, KEY_USER_SENTIMENT, KEY_IMPORTANCE_PROPOSAL }) @Retention(RetentionPolicy.SOURCE) public @interface Keys {} @@ -122,6 +122,18 @@ public final class Adjustment implements Parcelable { public static final String KEY_IMPORTANCE = "key_importance"; /** + * Weaker than {@link #KEY_IMPORTANCE}, this adjustment suggests an importance rather than + * mandates an importance change. + * + * A notification listener can interpet this suggestion to show the user a prompt to change + * notification importance for the notification (or type, or app) moving forward. + * + * Data type: int, one of importance values e.g. + * {@link android.app.NotificationManager#IMPORTANCE_MIN}. + */ + public static final String KEY_IMPORTANCE_PROPOSAL = "key_importance_proposal"; + + /** * Data type: float, a ranking score from 0 (lowest) to 1 (highest). * Used to rank notifications inside that fall under the same classification (i.e. alerting, * silenced). diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index d113a3cc0c4b..11e51ad92406 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -1730,6 +1730,8 @@ public abstract class NotificationListenerService extends Service { private ShortcutInfo mShortcutInfo; private @RankingAdjustment int mRankingAdjustment; private boolean mIsBubble; + // Notification assistant importance suggestion + private int mProposedImportance; private static final int PARCEL_VERSION = 2; @@ -1767,6 +1769,7 @@ public abstract class NotificationListenerService extends Service { out.writeParcelable(mShortcutInfo, flags); out.writeInt(mRankingAdjustment); out.writeBoolean(mIsBubble); + out.writeInt(mProposedImportance); } /** @hide */ @@ -1805,6 +1808,7 @@ public abstract class NotificationListenerService extends Service { mShortcutInfo = in.readParcelable(cl, android.content.pm.ShortcutInfo.class); mRankingAdjustment = in.readInt(); mIsBubble = in.readBoolean(); + mProposedImportance = in.readInt(); } @@ -1897,6 +1901,23 @@ public abstract class NotificationListenerService extends Service { } /** + * Returns the proposed importance provided by the {@link NotificationAssistantService}. + * + * This can be used to suggest that the user change the importance of this type of + * notification moving forward. A value of + * {@link NotificationManager#IMPORTANCE_UNSPECIFIED} means that the NAS has not recommended + * a change to the importance, and no UI should be shown to the user. See + * {@link Adjustment#KEY_IMPORTANCE_PROPOSAL}. + * + * @return the importance of the notification + * @hide + */ + @SystemApi + public @NotificationManager.Importance int getProposedImportance() { + return mProposedImportance; + } + + /** * If the system has overridden the group key, then this will be non-null, and this * key should be used to bundle notifications. */ @@ -2060,7 +2081,7 @@ public abstract class NotificationListenerService extends Service { boolean noisy, ArrayList<Notification.Action> smartActions, ArrayList<CharSequence> smartReplies, boolean canBubble, boolean isTextChanged, boolean isConversation, ShortcutInfo shortcutInfo, - int rankingAdjustment, boolean isBubble) { + int rankingAdjustment, boolean isBubble, int proposedImportance) { mKey = key; mRank = rank; mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW; @@ -2086,6 +2107,7 @@ public abstract class NotificationListenerService extends Service { mShortcutInfo = shortcutInfo; mRankingAdjustment = rankingAdjustment; mIsBubble = isBubble; + mProposedImportance = proposedImportance; } /** @@ -2126,7 +2148,8 @@ public abstract class NotificationListenerService extends Service { other.mIsConversation, other.mShortcutInfo, other.mRankingAdjustment, - other.mIsBubble); + other.mIsBubble, + other.mProposedImportance); } /** @@ -2185,7 +2208,8 @@ public abstract class NotificationListenerService extends Service { && Objects.equals((mShortcutInfo == null ? 0 : mShortcutInfo.getId()), (other.mShortcutInfo == null ? 0 : other.mShortcutInfo.getId())) && Objects.equals(mRankingAdjustment, other.mRankingAdjustment) - && Objects.equals(mIsBubble, other.mIsBubble); + && Objects.equals(mIsBubble, other.mIsBubble) + && Objects.equals(mProposedImportance, other.mProposedImportance); } } diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index a892570f589c..bffa660cdbd9 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -79,6 +79,12 @@ public final class ZenPolicy implements Parcelable { /** @hide */ public static final int PRIORITY_CATEGORY_CONVERSATIONS = 8; + /** + * Total number of priority categories. Keep updated with any updates to PriorityCategory enum. + * @hide + */ + public static final int NUM_PRIORITY_CATEGORIES = 9; + /** @hide */ @IntDef(prefix = { "VISUAL_EFFECT_" }, value = { VISUAL_EFFECT_FULL_SCREEN_INTENT, @@ -107,6 +113,12 @@ public final class ZenPolicy implements Parcelable { /** @hide */ public static final int VISUAL_EFFECT_NOTIFICATION_LIST = 6; + /** + * Total number of visual effects. Keep updated with any updates to VisualEffect enum. + * @hide + */ + public static final int NUM_VISUAL_EFFECTS = 7; + /** @hide */ @IntDef(prefix = { "PEOPLE_TYPE_" }, value = { PEOPLE_TYPE_UNSET, @@ -202,8 +214,8 @@ public final class ZenPolicy implements Parcelable { /** @hide */ public ZenPolicy() { - mPriorityCategories = new ArrayList<>(Collections.nCopies(9, 0)); - mVisualEffects = new ArrayList<>(Collections.nCopies(7, 0)); + mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0)); + mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0)); } /** @@ -804,8 +816,12 @@ public final class ZenPolicy implements Parcelable { @Override public ZenPolicy createFromParcel(Parcel source) { ZenPolicy policy = new ZenPolicy(); - policy.mPriorityCategories = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class); - policy.mVisualEffects = source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class); + policy.mPriorityCategories = trimList( + source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class), + NUM_PRIORITY_CATEGORIES); + policy.mVisualEffects = trimList( + source.readArrayList(Integer.class.getClassLoader(), java.lang.Integer.class), + NUM_VISUAL_EFFECTS); policy.mPriorityCalls = source.readInt(); policy.mPriorityMessages = source.readInt(); policy.mConversationSenders = source.readInt(); @@ -832,6 +848,15 @@ public final class ZenPolicy implements Parcelable { .toString(); } + // Returns a list containing the first maxLength elements of the input list if the list is + // longer than that size. For the lists in ZenPolicy, this should not happen unless the input + // is corrupt. + private static ArrayList<Integer> trimList(ArrayList<Integer> list, int maxLength) { + if (list == null || list.size() <= maxLength) { + return list; + } + return new ArrayList<>(list.subList(0, maxLength)); + } private String priorityCategoriesToString() { StringBuilder builder = new StringBuilder(); diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 65bc81b44d79..f6a86974f3ba 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -153,6 +153,11 @@ public class FeatureFlagUtils { */ public static final String SETTINGS_AUDIO_ROUTING = "settings_audio_routing"; + /** Flag to enable/disable flash alerts + * @hide + */ + public static final String SETTINGS_FLASH_ALERTS = "settings_flash_alerts"; + private static final Map<String, String> DEFAULT_FLAGS; static { @@ -192,6 +197,7 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_BIOMETRICS2_ENROLLMENT, "false"); DEFAULT_FLAGS.put(SETTINGS_ACCESSIBILITY_HEARING_AID_PAGE, "false"); DEFAULT_FLAGS.put(SETTINGS_AUDIO_ROUTING, "false"); + DEFAULT_FLAGS.put(SETTINGS_FLASH_ALERTS, "false"); } private static final Set<String> PERSISTENT_FLAGS; diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index 3b082bcfcfb2..787ffb74f92e 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -18,6 +18,7 @@ package android.view; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; +import android.graphics.Rect; import android.graphics.Region; import android.hardware.HardwareBuffer; import android.window.SurfaceSyncGroup; @@ -149,4 +150,21 @@ public interface AttachedSurfaceControl { default SurfaceSyncGroup getOrCreateSurfaceSyncGroup() { return null; } + + /** + * Set a crop region on all children parented to the layer represented by this + * AttachedSurfaceControl. This includes SurfaceView, and an example usage may + * be to ensure that SurfaceView with {@link android.view.SurfaceView#setZOrderOnTop} + * are cropped to a region not including the app bar. + * + * This cropped is expressed in terms of insets in window-space. Negative insets + * are considered invalid and will produce an exception. Insets of zero will produce + * the same result as if this function had never been called. + * + * @param insets The insets in each direction by which to bound the children + * expressed in window-space. + * @hide + */ + default void setChildBoundingInsets(@NonNull Rect insets) { + } } diff --git a/core/java/android/view/EventLogTags.logtags b/core/java/android/view/EventLogTags.logtags index 098f1af582c3..1ad3472e4c75 100644 --- a/core/java/android/view/EventLogTags.logtags +++ b/core/java/android/view/EventLogTags.logtags @@ -37,6 +37,14 @@ option java_package android.view # # See system/core/logcat/event.logtags for the master copy of the tags. +# 32000 - 32999 reserved for input method framework +# IME animation is started. +32006 imf_ime_anim_start (token|3),(animation type|1),(alpha|5),(current insets|3),(shown insets|3),(hidden insets|3) +# IME animation is finished. +32007 imf_ime_anim_finish (token|3),(animation type|1),(alpha|5),(shown|1),(insets|3) +# IME animation is canceled. +32008 imf_ime_anim_cancel (token|3),(animation type|1),(pending insets|3) + # 62000 - 62199 reserved for inputflinger # --------------------------- diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 164865966ead..309a94acf783 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -17,6 +17,9 @@ package android.view; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.view.EventLogTags.IMF_IME_ANIM_CANCEL; +import static android.view.EventLogTags.IMF_IME_ANIM_FINISH; +import static android.view.EventLogTags.IMF_IME_ANIM_START; import static android.view.InsetsAnimationControlImplProto.CURRENT_ALPHA; import static android.view.InsetsAnimationControlImplProto.IS_CANCELLED; import static android.view.InsetsAnimationControlImplProto.IS_FINISHED; @@ -36,7 +39,10 @@ import static android.view.InsetsState.ISIDE_LEFT; import static android.view.InsetsState.ISIDE_RIGHT; import static android.view.InsetsState.ISIDE_TOP; import static android.view.InsetsState.ITYPE_IME; +import static android.view.WindowInsets.Type.ime; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; +import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; +import static android.view.inputmethod.ImeTracker.TOKEN_NONE; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; @@ -47,6 +53,7 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; import android.util.ArraySet; +import android.util.EventLog; import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; @@ -156,6 +163,12 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro mLayoutInsetsDuringAnimation = layoutInsetsDuringAnimation; mTranslator = translator; mStatsToken = statsToken; + if (DEBUG_IME_VISIBILITY && (types & ime()) != 0) { + EventLog.writeEvent(IMF_IME_ANIM_START, + mStatsToken != null ? mStatsToken.getTag() : TOKEN_NONE, mAnimationType, + mCurrentAlpha, "Current:" + mCurrentInsets, "Shown:" + mShownInsets, + "Hidden:" + mHiddenInsets); + } mController.startAnimation(this, listener, types, mAnimation, new Bounds(mHiddenInsets, mShownInsets)); } @@ -314,11 +327,16 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } mShownOnFinish = shown; mFinished = true; - setInsetsAndAlpha(shown ? mShownInsets : mHiddenInsets, mPendingAlpha, 1f /* fraction */, - true /* allowWhenFinished */); + final Insets insets = shown ? mShownInsets : mHiddenInsets; + setInsetsAndAlpha(insets, mPendingAlpha, 1f /* fraction */, true /* allowWhenFinished */); if (DEBUG) Log.d(TAG, "notify control request finished for types: " + mTypes); mListener.onFinished(this); + if (DEBUG_IME_VISIBILITY && (mTypes & ime()) != 0) { + EventLog.writeEvent(IMF_IME_ANIM_FINISH, + mStatsToken != null ? mStatsToken.getTag() : TOKEN_NONE, mAnimationType, + mCurrentAlpha, shown ? 1 : 0, Objects.toString(insets)); + } } @Override @@ -339,7 +357,11 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro mCancelled = true; mListener.onCancelled(mReadyDispatched ? this : null); if (DEBUG) Log.d(TAG, "notify Control request cancelled for types: " + mTypes); - + if (DEBUG_IME_VISIBILITY && (mTypes & ime()) != 0) { + EventLog.writeEvent(IMF_IME_ANIM_CANCEL, + mStatsToken != null ? mStatsToken.getTag() : TOKEN_NONE, mAnimationType, + Objects.toString(mPendingInsets)); + } releaseLeashes(); } diff --git a/core/java/android/view/InsetsFrameProvider.java b/core/java/android/view/InsetsFrameProvider.java index 0a2b06ca7e70..58ee59d1b36b 100644 --- a/core/java/android/view/InsetsFrameProvider.java +++ b/core/java/android/view/InsetsFrameProvider.java @@ -323,8 +323,7 @@ public class InsetsFrameProvider implements Parcelable { public String toString() { StringBuilder sb = new StringBuilder(32); sb.append("TypedInsetsSize: {"); - sb.append("windowType=").append(ViewDebug.intToString( - WindowManager.LayoutParams.class, "type", windowType)); + sb.append("windowType=").append(windowType); sb.append(", insetsSize=").append(insetsSize); sb.append("}"); return sb.toString(); diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 9db084e01598..e38376d7d4a1 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -1062,6 +1062,15 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } /** + * @hide + */ + public String getName() { + ViewRootImpl viewRoot = getViewRootImpl(); + String viewRootName = viewRoot == null ? "detached" : viewRoot.getTitle().toString(); + return "SurfaceView[" + viewRootName + "]"; + } + + /** * If SV is trying to be part of the VRI sync, we need to add SV to the VRI sync before * invoking the redrawNeeded call to the owner. This is to ensure we can set up the SV in * the sync before the SV owner knows it needs to draw a new frame. @@ -1073,7 +1082,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private void handleSyncBufferCallback(SurfaceHolder.Callback[] callbacks, SyncBufferTransactionCallback syncBufferTransactionCallback) { - final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(); + final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(getName()); getViewRootImpl().addToSync(surfaceSyncGroup); redrawNeededAsync(callbacks, () -> { Transaction t = null; @@ -1088,7 +1097,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } private void handleSyncNoBuffer(SurfaceHolder.Callback[] callbacks) { - final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(); + final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(getName()); synchronized (mSyncGroups) { mSyncGroups.add(surfaceSyncGroup); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index d709840d1b10..01984571a8f4 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -21602,6 +21602,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return Math.max(vis1, vis2); } + private boolean mShouldFakeFocus = false; + + /** + * Fake send a focus event after attaching to window. + * See {@link android.view.ViewRootImpl#dispatchCompatFakeFocus()} for details. + * @hide + */ + public void fakeFocusAfterAttachingToWindow() { + mShouldFakeFocus = true; + } + /** * @param info the {@link android.view.View.AttachInfo} to associated with * this view @@ -21670,6 +21681,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, notifyEnterOrExitForAutoFillIfNeeded(true); notifyAppearedOrDisappearedForContentCaptureIfNeeded(true); + + if (mShouldFakeFocus) { + getViewRootImpl().dispatchCompatFakeFocus(); + mShouldFakeFocus = false; + } } @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index ea7a64ef5c24..9797556d3711 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -261,6 +261,7 @@ public final class ViewRootImpl implements ViewParent, private static final boolean DEBUG_CONTENT_CAPTURE = false || LOCAL_LOGV; private static final boolean DEBUG_SCROLL_CAPTURE = false || LOCAL_LOGV; private static final boolean DEBUG_BLAST = false || LOCAL_LOGV; + private static final int LOGTAG_INPUT_FOCUS = 62001; /** * Set to false if we do not want to use the multi threaded renderer even though @@ -675,12 +676,6 @@ public final class ViewRootImpl implements ViewParent, private BLASTBufferQueue mBlastBufferQueue; /** - * Transaction object that can be used to synchronize child SurfaceControl changes with - * ViewRootImpl SurfaceControl changes by the server. The object is passed along with - * the SurfaceChangedCallback. - */ - private final Transaction mSurfaceChangedTransaction = new Transaction(); - /** * Child container layer of {@code mSurface} with the same bounds as its parent, and cropped to * the surface insets. This surface is created only if a client requests it via {@link * #getBoundsLayer()}. By parenting to this bounds surface, child surfaces can ensure they do @@ -919,6 +914,8 @@ public final class ViewRootImpl implements ViewParent, } } }; + private final Rect mChildBoundingInsets = new Rect(); + private boolean mChildBoundingInsetsChanged = false; private String mTag = TAG; @@ -2139,9 +2136,9 @@ public final class ViewRootImpl implements ViewParent, mSurfaceChangedCallbacks.remove(c); } - private void notifySurfaceCreated() { + private void notifySurfaceCreated(Transaction t) { for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) { - mSurfaceChangedCallbacks.get(i).surfaceCreated(mSurfaceChangedTransaction); + mSurfaceChangedCallbacks.get(i).surfaceCreated(t); } } @@ -2150,9 +2147,9 @@ public final class ViewRootImpl implements ViewParent, * called if a new surface is created, only if the valid surface has been replaced with another * valid surface. */ - private void notifySurfaceReplaced() { + private void notifySurfaceReplaced(Transaction t) { for (int i = 0; i < mSurfaceChangedCallbacks.size(); i++) { - mSurfaceChangedCallbacks.get(i).surfaceReplaced(mSurfaceChangedTransaction); + mSurfaceChangedCallbacks.get(i).surfaceReplaced(t); } } @@ -2222,6 +2219,8 @@ public final class ViewRootImpl implements ViewParent, mTempRect.inset(mWindowAttributes.surfaceInsets.left, mWindowAttributes.surfaceInsets.top, mWindowAttributes.surfaceInsets.right, mWindowAttributes.surfaceInsets.bottom); + mTempRect.inset(mChildBoundingInsets.left, mChildBoundingInsets.top, + mChildBoundingInsets.right, mChildBoundingInsets.bottom); t.setWindowCrop(mBoundsLayer, mTempRect); } @@ -2243,7 +2242,7 @@ public final class ViewRootImpl implements ViewParent, if (!sc.isValid()) return; if (updateBoundsLayer(t)) { - mergeWithNextTransaction(t, mSurface.getNextFrameNumber()); + applyTransactionOnDraw(t); } } @@ -2329,7 +2328,6 @@ public final class ViewRootImpl implements ViewParent, */ void notifyRendererOfFramePending() { if (mAttachInfo.mThreadedRenderer != null) { - mAttachInfo.mThreadedRenderer.notifyCallbackPending(); mAttachInfo.mThreadedRenderer.notifyFramePending(); } } @@ -3447,7 +3445,8 @@ public final class ViewRootImpl implements ViewParent, } } - if (surfaceSizeChanged || surfaceReplaced || surfaceCreated || windowAttributesChanged) { + if (surfaceSizeChanged || surfaceReplaced || surfaceCreated || + windowAttributesChanged || mChildBoundingInsetsChanged) { // If the surface has been replaced, there's a chance the bounds layer is not parented // to the new layer. When updating bounds layer, also reparent to the main VRI // SurfaceControl to ensure it's correctly placed in the hierarchy. @@ -3458,6 +3457,7 @@ public final class ViewRootImpl implements ViewParent, // enough. WMS doesn't want to keep around old children since they will leak when the // client creates new children. prepareSurfaces(); + mChildBoundingInsetsChanged = false; } final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); @@ -3505,17 +3505,24 @@ public final class ViewRootImpl implements ViewParent, } } + boolean didUseTransaction = false; // These callbacks will trigger SurfaceView SurfaceHolder.Callbacks and must be invoked // after the measure pass. If its invoked before the measure pass and the app modifies // the view hierarchy in the callbacks, we could leave the views in a broken state. if (surfaceCreated) { - notifySurfaceCreated(); + notifySurfaceCreated(mTransaction); + didUseTransaction = true; } else if (surfaceReplaced) { - notifySurfaceReplaced(); + notifySurfaceReplaced(mTransaction); + didUseTransaction = true; } else if (surfaceDestroyed) { notifySurfaceDestroyed(); } + if (didUseTransaction) { + applyTransactionOnDraw(mTransaction); + } + if (triggerGlobalLayoutListener) { mAttachInfo.mRecomputeGlobalAttributes = false; mAttachInfo.mTreeObserver.dispatchOnGlobalLayout(); @@ -3719,22 +3726,18 @@ public final class ViewRootImpl implements ViewParent, final int seqId = mSyncSeqId; mWmsRequestSyncGroupState = WMS_SYNC_PENDING; - mWmsRequestSyncGroup = new SurfaceSyncGroup(t -> { - mWmsRequestSyncGroupState = WMS_SYNC_RETURNED; - // Callback will be invoked on executor thread so post to main thread. - mHandler.postAtFrontOfQueue(() -> { - if (t != null) { - mSurfaceChangedTransaction.merge(t); - } - mWmsRequestSyncGroupState = WMS_SYNC_MERGED; - reportDrawFinished(seqId); - }); + mWmsRequestSyncGroup = new SurfaceSyncGroup("wmsSync-" + mTag, t -> { + mWmsRequestSyncGroupState = WMS_SYNC_MERGED; + reportDrawFinished(t, seqId); }); + Trace.traceBegin(Trace.TRACE_TAG_VIEW, + "create WMS Sync group=" + mWmsRequestSyncGroup.getName()); if (DEBUG_BLAST) { - Log.d(mTag, "Setup new sync id=" + mWmsRequestSyncGroup); + Log.d(mTag, "Setup new sync=" + mWmsRequestSyncGroup.getName()); } mWmsRequestSyncGroup.addToSync(this); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); notifySurfaceSyncStarted(); } @@ -3835,8 +3838,7 @@ public final class ViewRootImpl implements ViewParent, } if (mAdded) { - dispatchFocusEvent(hasWindowFocus); - + dispatchFocusEvent(hasWindowFocus, false /* fakeFocus */); // Note: must be done after the focus change callbacks, // so all of the view state is set up correctly. mImeFocusController.onPostWindowFocus( @@ -3873,7 +3875,33 @@ public final class ViewRootImpl implements ViewParent, } } - private void dispatchFocusEvent(boolean hasWindowFocus) { + /** + * Send a fake focus event for unfocused apps in split screen as some game engines wait to + * get focus before drawing the content of the app. This will be used so that apps do not get + * blacked out when they are resumed and do not have focus yet. + * + * {@hide} + */ + // TODO(b/263094829): Investigate dispatching this for onPause as well + public void dispatchCompatFakeFocus() { + boolean aboutToHaveFocus = false; + synchronized (this) { + aboutToHaveFocus = mWindowFocusChanged && mUpcomingWindowFocus; + } + final boolean alreadyHaveFocus = mAttachInfo.mHasWindowFocus; + if (aboutToHaveFocus || alreadyHaveFocus) { + // Do not need to toggle focus if app doesn't need it, or has focus. + return; + } + EventLog.writeEvent(LOGTAG_INPUT_FOCUS, + "Giving fake focus to " + mBasePackageName, "reason=unity bug workaround"); + dispatchFocusEvent(true /* hasWindowFocus */, true /* fakeFocus */); + EventLog.writeEvent(LOGTAG_INPUT_FOCUS, + "Removing fake focus from " + mBasePackageName, "reason=timeout callback"); + dispatchFocusEvent(false /* hasWindowFocus */, true /* fakeFocus */); + } + + private void dispatchFocusEvent(boolean hasWindowFocus, boolean fakeFocus) { profileRendering(hasWindowFocus); if (hasWindowFocus && mAttachInfo.mThreadedRenderer != null && mSurface.isValid()) { mFullRedrawNeeded = true; @@ -3899,7 +3927,10 @@ public final class ViewRootImpl implements ViewParent, } mAttachInfo.mHasWindowFocus = hasWindowFocus; - mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes); + + if (!fakeFocus) { + mImeFocusController.onPreWindowFocus(hasWindowFocus, mWindowAttributes); + } if (mView != null) { mAttachInfo.mKeyDispatchState.reset(); @@ -4363,18 +4394,22 @@ public final class ViewRootImpl implements ViewParent, } } - private void reportDrawFinished(int seqId) { + private void reportDrawFinished(@Nullable Transaction t, int seqId) { if (DEBUG_BLAST) { - Log.d(mTag, "reportDrawFinished " + Debug.getCallers(5)); + Log.d(mTag, "reportDrawFinished"); } try { - mWindowSession.finishDrawing(mWindow, mSurfaceChangedTransaction, seqId); + mWindowSession.finishDrawing(mWindow, t, seqId); } catch (RemoteException e) { Log.e(mTag, "Unable to report draw finished", e); - mSurfaceChangedTransaction.apply(); + if (t != null) { + t.apply(); + } } finally { - mSurfaceChangedTransaction.clear(); + if (t != null) { + t.clear(); + } } } @@ -11078,7 +11113,7 @@ public final class ViewRootImpl implements ViewParent, @Nullable public SurfaceControl.Transaction buildReparentTransaction( @NonNull SurfaceControl child) { if (mSurfaceControl.isValid()) { - return new SurfaceControl.Transaction().reparent(child, mSurfaceControl); + return new SurfaceControl.Transaction().reparent(child, getBoundsLayer()); } return null; } @@ -11296,13 +11331,28 @@ public final class ViewRootImpl implements ViewParent, @Override public SurfaceSyncGroup getOrCreateSurfaceSyncGroup() { + boolean newSyncGroup = false; if (mActiveSurfaceSyncGroup == null) { - mActiveSurfaceSyncGroup = new SurfaceSyncGroup(); + mActiveSurfaceSyncGroup = new SurfaceSyncGroup(mTag); updateSyncInProgressCount(mActiveSurfaceSyncGroup); if (!mIsInTraversal && !mTraversalScheduled) { scheduleTraversals(); } + newSyncGroup = true; } + + Trace.instant(Trace.TRACE_TAG_VIEW, + "getOrCreateSurfaceSyncGroup isNew=" + newSyncGroup + " " + mTag); + + if (DEBUG_BLAST) { + if (newSyncGroup) { + Log.d(mTag, "Creating new active sync group " + mActiveSurfaceSyncGroup.getName()); + } else { + Log.d(mTag, "Return already created active sync group " + + mActiveSurfaceSyncGroup.getName()); + } + } + return mActiveSurfaceSyncGroup; }; @@ -11334,4 +11384,14 @@ public final class ViewRootImpl implements ViewParent, } mActiveSurfaceSyncGroup.addToSync(syncable, false /* parentSyncGroupMerge */); } + + @Override + public void setChildBoundingInsets(@NonNull Rect insets) { + if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { + throw new IllegalArgumentException("Negative insets passed to setChildBoundingInsets."); + } + mChildBoundingInsets.set(insets); + mChildBoundingInsetsChanged = true; + scheduleTraversals(); + } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index e4227322da10..f375ccb7e207 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -4759,10 +4759,10 @@ public interface WindowManager extends ViewManager { } if (providedInsets != null) { sb.append(System.lineSeparator()); - sb.append(prefix).append(" providedInsets:"); + sb.append(" providedInsets="); for (int i = 0; i < providedInsets.length; ++i) { - sb.append(System.lineSeparator()); - sb.append(prefix).append(" ").append(providedInsets[i]); + if (i > 0) sb.append(' '); + sb.append((providedInsets[i])); } } if (insetsRoundedCornerFrame) { @@ -4771,12 +4771,10 @@ public interface WindowManager extends ViewManager { } if (paramsForRotation != null && paramsForRotation.length != 0) { sb.append(System.lineSeparator()); - sb.append(prefix).append(" paramsForRotation:"); + sb.append(prefix).append(" paramsForRotation="); for (int i = 0; i < paramsForRotation.length; ++i) { - // Additional prefix needed for the beginning of the params of the new rotation. - sb.append(System.lineSeparator()).append(prefix).append(" "); - sb.append(Surface.rotationToString(i)).append("="); - sb.append(paramsForRotation[i].toString(prefix + " ")); + if (i > 0) sb.append(' '); + sb.append(paramsForRotation[i].toString()); } } diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index 1bcb04058de6..9ed5c29089b2 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -43,6 +43,13 @@ public interface ImeTracker { String TAG = "ImeTracker"; + /** The debug flag for IME visibility event log. */ + // TODO(b/239501597) : Have a system property to control this flag. + boolean DEBUG_IME_VISIBILITY = false; + + /** The message to indicate if there is no valid {@link Token}. */ + String TOKEN_NONE = "TOKEN_NONE"; + /** The type of the IME request. */ @IntDef(prefix = { "TYPE_" }, value = { TYPE_SHOW, diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java index f74d29486909..be9cbfff5db2 100644 --- a/core/java/android/window/ImeOnBackInvokedDispatcher.java +++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java @@ -123,7 +123,7 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc private void receive( int resultCode, Bundle resultData, - @NonNull OnBackInvokedDispatcher receivingDispatcher) { + @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { final int callbackId = resultData.getInt(RESULT_KEY_ID); if (resultCode == RESULT_CODE_REGISTER) { int priority = resultData.getInt(RESULT_KEY_PRIORITY); @@ -140,11 +140,11 @@ public class ImeOnBackInvokedDispatcher implements OnBackInvokedDispatcher, Parc @NonNull IOnBackInvokedCallback iCallback, @OnBackInvokedDispatcher.Priority int priority, int callbackId, - @NonNull OnBackInvokedDispatcher receivingDispatcher) { + @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) { final ImeOnBackInvokedCallback imeCallback = new ImeOnBackInvokedCallback(iCallback, callbackId, priority); mImeCallbacks.add(imeCallback); - receivingDispatcher.registerOnBackInvokedCallback(priority, imeCallback); + receivingDispatcher.registerOnBackInvokedCallbackUnchecked(imeCallback, priority); } private void unregisterReceivedCallback( diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java index c0686fa4164d..e1a9a48c76b7 100644 --- a/core/java/android/window/SurfaceSyncGroup.java +++ b/core/java/android/window/SurfaceSyncGroup.java @@ -19,6 +19,7 @@ package android.window; import android.annotation.Nullable; import android.annotation.UiThread; import android.os.Debug; +import android.os.Trace; import android.util.ArraySet; import android.util.Log; import android.util.Pair; @@ -30,6 +31,7 @@ import com.android.internal.annotations.GuardedBy; import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Supplier; @@ -75,6 +77,10 @@ public class SurfaceSyncGroup { private static final String TAG = "SurfaceSyncGroup"; private static final boolean DEBUG = false; + private static final int MAX_COUNT = 100; + + private static final AtomicInteger sCounter = new AtomicInteger(0); + private static Supplier<Transaction> sTransactionFactory = Transaction::new; /** @@ -83,6 +89,8 @@ public class SurfaceSyncGroup { */ private final Object mLock = new Object(); + private final String mName; + @GuardedBy("mLock") private final Set<TransactionReadyCallback> mPendingSyncs = new ArraySet<>(); @GuardedBy("mLock") @@ -112,9 +120,12 @@ public class SurfaceSyncGroup { /** * Starts a sync and will automatically apply the final, merged transaction. */ - public SurfaceSyncGroup() { - this(transaction -> { + public SurfaceSyncGroup(String name) { + this(name, transaction -> { if (transaction != null) { + if (DEBUG) { + Log.d(TAG, "Applying transaction " + transaction); + } transaction.apply(); } }); @@ -128,11 +139,27 @@ public class SurfaceSyncGroup { * transaction with all the sync data merged. The Transaction * passed back can be null. * - * NOTE: Only should be used by ViewRootImpl + * NOTE: Only should be used by ViewRootImpl * @hide */ - public SurfaceSyncGroup(Consumer<Transaction> transactionReadyCallback) { + public SurfaceSyncGroup(String name, Consumer<Transaction> transactionReadyCallback) { + // sCounter is a way to give the SurfaceSyncGroup a unique name even if the name passed in + // is not. + // Avoid letting the count get too big so just reset to 0. It's unlikely that we'll have + // more than MAX_COUNT active syncs that have overlapping names + if (sCounter.get() >= MAX_COUNT) { + sCounter.set(0); + } + + mName = name + "#" + sCounter.getAndIncrement(); + mTransactionReadyCallback = transaction -> { + if (DEBUG && transaction != null) { + Log.d(TAG, "Sending non null transaction " + transaction + " to callback for " + + mName); + } + Trace.instant(Trace.TRACE_TAG_VIEW, + "Final TransactionCallback with " + transaction + " for " + mName); transactionReadyCallback.accept(transaction); synchronized (mLock) { for (Pair<Executor, Runnable> callback : mSyncCompleteCallbacks) { @@ -141,8 +168,10 @@ public class SurfaceSyncGroup { } }; + Trace.instant(Trace.TRACE_TAG_VIEW, "new SurfaceSyncGroup " + mName); + if (DEBUG) { - Log.d(TAG, "setupSync " + this + " " + Debug.getCallers(2)); + Log.d(TAG, "setupSync " + mName + " " + Debug.getCallers(2)); } } @@ -171,9 +200,11 @@ public class SurfaceSyncGroup { /** * Similar to {@link #markSyncReady()}, but a transaction is passed in to merge with the * SurfaceSyncGroup. + * * @param t The transaction that merges into the main Transaction for the SurfaceSyncGroup. */ public void onTransactionReady(@Nullable Transaction t) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "markSyncReady " + mName); synchronized (mLock) { mSyncReady = true; if (t != null) { @@ -181,6 +212,7 @@ public class SurfaceSyncGroup { } checkIfSyncIsComplete(); } + Trace.traceEnd(Trace.TRACE_TAG_VIEW); } /** @@ -198,7 +230,7 @@ public class SurfaceSyncGroup { @UiThread public boolean addToSync(SurfaceView surfaceView, Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) { - SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(); + SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(surfaceView.getName()); if (addToSync(surfaceSyncGroup, false /* parentSyncGroupMerge */)) { frameCallbackConsumer.accept( () -> surfaceView.syncNextFrame(surfaceSyncGroup::onTransactionReady)); @@ -235,9 +267,15 @@ public class SurfaceSyncGroup { * otherwise. */ public boolean addToSync(SurfaceSyncGroup surfaceSyncGroup, boolean parentSyncGroupMerge) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, + "addToSync child=" + surfaceSyncGroup.mName + " parent=" + mName); TransactionReadyCallback transactionReadyCallback = new TransactionReadyCallback() { @Override public void onTransactionReady(Transaction t) { + if (DEBUG) { + Log.d(TAG, "onTransactionReady called for" + surfaceSyncGroup.mName + + " and sent to " + mName); + } synchronized (mLock) { if (t != null) { // When an older parent sync group is added due to a child syncGroup getting @@ -250,6 +288,9 @@ public class SurfaceSyncGroup { mTransaction.merge(t); } mPendingSyncs.remove(this); + Trace.instant(Trace.TRACE_TAG_VIEW, + "onTransactionReady child=" + surfaceSyncGroup.mName + " parent=" + + mName); checkIfSyncIsComplete(); } } @@ -257,13 +298,20 @@ public class SurfaceSyncGroup { synchronized (mLock) { if (mSyncReady) { - Log.e(TAG, "Sync " + this + " was already marked as ready. No more " + Log.e(TAG, "Sync " + mName + " was already marked as ready. No more " + "SurfaceSyncGroups can be added."); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); return false; } mPendingSyncs.add(transactionReadyCallback); + if (DEBUG) { + Log.d(TAG, "addToSync " + surfaceSyncGroup.mName + " to " + mName + " mSyncReady=" + + mSyncReady + " mPendingSyncs=" + mPendingSyncs.size()); + } } + surfaceSyncGroup.onAddedToSyncGroup(this, transactionReadyCallback); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); return true; } @@ -281,21 +329,25 @@ public class SurfaceSyncGroup { private void checkIfSyncIsComplete() { if (mFinished) { if (DEBUG) { - Log.d(TAG, "SurfaceSyncGroup=" + this + " is already complete"); + Log.d(TAG, "SurfaceSyncGroup=" + mName + " is already complete"); } return; } + Trace.instant(Trace.TRACE_TAG_VIEW, + "checkIfSyncIsComplete " + mName + " mSyncReady=" + mSyncReady + " mPendingSyncs=" + + mPendingSyncs.size()); if (!mSyncReady || !mPendingSyncs.isEmpty()) { if (DEBUG) { - Log.d(TAG, "SurfaceSyncGroup=" + this + " is not complete. mSyncReady=" - + mSyncReady + " mPendingSyncs=" + mPendingSyncs.size()); + Log.d(TAG, + "SurfaceSyncGroup=" + mName + " is not complete. mSyncReady=" + mSyncReady + + " mPendingSyncs=" + mPendingSyncs.size()); } return; } if (DEBUG) { - Log.d(TAG, "Successfully finished sync id=" + this); + Log.d(TAG, "Successfully finished sync id=" + mName); } mTransactionReadyCallback.onTransactionReady(mTransaction); mFinished = true; @@ -303,6 +355,8 @@ public class SurfaceSyncGroup { private void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, TransactionReadyCallback transactionReadyCallback) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, + "onAddedToSyncGroup child=" + mName + " parent=" + parentSyncGroup.mName); boolean finished = false; synchronized (mLock) { if (mFinished) { @@ -316,7 +370,9 @@ public class SurfaceSyncGroup { // from the original parent are also combined with the new parent SurfaceSyncGroup. if (mParentSyncGroup != null && mParentSyncGroup != parentSyncGroup) { if (DEBUG) { - Log.d(TAG, "Already part of sync group " + mParentSyncGroup + " " + this); + Log.d(TAG, "Trying to add to " + parentSyncGroup.mName + + " but already part of sync group " + mParentSyncGroup.mName + " " + + mName); } parentSyncGroup.addToSync(mParentSyncGroup, true /* parentSyncGroupMerge */); } @@ -329,8 +385,12 @@ public class SurfaceSyncGroup { mParentSyncGroup = parentSyncGroup; final TransactionReadyCallback lastCallback = mTransactionReadyCallback; mTransactionReadyCallback = t -> { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, + "transactionReadyCallback " + mName + " parent=" + + parentSyncGroup.mName); lastCallback.onTransactionReady(null); transactionReadyCallback.onTransactionReady(t); + Trace.traceEnd(Trace.TRACE_TAG_VIEW); }; } } @@ -340,7 +400,13 @@ public class SurfaceSyncGroup { if (finished) { transactionReadyCallback.onTransactionReady(null); } + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + public String getName() { + return mName; } + /** * Interface so the SurfaceSyncer can know when it's safe to start and when everything has been * completed. The caller should invoke the calls when the rendering has started and finished a diff --git a/core/java/android/window/SurfaceSyncGroup.md b/core/java/android/window/SurfaceSyncGroup.md index 659aade24a91..b4faeac57e58 100644 --- a/core/java/android/window/SurfaceSyncGroup.md +++ b/core/java/android/window/SurfaceSyncGroup.md @@ -59,7 +59,7 @@ As mentioned above, SurfaceViews are a special case because the buffers produced A simple example where you want to sync two windows and also include a transaction in the sync ```java -SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); +SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(NAME); SyncGroup.addSyncCompleteCallback(mMainThreadExecutor, () -> { Log.d(TAG, "syncComplete"); }; @@ -73,7 +73,7 @@ A SurfaceView example: See `frameworks/base/tests/SurfaceViewSyncTest` for a working example ```java -SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); +SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(NAME); syncGroup.addSyncCompleteCallback(mMainThreadExecutor, () -> { Log.d(TAG, "syncComplete"); }; diff --git a/core/java/com/android/internal/app/AppLocaleCollector.java b/core/java/com/android/internal/app/AppLocaleCollector.java index 65e8c646e17d..a50fbb81dc6b 100644 --- a/core/java/com/android/internal/app/AppLocaleCollector.java +++ b/core/java/com/android/internal/app/AppLocaleCollector.java @@ -19,49 +19,184 @@ package com.android.internal.app; import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_ASSET; import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG; +import android.app.LocaleManager; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Build; import android.os.LocaleList; +import android.os.SystemProperties; +import android.provider.Settings; import android.util.Log; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.stream.Collectors; /** The Locale data collector for per-app language. */ -class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { +public class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { private static final String TAG = AppLocaleCollector.class.getSimpleName(); private final Context mContext; private final String mAppPackageName; - private final LocaleStore.LocaleInfo mAppCurrentLocale; + private LocaleStore.LocaleInfo mAppCurrentLocale; + private Set<LocaleStore.LocaleInfo> mAllAppActiveLocales; + private Set<LocaleStore.LocaleInfo> mImeLocales; + private static final String PROP_APP_LANGUAGE_SUGGESTION = + "android.app.language.suggestion.enhanced"; + private static final boolean ENABLED = true; - AppLocaleCollector(Context context, String appPackageName) { + public AppLocaleCollector(Context context, String appPackageName) { mContext = context; mAppPackageName = appPackageName; - mAppCurrentLocale = LocaleStore.getAppCurrentLocaleInfo( - mContext, mAppPackageName); + } + + @VisibleForTesting + public LocaleStore.LocaleInfo getAppCurrentLocale() { + return LocaleStore.getAppActivatedLocaleInfo(mContext, mAppPackageName, true); + } + + /** + * Get all applications' activated locales. + * @return A set which includes all applications' activated LocaleInfo. + */ + @VisibleForTesting + public Set<LocaleStore.LocaleInfo> getAllAppActiveLocales() { + PackageManager pm = mContext.getPackageManager(); + LocaleManager lm = mContext.getSystemService(LocaleManager.class); + HashSet<LocaleStore.LocaleInfo> result = new HashSet<>(); + if (pm != null && lm != null) { + HashMap<String, LocaleStore.LocaleInfo> map = new HashMap<>(); + for (ApplicationInfo appInfo : pm.getInstalledApplications( + PackageManager.ApplicationInfoFlags.of(0))) { + LocaleStore.LocaleInfo localeInfo = LocaleStore.getAppActivatedLocaleInfo( + mContext, appInfo.packageName, false); + // For the locale to be added into the suggestion area, its country could not be + // empty. + if (localeInfo != null && localeInfo.getLocale().getCountry().length() > 0) { + map.put(localeInfo.getId(), localeInfo); + } + } + map.forEach((language, localeInfo) -> result.add(localeInfo)); + } + return result; + } + + /** + * Get all locales that active IME supports. + * + * @return A set which includes all LocaleInfo that active IME supports. + */ + @VisibleForTesting + public Set<LocaleStore.LocaleInfo> getActiveImeLocales() { + Set<LocaleStore.LocaleInfo> activeImeLocales = null; + InputMethodManager imm = mContext.getSystemService(InputMethodManager.class); + if (imm != null) { + InputMethodInfo activeIme = getActiveIme(imm); + if (activeIme != null) { + activeImeLocales = LocaleStore.transformImeLanguageTagToLocaleInfo( + imm.getEnabledInputMethodSubtypeList(activeIme, true)); + } + } + if (activeImeLocales == null) { + return Set.of(); + } else { + return activeImeLocales.stream().filter( + // For the locale to be added into the suggestion area, its country could not be + // empty. + info -> info.getLocale().getCountry().length() > 0).collect( + Collectors.toSet()); + } + } + + private InputMethodInfo getActiveIme(InputMethodManager imm) { + InputMethodInfo activeIme = null; + List<InputMethodInfo> infoList = imm.getEnabledInputMethodList(); + String imeId = Settings.Secure.getStringForUser(mContext.getContentResolver(), + Settings.Secure.DEFAULT_INPUT_METHOD, mContext.getUserId()); + for (InputMethodInfo method : infoList) { + if (method.getId().equals(imeId)) { + activeIme = method; + } + } + return activeIme; + } + + /** + * Get the AppLocaleResult that the application supports. + * @return The AppLocaleResult that the application supports. + */ + @VisibleForTesting + public AppLocaleStore.AppLocaleResult getAppSupportedLocales() { + return AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName); + } + + /** + * Get the locales that system supports excluding langTagsToIgnore. + * + * @param langTagsToIgnore A language set to be ignored. + * @param parent The parent locale. + * @param translatedOnly specified if is is only for translation. + * @return A set which includes the LocaleInfo that system supports, excluding langTagsToIgnore. + */ + @VisibleForTesting + public Set<LocaleStore.LocaleInfo> getSystemSupportedLocale(Set<String> langTagsToIgnore, + LocaleStore.LocaleInfo parent, boolean translatedOnly) { + return LocaleStore.getLevelLocales(mContext, langTagsToIgnore, parent, translatedOnly); + } + + /** + * Get the locales that system activates. + * @return A set which includes all the locales that system activates. + */ + @VisibleForTesting + public List<LocaleStore.LocaleInfo> getSystemCurrentLocale() { + return LocaleStore.getSystemCurrentLocaleInfo(); } @Override public HashSet<String> getIgnoredLocaleList(boolean translatedOnly) { HashSet<String> langTagsToIgnore = new HashSet<>(); - LocaleList systemLangList = LocaleList.getDefault(); - for(int i = 0; i < systemLangList.size(); i++) { - langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); - } - if (mAppCurrentLocale != null) { langTagsToIgnore.add(mAppCurrentLocale.getLocale().toLanguageTag()); } + + if (SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, ENABLED)) { + // Add the locale that other App activated + mAllAppActiveLocales.forEach( + info -> langTagsToIgnore.add(info.getLocale().toLanguageTag())); + // Add the locale that active IME enabled + mImeLocales.forEach(info -> langTagsToIgnore.add(info.getLocale().toLanguageTag())); + } + + // Add System locales + LocaleList systemLangList = LocaleList.getDefault(); + for (int i = 0; i < systemLangList.size(); i++) { + langTagsToIgnore.add(systemLangList.get(i).toLanguageTag()); + } return langTagsToIgnore; } @Override public Set<LocaleStore.LocaleInfo> getSupportedLocaleList(LocaleStore.LocaleInfo parent, boolean translatedOnly, boolean isForCountryMode) { - AppLocaleStore.AppLocaleResult result = - AppLocaleStore.getAppSupportedLocales(mContext, mAppPackageName); + if (mAppCurrentLocale == null) { + mAppCurrentLocale = getAppCurrentLocale(); + } + if (mAllAppActiveLocales == null) { + mAllAppActiveLocales = getAllAppActiveLocales(); + } + if (mImeLocales == null) { + mImeLocales = getActiveImeLocales(); + } + AppLocaleStore.AppLocaleResult result = getAppSupportedLocales(); Set<String> langTagsToIgnore = getIgnoredLocaleList(translatedOnly); Set<LocaleStore.LocaleInfo> appLocaleList = new HashSet<>(); Set<LocaleStore.LocaleInfo> systemLocaleList; @@ -71,11 +206,9 @@ class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { // Get system supported locale list if (isForCountryMode) { - systemLocaleList = LocaleStore.getLevelLocales(mContext, - langTagsToIgnore, parent, translatedOnly); + systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, parent, translatedOnly); } else { - systemLocaleList = LocaleStore.getLevelLocales(mContext, langTagsToIgnore, - null /* no parent */, translatedOnly); + systemLocaleList = getSystemSupportedLocale(langTagsToIgnore, null, translatedOnly); } // Add current app locale @@ -84,19 +217,46 @@ class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { } // Add current system language into suggestion list - for(LocaleStore.LocaleInfo localeInfo: - LocaleStore.getSystemCurrentLocaleInfo()) { - boolean isNotCurrentLocale = mAppCurrentLocale == null - || !localeInfo.getLocale().equals(mAppCurrentLocale.getLocale()); - if (!isForCountryMode && isNotCurrentLocale) { - appLocaleList.add(localeInfo); + if (!isForCountryMode) { + boolean isCurrentLocale, isInAppOrIme; + for (LocaleStore.LocaleInfo localeInfo : getSystemCurrentLocale()) { + isCurrentLocale = mAppCurrentLocale != null + && localeInfo.getLocale().equals(mAppCurrentLocale.getLocale()); + isInAppOrIme = existsInAppOrIme(localeInfo.getLocale()); + if (!isCurrentLocale && !isInAppOrIme) { + appLocaleList.add(localeInfo); + } } } - // Add the languages that included in system supported locale + // Add the languages that are included in system supported locale + Set<LocaleStore.LocaleInfo> suggestedSet = null; if (shouldShowList) { - appLocaleList.addAll(filterTheLanguagesNotIncludedInSystemLocale( - systemLocaleList, result.mAppSupportedLocales)); + appLocaleList.addAll(filterSupportedLocales(systemLocaleList, + result.mAppSupportedLocales)); + suggestedSet = getSuggestedLocales(appLocaleList); + } + + if (!isForCountryMode && SystemProperties.getBoolean(PROP_APP_LANGUAGE_SUGGESTION, + ENABLED)) { + // Add the language that other apps activate into the suggestion list. + Set<LocaleStore.LocaleInfo> localeSet = filterSupportedLocales(mAllAppActiveLocales, + result.mAppSupportedLocales); + if (suggestedSet != null) { + // Filter out the locale with the same language and country + // like zh-TW vs zh-Hant-TW. + localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet); + } + appLocaleList.addAll(localeSet); + suggestedSet.addAll(localeSet); + + // Add the language that the active IME enables into the suggestion list. + localeSet = filterSupportedLocales(mImeLocales, result.mAppSupportedLocales); + if (suggestedSet != null) { + localeSet = filterSameLanguageAndCountry(localeSet, suggestedSet); + } + appLocaleList.addAll(localeSet); + suggestedSet.addAll(localeSet); } // Add "system language" option @@ -117,17 +277,55 @@ class AppLocaleCollector implements LocalePickerWithRegion.LocaleCollectorBase { return true; } - private Set<LocaleStore.LocaleInfo> filterTheLanguagesNotIncludedInSystemLocale( - Set<LocaleStore.LocaleInfo> systemLocaleList, + private Set<LocaleStore.LocaleInfo> getSuggestedLocales(Set<LocaleStore.LocaleInfo> localeSet) { + return localeSet.stream().filter(localeInfo -> localeInfo.isSuggested()).collect( + Collectors.toSet()); + } + + private Set<LocaleStore.LocaleInfo> filterSameLanguageAndCountry( + Set<LocaleStore.LocaleInfo> newLocaleList, + Set<LocaleStore.LocaleInfo> existingLocaleList) { + Set<LocaleStore.LocaleInfo> result = new HashSet<>(newLocaleList.size()); + for (LocaleStore.LocaleInfo appLocaleInfo : newLocaleList) { + boolean same = false; + Locale appLocale = appLocaleInfo.getLocale(); + for (LocaleStore.LocaleInfo localeInfo : existingLocaleList) { + Locale suggested = localeInfo.getLocale(); + if (appLocale.getLanguage().equals(suggested.getLanguage()) + && appLocale.getCountry().equals(suggested.getCountry())) { + same = true; + break; + } + } + if (!same) { + result.add(appLocaleInfo); + } + } + return result; + } + + private boolean existsInAppOrIme(Locale locale) { + boolean existInApp = mAllAppActiveLocales.stream().anyMatch( + localeInfo -> localeInfo.getLocale().equals(locale)); + if (existInApp) { + return true; + } else { + return mImeLocales.stream().anyMatch( + localeInfo -> localeInfo.getLocale().equals(locale)); + } + } + + private Set<LocaleStore.LocaleInfo> filterSupportedLocales( + Set<LocaleStore.LocaleInfo> suggestedLocales, HashSet<Locale> appSupportedLocales) { Set<LocaleStore.LocaleInfo> filteredList = new HashSet<>(); - for(LocaleStore.LocaleInfo li: systemLocaleList) { + for (LocaleStore.LocaleInfo li : suggestedLocales) { if (appSupportedLocales.contains(li.getLocale())) { filteredList.add(li); } else { - for(Locale l: appSupportedLocales) { - if(LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { + for (Locale l : appSupportedLocales) { + if (LocaleList.matchesLanguageAndScript(li.getLocale(), l)) { filteredList.add(li); break; } diff --git a/core/java/com/android/internal/app/AppLocaleStore.java b/core/java/com/android/internal/app/AppLocaleStore.java index f3a322cac79a..a450a05dd315 100644 --- a/core/java/com/android/internal/app/AppLocaleStore.java +++ b/core/java/com/android/internal/app/AppLocaleStore.java @@ -30,7 +30,10 @@ import java.util.HashSet; import java.util.Locale; import java.util.stream.Collectors; -class AppLocaleStore { +/** + * A class used to access an application's supporting locales. + */ +public class AppLocaleStore { private static final String TAG = AppLocaleStore.class.getSimpleName(); public static AppLocaleResult getAppSupportedLocales( @@ -148,8 +151,11 @@ class AppLocaleStore { return false; } - static class AppLocaleResult { - enum LocaleStatus { + /** + * A class used to store an application's supporting locales. + */ + public static class AppLocaleResult { + public enum LocaleStatus { UNKNOWN_FAILURE, NO_SUPPORTED_LANGUAGE_IN_APP, ASSET_LOCALE_IS_EMPTY, @@ -158,7 +164,7 @@ class AppLocaleStore { } LocaleStatus mLocaleStatus; - HashSet<Locale> mAppSupportedLocales; + public HashSet<Locale> mAppSupportedLocales; public AppLocaleResult(LocaleStatus localeStatus, HashSet<Locale> appSupportedLocales) { this.mLocaleStatus = localeStatus; diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index 689dec479c89..d2eee91d257f 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -23,6 +23,7 @@ import android.os.LocaleList; import android.provider.Settings; import android.telephony.TelephonyManager; import android.util.Log; +import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.VisibleForTesting; @@ -45,10 +46,11 @@ public class LocaleStore { @VisibleForTesting static final int SUGGESTION_TYPE_SIM = 1 << 0; @VisibleForTesting static final int SUGGESTION_TYPE_CFG = 1 << 1; // Only for per-app language picker - private static final int SUGGESTION_TYPE_CURRENT = 1 << 2; + @VisibleForTesting static final int SUGGESTION_TYPE_CURRENT = 1 << 2; // Only for per-app language picker - private static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3; - + @VisibleForTesting static final int SUGGESTION_TYPE_SYSTEM_LANGUAGE = 1 << 3; + // Only for per-app language picker + @VisibleForTesting static final int SUGGESTION_TYPE_OTHER_APP_LANGUAGE = 1 << 4; private final Locale mLocale; private final Locale mParent; private final String mId; @@ -259,7 +261,16 @@ public class LocaleStore { } } - public static LocaleInfo getAppCurrentLocaleInfo(Context context, String appPackageName) { + /** + * Get the application's activated locale. + * + * @param context UI activity's context. + * @param appPackageName The application's package name. + * @param isAppSelected True if the application is selected in the UI; false otherwise. + * @return A LocaleInfo with the application's activated locale. + */ + public static LocaleInfo getAppActivatedLocaleInfo(Context context, String appPackageName, + boolean isAppSelected) { if (appPackageName == null) { return null; } @@ -272,7 +283,11 @@ public class LocaleStore { if (locale != null) { LocaleInfo localeInfo = new LocaleInfo(locale); - localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT; + if (isAppSelected) { + localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CURRENT; + } else { + localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE; + } localeInfo.mIsTranslated = true; return localeInfo; } @@ -283,6 +298,24 @@ public class LocaleStore { } /** + * Transform IME's language tag to LocaleInfo. + * + * @param list A list which includes IME's subtype. + * @return A LocaleInfo set which includes IME's language tags. + */ + public static Set<LocaleInfo> transformImeLanguageTagToLocaleInfo( + List<InputMethodSubtype> list) { + Set<LocaleInfo> imeLocales = new HashSet<>(); + for (InputMethodSubtype subtype : list) { + LocaleInfo localeInfo = new LocaleInfo(subtype.getLanguageTag()); + localeInfo.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE; + localeInfo.mIsTranslated = true; + imeLocales.add(localeInfo); + } + return imeLocales; + } + + /** * Returns a list of system languages with LocaleInfo */ public static List<LocaleInfo> getSystemCurrentLocaleInfo() { @@ -458,4 +491,13 @@ public class LocaleStore { } return result; } + + /** + * API for testing. + */ + @UnsupportedAppUsage + @VisibleForTesting + public static LocaleInfo fromLocale(Locale locale) { + return new LocaleInfo(locale); + } } diff --git a/core/java/com/android/internal/app/OWNERS b/core/java/com/android/internal/app/OWNERS index 3833976fcdb1..970728f20141 100644 --- a/core/java/com/android/internal/app/OWNERS +++ b/core/java/com/android/internal/app/OWNERS @@ -2,6 +2,7 @@ per-file *AppOp* = file:/core/java/android/permission/OWNERS per-file *Resolver* = file:/packages/SystemUI/OWNERS per-file *Chooser* = file:/packages/SystemUI/OWNERS per-file SimpleIconFactory.java = file:/packages/SystemUI/OWNERS +per-file AbstractMultiProfilePagerAdapter.java = file:/packages/SystemUI/OWNERS per-file NetInitiatedActivity.java = file:/location/java/android/location/OWNERS per-file *BatteryStats* = file:/BATTERY_STATS_OWNERS @@ -11,4 +12,4 @@ per-file *Hotword* = file:/core/java/android/service/voice/OWNERS per-file *Voice* = file:/core/java/android/service/voice/OWNERS # System language settings -per-file *Locale* = file:platform/packages/apps/Settings:/src/com/android/settings/localepicker/OWNERS
\ No newline at end of file +per-file *Locale* = file:platform/packages/apps/Settings:/src/com/android/settings/localepicker/OWNERS diff --git a/core/java/com/android/internal/content/InstallLocationUtils.java b/core/java/com/android/internal/content/InstallLocationUtils.java index c456cf3a4bb6..4d9c09e92617 100644 --- a/core/java/com/android/internal/content/InstallLocationUtils.java +++ b/core/java/com/android/internal/content/InstallLocationUtils.java @@ -31,6 +31,7 @@ import android.os.Environment; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.storage.IStorageManager; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; @@ -261,12 +262,12 @@ public class InstallLocationUtils { // We're left with new installations with either preferring external or auto, so just pick // volume with most space + String bestCandidate = !volumePaths.isEmpty() ? volumePaths.keyAt(0) : null; if (volumePaths.size() == 1) { if (checkFitOnVolume(storageManager, volumePaths.valueAt(0), params)) { - return volumePaths.keyAt(0); + return bestCandidate; } } else { - String bestCandidate = null; long bestCandidateAvailBytes = Long.MIN_VALUE; for (String vol : volumePaths.keySet()) { final String volumePath = volumePaths.get(vol); @@ -289,6 +290,14 @@ public class InstallLocationUtils { } + // For new installations of a predefined size, check property to let it through + // regardless of the actual free space. + if (bestCandidate != null && Integer.MAX_VALUE == params.sizeBytes + && SystemProperties.getBoolean("debug.pm.install_skip_size_check_for_maxint", + false)) { + return bestCandidate; + } + throw new IOException("No special requests, but no room on allowed volumes. " + " allow3rdPartyOnInternal? " + allow3rdPartyOnInternal); } diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 7bd6ec8f0f2c..d9e9a5f19e2d 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -28,6 +28,8 @@ import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_IN import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_RECENTS; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_WIDGET; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_SWIPE_TO_RECENTS; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_SWIPE; +import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_TO_HOME; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_ALL_APPS; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_QUICK_SWITCH; import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION; @@ -228,6 +230,8 @@ public class InteractionJankMonitor { public static final int CUJ_LOCKSCREEN_OCCLUSION = 64; public static final int CUJ_RECENTS_SCROLLING = 65; public static final int CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS = 66; + public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE = 67; + public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME = 68; private static final int NO_STATSD_LOGGING = -1; @@ -303,6 +307,8 @@ public class InteractionJankMonitor { UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__RECENTS_SCROLLING, UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_SWIPE_TO_RECENTS, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_SWIPE, + UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_TO_HOME, }; private static class InstanceHolder { @@ -393,7 +399,9 @@ public class InteractionJankMonitor { CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, CUJ_LOCKSCREEN_OCCLUSION, CUJ_RECENTS_SCROLLING, - CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS + CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS, + CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE, + CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -911,6 +919,10 @@ public class InteractionJankMonitor { return "RECENTS_SCROLLING"; case CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS: return "LAUNCHER_APP_SWIPE_TO_RECENTS"; + case CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE: + return "LAUNCHER_CLOSE_ALL_APPS_SWIPE"; + case CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME: + return "LAUNCHER_CLOSE_ALL_APPS_TO_HOME"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/os/ProcessCpuTracker.java b/core/java/com/android/internal/os/ProcessCpuTracker.java index 0df006d3a9bf..65655b7b5a66 100644 --- a/core/java/com/android/internal/os/ProcessCpuTracker.java +++ b/core/java/com/android/internal/os/ProcessCpuTracker.java @@ -841,7 +841,19 @@ public class ProcessCpuTracker { return sw.toString(); } - final public String printCurrentState(long now) { + /** + * Returns current CPU state with all the processes as a String, sorted by load + * in descending order. + */ + public final String printCurrentState(long now) { + return printCurrentState(now, Integer.MAX_VALUE); + } + + /** + * Returns current CPU state with the top {@code maxProcessesToDump} highest load + * processes as a String, sorted by load in descending order. + */ + public final String printCurrentState(long now, int maxProcessesToDump) { final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); buildWorkingProcs(); @@ -883,8 +895,8 @@ public class ProcessCpuTracker { if (DEBUG) Slog.i(TAG, "totalTime " + totalTime + " over sample time " + (mCurrentSampleTime-mLastSampleTime)); - int N = mWorkingProcs.size(); - for (int i=0; i<N; i++) { + int dumpedProcessCount = Math.min(maxProcessesToDump, mWorkingProcs.size()); + for (int i = 0; i < dumpedProcessCount; i++) { Stats st = mWorkingProcs.get(i); printProcessCPU(pw, st.added ? " +" : (st.removed ? " -": " "), st.pid, st.name, (int)st.rel_uptime, diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 19cb30e4b6cb..75f84023766f 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -2739,12 +2739,26 @@ static jint android_media_AudioSystem_setDevicesRoleForStrategy(JNIEnv *env, job } static jint android_media_AudioSystem_removeDevicesRoleForStrategy(JNIEnv *env, jobject thiz, - jint strategy, jint role) { + jint strategy, jint role, + jintArray jDeviceTypes, + jobjectArray jDeviceAddresses) { + AudioDeviceTypeAddrVector nDevices; + jint results = getVectorOfAudioDeviceTypeAddr(env, jDeviceTypes, jDeviceAddresses, nDevices); + if (results != NO_ERROR) { + return results; + } + int status = check_AudioSystem_Command( + AudioSystem::removeDevicesRoleForStrategy((product_strategy_t)strategy, + (device_role_t)role, nDevices)); + return (jint)status; +} + +static jint android_media_AudioSystem_clearDevicesRoleForStrategy(JNIEnv *env, jobject thiz, + jint strategy, jint role) { return (jint) - check_AudioSystem_Command(AudioSystem::removeDevicesRoleForStrategy((product_strategy_t) - strategy, - (device_role_t) - role), + check_AudioSystem_Command(AudioSystem::clearDevicesRoleForStrategy((product_strategy_t) + strategy, + (device_role_t)role), {NAME_NOT_FOUND}); } @@ -3341,8 +3355,10 @@ static const JNINativeMethod gMethods[] = (void *)android_media_AudioSystem_isCallScreeningModeSupported}, {"setDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I", (void *)android_media_AudioSystem_setDevicesRoleForStrategy}, - {"removeDevicesRoleForStrategy", "(II)I", + {"removeDevicesRoleForStrategy", "(II[I[Ljava/lang/String;)I", (void *)android_media_AudioSystem_removeDevicesRoleForStrategy}, + {"clearDevicesRoleForStrategy", "(II)I", + (void *)android_media_AudioSystem_clearDevicesRoleForStrategy}, {"getDevicesForRoleAndStrategy", "(IILjava/util/List;)I", (void *)android_media_AudioSystem_getDevicesForRoleAndStrategy}, {"setDevicesRoleForCapturePreset", "(II[I[Ljava/lang/String;)I", diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 3274e85cb88a..124d9e660aad 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -258,6 +258,7 @@ android:name="com.android.bluetooth.BluetoothMapContentObserver.action.MESSAGE_DELIVERY" /> <protected-broadcast android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" /> + <protected-broadcast android:name="android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED" /> <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED" /> <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED" /> <protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONF_CHANGED" /> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 1072f57e7928..1d1c02d648d8 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -9401,8 +9401,34 @@ <attr name="label" /> <!-- The key character map file resource. --> <attr name="keyboardLayout" format="reference" /> - <!-- The locales the given keyboard layout corresponds to. --> - <attr name="locale" format="string" /> + <!-- The locales the given keyboard layout corresponds to. This is a list of + BCP-47 conformant language tags separated by the delimiter ',' or '|'. + Some examples of language tags are: en-US, zh-Hans-CN, el-Grek-polyton. + It includes information for language code, country code, variant, and script + code like ‘Latn’, ‘Cyrl’, etc. --> + <attr name="keyboardLocale" format="string"/> + <!-- The layout type of the given keyboardLayout. + NOTE: The enum to int value mapping must remain stable --> + <attr name="keyboardLayoutType" format="enum"> + <!-- Qwerty-based keyboard layout. --> + <enum name="qwerty" value="1" /> + <!-- Qwertz-based keyboard layout. --> + <enum name="qwertz" value="2" /> + <!-- Azerty-based keyboard layout. --> + <enum name="azerty" value="3" /> + <!-- Dvorak keyboard layout. --> + <enum name="dvorak" value="4" /> + <!-- Colemak keyboard layout. --> + <enum name="colemak" value="5" /> + <!-- Workman keyboard layout. --> + <enum name="workman" value="6" /> + <!-- Turkish-Q keyboard layout. --> + <enum name="turkish_q" value="7" /> + <!-- Turkish-F keyboard layout. --> + <enum name="turkish_f" value="8" /> + <!-- Keyboard layout that has been enhanced with a large number of extra characters. --> + <enum name="extended" value="9" /> + </attr> <!-- The vendor ID of the hardware the given layout corresponds to. @hide --> <attr name="vendorId" format="integer" /> <!-- The product ID of the hardware the given layout corresponds to. @hide --> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 44f85dad9521..57eff9a63f5d 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1027,6 +1027,9 @@ <flag name="layoutDirection" value="0x2000" /> <!-- The color mode of the screen has changed (color gamut or dynamic range). --> <flag name="colorMode" value="0x4000" /> + <!-- The grammatical gender has changed, for example the user set the grammatical gender + from the UI. --> + <flag name="grammaticalGender" value="0x8000" /> <!-- The font scaling factor has changed, that is the user has selected a new global font size. --> <flag name="fontScale" value="0x40000000" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 35ff7e855b75..184f86968e51 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2701,6 +2701,10 @@ <!-- Whether UI for multi user should be shown --> <bool name="config_enableMultiUserUI">false</bool> + <!-- Whether multiple admins are allowed on the device. If set to true, new users can be created + with admin privileges and admin privileges can be granted/revoked from existing users. --> + <bool name="config_enableMultipleAdmins">false</bool> + <!-- Whether the new Auto Selection Network UI should be shown --> <bool name="config_enableNewAutoSelectNetworkUI">false</bool> @@ -5366,6 +5370,11 @@ TODO(b/255532890) Enable when ignoreOrientationRequest is set --> <bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool> + <!-- Whether sending compat fake focus for split screen resumed activities is enabled. + Needed because some game engines wait to get focus before drawing the content of + the app which isn't guaranteed by default in multi-window modes. --> + <bool name="config_isCompatFakeFocusEnabled">false</bool> + <!-- Whether camera compat treatment is enabled for issues caused by orientation mismatch between camera buffers and an app window. This includes force rotation of fixed orientation activities connected to the camera in fullscreen and showing a tooltip in diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 2924ddf69d16..97feaace7e59 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -122,6 +122,8 @@ <public name="physicalKeyboardHintLanguageTag" /> <public name="physicalKeyboardHintLayoutType" /> <public name="allowSharedIsolatedProcess" /> + <public name="keyboardLocale" /> + <public name="keyboardLayoutType" /> </staging-public-group> <staging-public-group type="id" first-id="0x01cd0000"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index c73f2f498355..af29b233ba83 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -353,6 +353,7 @@ <java-symbol type="bool" name="config_speed_up_audio_on_mt_calls" /> <java-symbol type="bool" name="config_useFixedVolume" /> <java-symbol type="bool" name="config_enableMultiUserUI"/> + <java-symbol type="bool" name="config_enableMultipleAdmins"/> <java-symbol type="bool" name="config_enableNewAutoSelectNetworkUI"/> <java-symbol type="bool" name="config_disableUsbPermissionDialogs"/> <java-symbol type="dimen" name="config_highResTaskSnapshotScale" /> @@ -4432,6 +4433,7 @@ <java-symbol type="bool" name="config_letterboxIsEducationEnabled" /> <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" /> <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" /> + <java-symbol type="bool" name="config_isCompatFakeFocusEnabled" /> <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" /> <java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" /> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java index 87f91fa436ed..9a999e4067c5 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java @@ -72,9 +72,9 @@ public final class ProgramListTest { /* value= */ 94300); private static final ProgramSelector.Identifier RDS_IDENTIFIER = new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_RDS_PI, 15019); - private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER = - new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, - /* value= */ 0x10000111); + private static final ProgramSelector.Identifier DAB_DMB_SID_EXT_IDENTIFIER = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, + /* value= */ 0xA000000111L); private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER = new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, /* value= */ 0x1013); @@ -89,7 +89,7 @@ public final class ProgramListTest { private static final ProgramList.Chunk FM_RDS_ADD_CHUNK = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO), - Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); + Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); private static final ProgramList.Chunk FM_ADD_INCOMPLETE_CHUNK = new ProgramList.Chunk(IS_PURGE, /* complete= */ false, Set.of(FM_PROGRAM_INFO), new ArraySet<>()); private static final ProgramList.Filter TEST_FILTER = new ProgramList.Filter( @@ -216,7 +216,7 @@ public final class ProgramListTest { public void isPurge_forChunk() { ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO), - Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); + Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); assertWithMessage("Puring chunk").that(chunk.isPurge()).isEqualTo(IS_PURGE); } @@ -225,7 +225,7 @@ public final class ProgramListTest { public void isComplete_forChunk() { ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO), - Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); + Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); assertWithMessage("Complete chunk").that(chunk.isComplete()).isEqualTo(IS_COMPLETE); } @@ -234,7 +234,7 @@ public final class ProgramListTest { public void getModified_forChunk() { ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO), - Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); + Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); assertWithMessage("Modified program info in chunk") .that(chunk.getModified()).containsExactly(FM_PROGRAM_INFO, RDS_PROGRAM_INFO); @@ -244,10 +244,10 @@ public final class ProgramListTest { public void getRemoved_forChunk() { ProgramList.Chunk chunk = new ProgramList.Chunk(IS_PURGE, IS_COMPLETE, Set.of(FM_PROGRAM_INFO, RDS_PROGRAM_INFO), - Set.of(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); + Set.of(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER)); assertWithMessage("Removed program identifiers in chunk").that(chunk.getRemoved()) - .containsExactly(DAB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER); + .containsExactly(DAB_DMB_SID_EXT_IDENTIFIER, DAB_ENSEMBLE_IDENTIFIER); } @Test @@ -276,6 +276,19 @@ public final class ProgramListTest { } @Test + public void getProgramList_forTunerAdapterWhenServiceDied_fails() throws Exception { + Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); + createRadioTuner(); + doThrow(new RemoteException()).when(mTunerMock).startProgramListUpdates(any()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.getProgramList(parameters)); + + assertWithMessage("Exception for getting program list when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void getDynamicProgramList_forTunerAdapter() throws Exception { createRadioTuner(); diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java index 5bd018bea1d1..9399907cf33c 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramSelectorTest.java @@ -36,18 +36,18 @@ public final class ProgramSelectorTest { private static final long AM_FREQUENCY = 700; private static final ProgramSelector.Identifier FM_IDENTIFIER = new ProgramSelector.Identifier( ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, FM_FREQUENCY); - private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_1 = - new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, - /* value= */ 0x1000011); - private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_2 = - new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, - /* value= */ 0x10000112); + private static final ProgramSelector.Identifier DAB_DMB_SID_EXT_IDENTIFIER_1 = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, + /* value= */ 0xA000000111L); + private static final ProgramSelector.Identifier DAB_DMB_SID_EXT_IDENTIFIER_2 = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, + /* value= */ 0xA000000112L); private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER = new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, /* value= */ 0x1001); private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER = new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, - /* value= */ 94500); + /* value= */ 220352); @Test public void getType_forIdentifier() { @@ -80,13 +80,13 @@ public final class ProgramSelectorTest { @Test public void equals_withDifferentTypesForIdentifiers_returnsFalse() { assertWithMessage("Identifier with different identifier type") - .that(FM_IDENTIFIER).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1); + .that(FM_IDENTIFIER).isNotEqualTo(DAB_DMB_SID_EXT_IDENTIFIER_1); } @Test public void equals_withDifferentValuesForIdentifiers_returnsFalse() { assertWithMessage("Identifier with different identifier value") - .that(DAB_SID_EXT_IDENTIFIER_2).isNotEqualTo(DAB_SID_EXT_IDENTIFIER_1); + .that(DAB_DMB_SID_EXT_IDENTIFIER_2).isNotEqualTo(DAB_DMB_SID_EXT_IDENTIFIER_1); } @Test @@ -168,19 +168,19 @@ public final class ProgramSelectorTest { @Test public void getFirstId_withIdInSelector() { ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{ - DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER}; + DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER}; ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null); - long firstIdValue = selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT); + long firstIdValue = selector.getFirstId(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT); assertWithMessage("Value of the first DAB_SID_EXT identifier") - .that(firstIdValue).isEqualTo(DAB_SID_EXT_IDENTIFIER_1.getValue()); + .that(firstIdValue).isEqualTo(DAB_DMB_SID_EXT_IDENTIFIER_1.getValue()); } @Test public void getFirstId_withIdNotInSelector() { ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{ - DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2}; + DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2}; ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null); int idType = ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY; @@ -195,13 +195,13 @@ public final class ProgramSelectorTest { @Test public void getAllIds_withIdInSelector() { ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{ - DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER}; + DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER}; ProgramSelector.Identifier[] allIdsExpected = - {DAB_SID_EXT_IDENTIFIER_1, DAB_SID_EXT_IDENTIFIER_2}; + {DAB_DMB_SID_EXT_IDENTIFIER_1, DAB_DMB_SID_EXT_IDENTIFIER_2}; ProgramSelector selector = getDabSelector(secondaryIds, /* vendorIds= */ null); ProgramSelector.Identifier[] allIds = - selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT); + selector.getAllIds(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT); assertWithMessage("All DAB_SID_EXT identifiers in selector") .that(allIds).isEqualTo(allIdsExpected); @@ -244,14 +244,14 @@ public final class ProgramSelectorTest { @Test public void withSecondaryPreferred() { ProgramSelector.Identifier[] secondaryIds = new ProgramSelector.Identifier[]{ - DAB_ENSEMBLE_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER}; + DAB_ENSEMBLE_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_2, DAB_FREQUENCY_IDENTIFIER}; long[] vendorIdsExpected = {12345, 678}; ProgramSelector selector = getDabSelector(secondaryIds, vendorIdsExpected); ProgramSelector.Identifier[] secondaryIdsExpected = new ProgramSelector.Identifier[]{ - DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER, DAB_SID_EXT_IDENTIFIER_1}; + DAB_ENSEMBLE_IDENTIFIER, DAB_FREQUENCY_IDENTIFIER, DAB_DMB_SID_EXT_IDENTIFIER_1}; ProgramSelector selectorPreferred = - selector.withSecondaryPreferred(DAB_SID_EXT_IDENTIFIER_1); + selector.withSecondaryPreferred(DAB_DMB_SID_EXT_IDENTIFIER_1); assertWithMessage("Program type") .that(selectorPreferred.getProgramType()).isEqualTo(selector.getProgramType()); @@ -458,7 +458,7 @@ public final class ProgramSelectorTest { private ProgramSelector getDabSelector(@Nullable ProgramSelector.Identifier[] secondaryIds, @Nullable long[] vendorIds) { - return new ProgramSelector(DAB_PROGRAM_TYPE, DAB_SID_EXT_IDENTIFIER_1, secondaryIds, + return new ProgramSelector(DAB_PROGRAM_TYPE, DAB_DMB_SID_EXT_IDENTIFIER_1, secondaryIds, vendorIds); } } diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java index 03742eb93a3b..afbf8c304e3d 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java @@ -18,7 +18,9 @@ package android.hardware.radio.tests.unittests; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; @@ -105,17 +107,17 @@ public final class RadioManagerTest { private static final int INFO_FLAGS = 0b110001; private static final int SIGNAL_QUALITY = 2; private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER = - new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, - /* value= */ 0x10000111); + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, + /* value= */ 0xA000000111L); private static final ProgramSelector.Identifier DAB_SID_EXT_IDENTIFIER_RELATED = - new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, - /* value= */ 0x10000113); + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, + /* value= */ 0xA000000113L); private static final ProgramSelector.Identifier DAB_ENSEMBLE_IDENTIFIER = new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, /* value= */ 0x1013); private static final ProgramSelector.Identifier DAB_FREQUENCY_IDENTIFIER = new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, - /* value= */ 95500); + /* value= */ 220352); private static final ProgramSelector DAB_SELECTOR = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, DAB_SID_EXT_IDENTIFIER, new ProgramSelector.Identifier[]{ @@ -859,13 +861,13 @@ public final class RadioManagerTest { @Test public void getLogicallyTunedTo_forProgramInfo() { assertWithMessage("Identifier logically tuned to in DAB program info") - .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER); + .that(DAB_PROGRAM_INFO.getLogicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER); } @Test public void getPhysicallyTunedTo_forProgramInfo() { assertWithMessage("Identifier physically tuned to DAB program info") - .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_SID_EXT_IDENTIFIER); + .that(DAB_PROGRAM_INFO.getPhysicallyTunedTo()).isEqualTo(DAB_FREQUENCY_IDENTIFIER); } @Test @@ -1006,6 +1008,35 @@ public final class RadioManagerTest { } @Test + public void listModules_forRadioManagerWithNullListAsInput_fails() throws Exception { + createRadioManager(); + + assertWithMessage("Status when listing module with empty list input") + .that(mRadioManager.listModules(null)).isEqualTo(RadioManager.STATUS_BAD_VALUE); + } + + @Test + public void listModules_withNullListFromService_fails() throws Exception { + createRadioManager(); + when(mRadioServiceMock.listModules()).thenReturn(null); + List<RadioManager.ModuleProperties> modules = new ArrayList<>(); + + assertWithMessage("Status for listing module when getting null list from HAL client") + .that(mRadioManager.listModules(modules)).isEqualTo(RadioManager.STATUS_ERROR); + } + + @Test + public void listModules_whenServiceDied_fails() throws Exception { + createRadioManager(); + when(mRadioServiceMock.listModules()).thenThrow(new RemoteException()); + List<RadioManager.ModuleProperties> modules = new ArrayList<>(); + + assertWithMessage("Status for listing module when HAL client service is dead") + .that(mRadioManager.listModules(modules)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void openTuner_forRadioModule() throws Exception { createRadioManager(); int moduleId = 0; @@ -1019,6 +1050,18 @@ public final class RadioManagerTest { } @Test + public void openTuner_whenServiceDied_returnsNull() throws Exception { + createRadioManager(); + when(mRadioServiceMock.openTuner(anyInt(), any(), anyBoolean(), any(), anyInt())) + .thenThrow(new RemoteException()); + + RadioTuner nullTuner = mRadioManager.openTuner(/* moduleId= */ 0, FM_BAND_CONFIG, + /* withAudio= */ true, mCallbackMock, /* handler= */ null); + + assertWithMessage("Radio tuner when service is dead").that(nullTuner).isNull(); + } + + @Test public void addAnnouncementListener_withListenerNotAddedBefore() throws Exception { createRadioManager(); Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE); @@ -1049,6 +1092,21 @@ public final class RadioManagerTest { } @Test + public void addAnnouncementListener_whenServiceDied_throwException() throws Exception { + createRadioManager(); + String exceptionMessage = "service is dead"; + when(mRadioServiceMock.addAnnouncementListener(any(), any())) + .thenThrow(new RemoteException(exceptionMessage)); + Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener)); + + assertWithMessage("Exception for adding announcement listener with dead service") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void removeAnnouncementListener_withListenerNotAddedBefore_ignores() throws Exception { createRadioManager(); @@ -1104,8 +1162,8 @@ public final class RadioManagerTest { } private static RadioManager.ProgramInfo createDabProgramInfo(ProgramSelector selector) { - return new RadioManager.ProgramInfo(selector, DAB_FREQUENCY_IDENTIFIER, - DAB_SID_EXT_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS, + return new RadioManager.ProgramInfo(selector, DAB_SID_EXT_IDENTIFIER, + DAB_FREQUENCY_IDENTIFIER, Arrays.asList(DAB_SID_EXT_IDENTIFIER_RELATED), INFO_FLAGS, SIGNAL_QUALITY, METADATA, /* vendorInfo= */ null); } diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java index d851a7724e8e..c8b4493a07d7 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java @@ -18,10 +18,13 @@ package android.hardware.radio.tests.unittests; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,6 +40,7 @@ import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; import android.hardware.radio.RadioTuner; import android.os.Build; +import android.os.RemoteException; import org.junit.After; import org.junit.Before; @@ -46,7 +50,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -131,6 +134,24 @@ public final class TunerAdapterTest { } @Test + public void setConfiguration_withInvalidParameters_fails() throws Exception { + doThrow(new IllegalArgumentException()).when(mTunerMock).setConfiguration(any()); + + assertWithMessage("Status for setting configuration with invalid parameters") + .that(mRadioTuner.setConfiguration(TEST_BAND_CONFIG)) + .isEqualTo(RadioManager.STATUS_BAD_VALUE); + } + + @Test + public void setConfiguration_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).setConfiguration(any()); + + assertWithMessage("Status for setting configuration when service is dead") + .that(mRadioTuner.setConfiguration(TEST_BAND_CONFIG)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void getConfiguration_forTunerAdapter() throws Exception { when(mTunerMock.getConfiguration()).thenReturn(TEST_BAND_CONFIG); RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1]; @@ -144,14 +165,52 @@ public final class TunerAdapterTest { } @Test + public void getConfiguration_withInvalidParameters_fails() throws Exception { + RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[0]; + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mRadioTuner.getConfiguration(bandConfigs)); + + assertWithMessage("Exception for getting configuration with invalid parameters") + .that(thrown).hasMessageThat().contains("must be an array of length 1"); + } + + @Test + public void getConfiguration_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).getConfiguration(); + RadioManager.BandConfig[] bandConfigs = new RadioManager.BandConfig[1]; + + assertWithMessage("Status for getting configuration when service is dead") + .that(mRadioTuner.getConfiguration(bandConfigs)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void setMute_forTunerAdapter() { - int status = mRadioTuner.setMute(/* mute= */ true); + int status = mRadioTuner.setMute(true); assertWithMessage("Status for setting mute") .that(status).isEqualTo(RadioManager.STATUS_OK); } @Test + public void setMute_whenIllegalState_fails() throws Exception { + doThrow(new IllegalStateException()).when(mTunerMock).setMuted(anyBoolean()); + + assertWithMessage("Status for setting muted when service is in illegal state") + .that(mRadioTuner.setMute(true)).isEqualTo(RadioManager.STATUS_ERROR); + } + + @Test + public void setMute_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).setMuted(anyBoolean()); + + assertWithMessage("Status for setting muted when service is dead") + .that(mRadioTuner.setMute(true)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void getMute_forTunerAdapter() throws Exception { when(mTunerMock.isMuted()).thenReturn(true); @@ -161,6 +220,14 @@ public final class TunerAdapterTest { } @Test + public void getMute_whenServiceDied_returnsTrue() throws Exception { + when(mTunerMock.isMuted()).thenThrow(new RemoteException()); + + assertWithMessage("Status for getting muted when service is dead") + .that(mRadioTuner.getMute()).isEqualTo(true); + } + + @Test public void step_forTunerAdapter_succeeds() throws Exception { doAnswer(invocation -> { mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); @@ -176,6 +243,24 @@ public final class TunerAdapterTest { } @Test + public void step_whenIllegalState_fails() throws Exception { + doThrow(new IllegalStateException()).when(mTunerMock).step(anyBoolean(), anyBoolean()); + + assertWithMessage("Status for stepping when service is in illegal state") + .that(mRadioTuner.step(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false)) + .isEqualTo(RadioManager.STATUS_INVALID_OPERATION); + } + + @Test + public void step_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).step(anyBoolean(), anyBoolean()); + + assertWithMessage("Status for stepping when service is dead") + .that(mRadioTuner.step(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void scan_forTunerAdapter_succeeds() throws Exception { doAnswer(invocation -> { mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); @@ -191,13 +276,31 @@ public final class TunerAdapterTest { } @Test + public void scan_whenIllegalState_fails() throws Exception { + doThrow(new IllegalStateException()).when(mTunerMock).seek(anyBoolean(), anyBoolean()); + + assertWithMessage("Status for scanning when service is in illegal state") + .that(mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false)) + .isEqualTo(RadioManager.STATUS_INVALID_OPERATION); + } + + @Test + public void scan_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).seek(anyBoolean(), anyBoolean()); + + assertWithMessage("Status for scan when service is dead") + .that(mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ true)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void seek_forTunerAdapter_succeeds() throws Exception { doAnswer(invocation -> { mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO); return RadioManager.STATUS_OK; }).when(mTunerMock).seek(anyBoolean(), anyBoolean()); - int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false); + int scanStatus = mRadioTuner.seek(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false); verify(mTunerMock).seek(/* directionDown= */ true, /* skipSubChannel= */ false); assertWithMessage("Status for seeking") @@ -212,13 +315,31 @@ public final class TunerAdapterTest { return RadioManager.STATUS_OK; }).when(mTunerMock).seek(anyBoolean(), anyBoolean()); - mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true); + mRadioTuner.seek(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ true); verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onTuneFailed( RadioTuner.TUNER_RESULT_TIMEOUT, FM_SELECTOR); } @Test + public void seek_whenIllegalState_fails() throws Exception { + doThrow(new IllegalStateException()).when(mTunerMock).seek(anyBoolean(), anyBoolean()); + + assertWithMessage("Status for seeking when service is in illegal state") + .that(mRadioTuner.seek(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ false)) + .isEqualTo(RadioManager.STATUS_INVALID_OPERATION); + } + + @Test + public void seek_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).seek(anyBoolean(), anyBoolean()); + + assertWithMessage("Status for seeking when service is dead") + .that(mRadioTuner.seek(RadioTuner.DIRECTION_UP, /* skipSubChannel= */ true)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void tune_withChannelsForTunerAdapter_succeeds() { int status = mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0); @@ -228,6 +349,33 @@ public final class TunerAdapterTest { } @Test + public void tune_withInvalidChannel_fails() throws Exception { + doThrow(new IllegalArgumentException()).when(mTunerMock).tune(any()); + + assertWithMessage("Status for tuning when service is in illegal state") + .that(mRadioTuner.tune(/* channel= */ 300, /* subChannel= */ 0)) + .isEqualTo(RadioManager.STATUS_BAD_VALUE); + } + + @Test + public void tune_withChannelsWhenIllegalState_fails() throws Exception { + doThrow(new IllegalStateException()).when(mTunerMock).tune(any()); + + assertWithMessage("Status for tuning when service is in illegal state") + .that(mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0)) + .isEqualTo(RadioManager.STATUS_INVALID_OPERATION); + } + + @Test + public void tune_withChannelsWhenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).tune(any()); + + assertWithMessage("Status for tuning when service is dead") + .that(mRadioTuner.tune(/* channel= */ 92300, /* subChannel= */ 0)) + .isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void tune_withValidSelectorForTunerAdapter_succeeds() throws Exception { mRadioTuner.tune(FM_SELECTOR); @@ -250,6 +398,17 @@ public final class TunerAdapterTest { } @Test + public void tune_withSelectorWhenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).tune(any()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.tune(FM_SELECTOR)); + + assertWithMessage("Exception for tuning when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void cancel_forTunerAdapter() throws Exception { mRadioTuner.tune(FM_SELECTOR); @@ -259,6 +418,22 @@ public final class TunerAdapterTest { } @Test + public void cancel_whenIllegalState_fails() throws Exception { + doThrow(new IllegalStateException()).when(mTunerMock).cancel(); + + assertWithMessage("Status for canceling when service is in illegal state") + .that(mRadioTuner.cancel()).isEqualTo(RadioManager.STATUS_INVALID_OPERATION); + } + + @Test + public void cancel_forTunerAdapterWhenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).cancel(); + + assertWithMessage("Status for canceling when service is dead") + .that(mRadioTuner.cancel()).isEqualTo(RadioManager.STATUS_DEAD_OBJECT); + } + + @Test public void cancelAnnouncement_forTunerAdapter() throws Exception { mRadioTuner.cancelAnnouncement(); @@ -266,6 +441,17 @@ public final class TunerAdapterTest { } @Test + public void cancelAnnouncement_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).cancelAnnouncement(); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.cancelAnnouncement()); + + assertWithMessage("Exception for canceling announcement when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void getProgramInfo_beforeProgramInfoSetForTunerAdapter() { RadioManager.ProgramInfo[] programInfoArray = new RadioManager.ProgramInfo[1]; @@ -295,13 +481,24 @@ public final class TunerAdapterTest { when(mTunerMock.getImage(anyInt())).thenReturn(bitmapExpected); int imageId = 1; - Bitmap image = mRadioTuner.getMetadataImage(/* id= */ imageId); + Bitmap image = mRadioTuner.getMetadataImage(imageId); assertWithMessage("Image obtained from id %s", imageId) .that(image).isEqualTo(bitmapExpected); } @Test + public void getMetadataImage_whenServiceDied_fails() throws Exception { + when(mTunerMock.getImage(anyInt())).thenThrow(new RemoteException()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.getMetadataImage(/* id= */ 1)); + + assertWithMessage("Exception for getting metadata image when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void startBackgroundScan_forTunerAdapter() throws Exception { when(mTunerMock.startBackgroundScan()).thenReturn(false); @@ -312,6 +509,17 @@ public final class TunerAdapterTest { } @Test + public void startBackgroundScan_whenServiceDied_fails() throws Exception { + when(mTunerMock.startBackgroundScan()).thenThrow(new RemoteException()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.startBackgroundScan()); + + assertWithMessage("Exception for background scan when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void isAnalogForced_forTunerAdapter() throws Exception { when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)).thenReturn(true); @@ -322,6 +530,19 @@ public final class TunerAdapterTest { } @Test + public void isAnalogForced_whenNotSupported_fails() throws Exception { + String errorMessage = "Analog forced switch is not supported"; + when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG)) + .thenThrow(new UnsupportedOperationException(errorMessage)); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, + () -> mRadioTuner.isAnalogForced()); + + assertWithMessage("Exception for checking analog playback switch when not supported") + .that(thrown).hasMessageThat().contains(errorMessage); + } + + @Test public void setAnalogForced_forTunerAdapter() throws Exception { boolean analogForced = true; @@ -331,6 +552,19 @@ public final class TunerAdapterTest { } @Test + public void setAnalogForced_whenNotSupported_fails() throws Exception { + String errorMessage = "Analog forced switch is not supported"; + doThrow(new UnsupportedOperationException(errorMessage)) + .when(mTunerMock).setConfigFlag(eq(RadioManager.CONFIG_FORCE_ANALOG), anyBoolean()); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, + () -> mRadioTuner.setAnalogForced(/* isForced= */ false)); + + assertWithMessage("Exception for setting analog playback switch when not supported") + .that(thrown).hasMessageThat().contains(errorMessage); + } + + @Test public void isConfigFlagSupported_forTunerAdapter() throws Exception { when(mTunerMock.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING)) .thenReturn(true); @@ -343,6 +577,17 @@ public final class TunerAdapterTest { } @Test + public void isConfigFlagSupported_whenServiceDied_fails() throws Exception { + when(mTunerMock.isConfigFlagSupported(anyInt())).thenThrow(new RemoteException()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.isConfigFlagSupported(RadioManager.CONFIG_DAB_DAB_LINKING)); + + assertWithMessage("Exception for checking config flag support when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void isConfigFlagSet_forTunerAdapter() throws Exception { when(mTunerMock.isConfigFlagSet(RadioManager.CONFIG_DAB_FM_SOFT_LINKING)) .thenReturn(true); @@ -355,6 +600,17 @@ public final class TunerAdapterTest { } @Test + public void isConfigFlagSet_whenServiceDied_fails() throws Exception { + when(mTunerMock.isConfigFlagSet(anyInt())).thenThrow(new RemoteException()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.isConfigFlagSet(RadioManager.CONFIG_DAB_DAB_LINKING)); + + assertWithMessage("Exception for getting config flag when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void setConfigFlag_forTunerAdapter() throws Exception { boolean dabFmLinking = true; @@ -364,8 +620,20 @@ public final class TunerAdapterTest { } @Test + public void setConfigFlag_whenServiceDied_fails() throws Exception { + doThrow(new RemoteException()).when(mTunerMock).setConfigFlag(anyInt(), anyBoolean()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.setConfigFlag(RadioManager.CONFIG_DAB_DAB_LINKING, + /* value= */ true)); + + assertWithMessage("Exception for setting config flag when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void getParameters_forTunerAdapter() throws Exception { - List<String> parameterKeys = Arrays.asList("ParameterKeyMock"); + List<String> parameterKeys = List.of("ParameterKeyMock"); Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); when(mTunerMock.getParameters(parameterKeys)).thenReturn(parameters); @@ -374,6 +642,18 @@ public final class TunerAdapterTest { } @Test + public void getParameters_whenServiceDied_fails() throws Exception { + List<String> parameterKeys = List.of("ParameterKeyMock"); + when(mTunerMock.getParameters(parameterKeys)).thenThrow(new RemoteException()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.getParameters(parameterKeys)); + + assertWithMessage("Exception for getting parameters when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void setParameters_forTunerAdapter() throws Exception { Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); when(mTunerMock.setParameters(parameters)).thenReturn(parameters); @@ -383,6 +663,18 @@ public final class TunerAdapterTest { } @Test + public void setParameters_whenServiceDied_fails() throws Exception { + Map<String, String> parameters = Map.of("ParameterKeyMock", "ParameterValueMock"); + when(mTunerMock.setParameters(parameters)).thenThrow(new RemoteException()); + + RuntimeException thrown = assertThrows(RuntimeException.class, + () -> mRadioTuner.setParameters(parameters)); + + assertWithMessage("Exception for setting parameters when service is dead") + .that(thrown).hasMessageThat().contains("Service died"); + } + + @Test public void isAntennaConnected_forTunerAdapter() throws Exception { mTunerCallback.onAntennaState(/* connected= */ false); @@ -391,6 +683,15 @@ public final class TunerAdapterTest { } @Test + public void onError_forTunerAdapter() throws Exception { + int errorStatus = RadioTuner.ERROR_HARDWARE_FAILURE; + + mTunerCallback.onError(errorStatus); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(errorStatus); + } + + @Test public void hasControl_forTunerAdapter() throws Exception { when(mTunerMock.isClosed()).thenReturn(true); @@ -398,6 +699,14 @@ public final class TunerAdapterTest { } @Test + public void hasControl_whenServiceDied_returnsFalse() throws Exception { + when(mTunerMock.isClosed()).thenThrow(new RemoteException()); + + assertWithMessage("Control on tuner when service is dead") + .that(mRadioTuner.hasControl()).isFalse(); + } + + @Test public void onConfigurationChanged_forTunerCallbackAdapter() throws Exception { mTunerCallback.onConfigurationChanged(TEST_BAND_CONFIG); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java index a4212180d0b5..82db716fcdc2 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AidlTestUtils.java @@ -19,6 +19,7 @@ import android.hardware.broadcastradio.IdentifierType; import android.hardware.broadcastradio.Metadata; import android.hardware.broadcastradio.ProgramIdentifier; import android.hardware.broadcastradio.ProgramInfo; +import android.hardware.broadcastradio.ProgramListChunk; import android.hardware.broadcastradio.VendorKeyValue; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; @@ -41,17 +42,25 @@ final class AidlTestUtils { /* dabFrequencyTable= */ null, /* vendorInfo= */ null); } - static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) { + static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, + ProgramSelector.Identifier logicallyTunedTo, + ProgramSelector.Identifier physicallyTunedTo, int signalQuality) { return new RadioManager.ProgramInfo(selector, - selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null, + logicallyTunedTo, physicallyTunedTo, /* relatedContents= */ null, /* infoFlags= */ 0, signalQuality, new RadioMetadata.Builder().build(), new ArrayMap<>()); } - static RadioManager.ProgramInfo makeProgramInfo(int programType, - ProgramSelector.Identifier identifier, int signalQuality) { - ProgramSelector selector = makeProgramSelector(programType, identifier); - return makeProgramInfo(selector, signalQuality); + static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) { + return makeProgramInfo(selector, selector.getPrimaryId(), selector.getPrimaryId(), + signalQuality); + } + + static ProgramIdentifier makeHalIdentifier(@IdentifierType int type, long value) { + ProgramIdentifier halDabId = new ProgramIdentifier(); + halDabId.type = type; + halDabId.value = value; + return halDabId; } static ProgramSelector makeFmSelector(long freq) { @@ -67,44 +76,48 @@ final class AidlTestUtils { } static android.hardware.broadcastradio.ProgramSelector makeHalFmSelector(int freq) { - ProgramIdentifier halId = new ProgramIdentifier(); - halId.type = IdentifierType.AMFM_FREQUENCY_KHZ; - halId.value = freq; + ProgramIdentifier halId = makeHalIdentifier(IdentifierType.AMFM_FREQUENCY_KHZ, freq); + return makeHalSelector(halId, /* secondaryIds= */ new ProgramIdentifier[0]); + } - android.hardware.broadcastradio.ProgramSelector halSelector = + static android.hardware.broadcastradio.ProgramSelector makeHalSelector( + ProgramIdentifier primaryId, ProgramIdentifier[] secondaryIds) { + android.hardware.broadcastradio.ProgramSelector hwSelector = new android.hardware.broadcastradio.ProgramSelector(); - halSelector.primaryId = halId; - halSelector.secondaryIds = new ProgramIdentifier[0]; - return halSelector; + hwSelector.primaryId = primaryId; + hwSelector.secondaryIds = secondaryIds; + return hwSelector; } - static ProgramInfo programInfoToHalProgramInfo(RadioManager.ProgramInfo info) { - // Note that because ConversionUtils does not by design provide functions for all - // conversions, this function only copies fields that are set by makeProgramInfo(). - ProgramInfo hwInfo = new ProgramInfo(); - hwInfo.selector = ConversionUtils.programSelectorToHalProgramSelector(info.getSelector()); - hwInfo.logicallyTunedTo = - ConversionUtils.identifierToHalProgramIdentifier(info.getLogicallyTunedTo()); - hwInfo.physicallyTunedTo = - ConversionUtils.identifierToHalProgramIdentifier(info.getPhysicallyTunedTo()); - hwInfo.signalQuality = info.getSignalStrength(); - hwInfo.relatedContent = new ProgramIdentifier[]{}; - hwInfo.metadata = new Metadata[]{}; - return hwInfo; + static ProgramInfo makeHalProgramInfo( + android.hardware.broadcastradio.ProgramSelector hwSel, int hwSignalQuality) { + return makeHalProgramInfo(hwSel, hwSel.primaryId, hwSel.primaryId, hwSignalQuality); } static ProgramInfo makeHalProgramInfo( - android.hardware.broadcastradio.ProgramSelector hwSel, int hwSignalQuality) { + android.hardware.broadcastradio.ProgramSelector hwSel, + ProgramIdentifier logicallyTunedTo, ProgramIdentifier physicallyTunedTo, + int hwSignalQuality) { ProgramInfo hwInfo = new ProgramInfo(); hwInfo.selector = hwSel; - hwInfo.logicallyTunedTo = hwSel.primaryId; - hwInfo.physicallyTunedTo = hwSel.primaryId; + hwInfo.logicallyTunedTo = logicallyTunedTo; + hwInfo.physicallyTunedTo = physicallyTunedTo; hwInfo.signalQuality = hwSignalQuality; hwInfo.relatedContent = new ProgramIdentifier[]{}; hwInfo.metadata = new Metadata[]{}; return hwInfo; } + static ProgramListChunk makeProgramListChunk(boolean purge, boolean complete, + ProgramInfo[] modified, ProgramIdentifier[] removed) { + ProgramListChunk halChunk = new ProgramListChunk(); + halChunk.purge = purge; + halChunk.complete = complete; + halChunk.modified = modified; + halChunk.removed = removed; + return halChunk; + } + static VendorKeyValue makeVendorKeyValue(String vendorKey, String vendorValue) { VendorKeyValue vendorKeyValue = new VendorKeyValue(); vendorKeyValue.key = vendorKey; diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java index f4040825e9a7..98103f6eddd3 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java @@ -173,6 +173,19 @@ public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTes } @Test + public void openSession_withoutAudio_fails() throws Exception { + createBroadcastRadioService(); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID, + /* legacyConfig= */ null, /* withAudio= */ false, mTunerCallbackMock, + TARGET_SDK_VERSION)); + + assertWithMessage("Exception for opening session without audio") + .that(thrown).hasMessageThat().contains("not supported"); + } + + @Test public void binderDied_forDeathRecipient() throws Exception { createBroadcastRadioService(); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java index a1cebb630aac..710c150c006c 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java @@ -20,9 +20,13 @@ import android.hardware.broadcastradio.AmFmBandRange; import android.hardware.broadcastradio.AmFmRegionConfig; import android.hardware.broadcastradio.DabTableEntry; import android.hardware.broadcastradio.IdentifierType; +import android.hardware.broadcastradio.ProgramIdentifier; +import android.hardware.broadcastradio.ProgramInfo; +import android.hardware.broadcastradio.ProgramListChunk; import android.hardware.broadcastradio.Properties; import android.hardware.broadcastradio.VendorKeyValue; import android.hardware.radio.Announcement; +import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.os.Build; @@ -33,19 +37,20 @@ import org.junit.Rule; import org.junit.Test; import java.util.Map; +import java.util.Set; public final class ConversionUtilsTest { - private static final int FM_LOWER_LIMIT = 87500; - private static final int FM_UPPER_LIMIT = 108000; + private static final int FM_LOWER_LIMIT = 87_500; + private static final int FM_UPPER_LIMIT = 108_000; private static final int FM_SPACING = 200; private static final int AM_LOWER_LIMIT = 540; - private static final int AM_UPPER_LIMIT = 1700; + private static final int AM_UPPER_LIMIT = 1_700; private static final int AM_SPACING = 10; private static final String DAB_ENTRY_LABEL_1 = "5A"; - private static final int DAB_ENTRY_FREQUENCY_1 = 174928; + private static final int DAB_ENTRY_FREQUENCY_1 = 174_928; private static final String DAB_ENTRY_LABEL_2 = "12D"; - private static final int DAB_ENTRY_FREQUENCY_2 = 229072; + private static final int DAB_ENTRY_FREQUENCY_2 = 229_072; private static final String VENDOR_INFO_KEY_1 = "vendorKey1"; private static final String VENDOR_INFO_VALUE_1 = "vendorValue1"; private static final String VENDOR_INFO_KEY_2 = "vendorKey2"; @@ -57,6 +62,50 @@ public final class ConversionUtilsTest { private static final String TEST_VERSION = "versionMock"; private static final String TEST_SERIAL = "serialMock"; + private static final int TEST_SIGNAL_QUALITY = 1; + private static final long TEST_DAB_DMB_SID_EXT_VALUE = 0xA000000111L; + private static final long TEST_DAB_ENSEMBLE_VALUE = 0x1001; + private static final long TEST_DAB_FREQUENCY_VALUE = 220_352; + private static final long TEST_FM_FREQUENCY_VALUE = 92_100; + private static final long TEST_VENDOR_ID_VALUE = 9_901; + + private static final ProgramSelector.Identifier TEST_DAB_SID_EXT_ID = + new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, TEST_DAB_DMB_SID_EXT_VALUE); + private static final ProgramSelector.Identifier TEST_DAB_ENSEMBLE_ID = + new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, TEST_DAB_ENSEMBLE_VALUE); + private static final ProgramSelector.Identifier TEST_DAB_FREQUENCY_ID = + new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, TEST_DAB_FREQUENCY_VALUE); + private static final ProgramSelector.Identifier TEST_FM_FREQUENCY_ID = + new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, TEST_FM_FREQUENCY_VALUE); + private static final ProgramSelector.Identifier TEST_VENDOR_ID = + new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, TEST_VENDOR_ID_VALUE); + + private static final ProgramIdentifier TEST_HAL_DAB_SID_EXT_ID = + AidlTestUtils.makeHalIdentifier(IdentifierType.DAB_SID_EXT, TEST_DAB_DMB_SID_EXT_VALUE); + private static final ProgramIdentifier TEST_HAL_DAB_ENSEMBLE_ID = + AidlTestUtils.makeHalIdentifier(IdentifierType.DAB_ENSEMBLE, TEST_DAB_ENSEMBLE_VALUE); + private static final ProgramIdentifier TEST_HAL_DAB_FREQUENCY_ID = + AidlTestUtils.makeHalIdentifier(IdentifierType.DAB_FREQUENCY_KHZ, + TEST_DAB_FREQUENCY_VALUE); + private static final ProgramIdentifier TEST_HAL_FM_FREQUENCY_ID = + AidlTestUtils.makeHalIdentifier(IdentifierType.AMFM_FREQUENCY_KHZ, + TEST_FM_FREQUENCY_VALUE); + private static final ProgramIdentifier TEST_HAL_VENDOR_ID = + AidlTestUtils.makeHalIdentifier(IdentifierType.VENDOR_START, + TEST_VENDOR_ID_VALUE); + + private static final ProgramSelector TEST_DAB_SELECTOR = new ProgramSelector( + ProgramSelector.PROGRAM_TYPE_DAB, TEST_DAB_SID_EXT_ID, + new ProgramSelector.Identifier[]{TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID}, + /* vendorIds= */ null); + private static final ProgramSelector TEST_FM_SELECTOR = + AidlTestUtils.makeFmSelector(TEST_FM_FREQUENCY_VALUE); + private static final int TEST_ENABLED_TYPE = Announcement.TYPE_EMERGENCY; private static final int TEST_ANNOUNCEMENT_FREQUENCY = FM_LOWER_LIMIT + FM_SPACING; @@ -159,6 +208,246 @@ public final class ConversionUtilsTest { } @Test + public void identifierToHalProgramIdentifier_withDabId() { + ProgramIdentifier halDabId = + ConversionUtils.identifierToHalProgramIdentifier(TEST_DAB_SID_EXT_ID); + + expect.withMessage("Converted HAL DAB identifier").that(halDabId) + .isEqualTo(TEST_HAL_DAB_SID_EXT_ID); + } + + @Test + public void identifierFromHalProgramIdentifier_withDabId() { + ProgramSelector.Identifier dabId = + ConversionUtils.identifierFromHalProgramIdentifier(TEST_HAL_DAB_SID_EXT_ID); + + expect.withMessage("Converted DAB identifier").that(dabId).isEqualTo(TEST_DAB_SID_EXT_ID); + } + + @Test + public void programSelectorToHalProgramSelector_withValidSelector() { + android.hardware.broadcastradio.ProgramSelector halDabSelector = + ConversionUtils.programSelectorToHalProgramSelector(TEST_DAB_SELECTOR); + + expect.withMessage("Primary identifier of converted HAL DAB selector") + .that(halDabSelector.primaryId).isEqualTo(TEST_HAL_DAB_SID_EXT_ID); + expect.withMessage("Secondary identifiers of converted HAL DAB selector") + .that(halDabSelector.secondaryIds).asList() + .containsExactly(TEST_HAL_DAB_FREQUENCY_ID, TEST_HAL_DAB_ENSEMBLE_ID); + } + + @Test + public void programSelectorToHalProgramSelector_withInvalidDabSelector_returnsNull() { + ProgramSelector invalidDbSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, + TEST_DAB_SID_EXT_ID, + new ProgramSelector.Identifier[0], + new long[0]); + + android.hardware.broadcastradio.ProgramSelector invalidHalDabSelector = + ConversionUtils.programSelectorToHalProgramSelector(invalidDbSelector); + + expect.withMessage("Invalid HAL DAB selector without required secondary ids") + .that(invalidHalDabSelector).isNull(); + } + + @Test + public void programSelectorFromHalProgramSelector_withValidSelector() { + android.hardware.broadcastradio.ProgramSelector halDabSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{ + TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID}); + + ProgramSelector dabSelector = + ConversionUtils.programSelectorFromHalProgramSelector(halDabSelector); + + expect.withMessage("Primary identifier of converted DAB selector") + .that(dabSelector.getPrimaryId()).isEqualTo(TEST_DAB_SID_EXT_ID); + expect.withMessage("Secondary identifiers of converted DAB selector") + .that(dabSelector.getSecondaryIds()).asList() + .containsExactly(TEST_DAB_FREQUENCY_ID, TEST_DAB_ENSEMBLE_ID); + } + + @Test + public void programSelectorFromHalProgramSelector_withInvalidSelector_returnsNull() { + android.hardware.broadcastradio.ProgramSelector invalidHalDabSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{}); + + ProgramSelector invalidDabSelector = + ConversionUtils.programSelectorFromHalProgramSelector(invalidHalDabSelector); + + expect.withMessage("Invalid DAB selector without required secondary ids") + .that(invalidDabSelector).isNull(); + } + + @Test + public void programInfoFromHalProgramInfo_withValidProgramInfo() { + android.hardware.broadcastradio.ProgramSelector halDabSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{ + TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID}); + ProgramInfo halProgramInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector, + TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY); + + RadioManager.ProgramInfo programInfo = + ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo); + + expect.withMessage("Primary id of selector of converted program info") + .that(programInfo.getSelector().getPrimaryId()).isEqualTo(TEST_DAB_SID_EXT_ID); + expect.withMessage("Secondary id of selector of converted program info") + .that(programInfo.getSelector().getSecondaryIds()).asList() + .containsExactly(TEST_DAB_ENSEMBLE_ID, TEST_DAB_FREQUENCY_ID); + expect.withMessage("Logically tuned identifier of converted program info") + .that(programInfo.getLogicallyTunedTo()).isEqualTo(TEST_DAB_SID_EXT_ID); + expect.withMessage("Physically tuned identifier of converted program info") + .that(programInfo.getPhysicallyTunedTo()).isEqualTo(TEST_DAB_FREQUENCY_ID); + expect.withMessage("Signal quality of converted program info") + .that(programInfo.getSignalStrength()).isEqualTo(TEST_SIGNAL_QUALITY); + } + + @Test + public void programInfoFromHalProgramInfo_withInvalidDabProgramInfo() { + android.hardware.broadcastradio.ProgramSelector invalidHalDabSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, + new ProgramIdentifier[]{TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID}); + ProgramInfo halProgramInfo = AidlTestUtils.makeHalProgramInfo(invalidHalDabSelector, + TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_ENSEMBLE_ID, TEST_SIGNAL_QUALITY); + + RadioManager.ProgramInfo programInfo = + ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo); + + expect.withMessage("Invalid DAB program info with incorrect type of physically tuned to id") + .that(programInfo).isNull(); + } + + @Test + public void chunkFromHalProgramListChunk_withValidChunk() { + boolean purge = false; + boolean complete = true; + android.hardware.broadcastradio.ProgramSelector halDabSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{ + TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID}); + ProgramInfo halDabInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector, + TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY); + RadioManager.ProgramInfo dabInfo = + ConversionUtils.programInfoFromHalProgramInfo(halDabInfo); + ProgramListChunk halChunk = AidlTestUtils.makeProgramListChunk(purge, complete, + new ProgramInfo[]{halDabInfo}, + new ProgramIdentifier[]{TEST_HAL_VENDOR_ID, TEST_HAL_FM_FREQUENCY_ID}); + + ProgramList.Chunk chunk = ConversionUtils.chunkFromHalProgramListChunk(halChunk); + + expect.withMessage("Purged state of the converted valid program list chunk") + .that(chunk.isPurge()).isEqualTo(purge); + expect.withMessage("Completion state of the converted valid program list chunk") + .that(chunk.isComplete()).isEqualTo(complete); + expect.withMessage("Modified program info in the converted valid program list chunk") + .that(chunk.getModified()).containsExactly(dabInfo); + expect.withMessage("Removed program ides in the converted valid program list chunk") + .that(chunk.getRemoved()).containsExactly(TEST_VENDOR_ID, TEST_FM_FREQUENCY_ID); + } + + @Test + public void chunkFromHalProgramListChunk_withInvalidModifiedProgramInfo() { + boolean purge = true; + boolean complete = false; + android.hardware.broadcastradio.ProgramSelector halDabSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{ + TEST_HAL_DAB_ENSEMBLE_ID, TEST_HAL_DAB_FREQUENCY_ID}); + ProgramInfo halDabInfo = AidlTestUtils.makeHalProgramInfo(halDabSelector, + TEST_HAL_DAB_SID_EXT_ID, TEST_HAL_DAB_ENSEMBLE_ID, TEST_SIGNAL_QUALITY); + ProgramListChunk halChunk = AidlTestUtils.makeProgramListChunk(purge, complete, + new ProgramInfo[]{halDabInfo}, new ProgramIdentifier[]{TEST_HAL_FM_FREQUENCY_ID}); + + ProgramList.Chunk chunk = ConversionUtils.chunkFromHalProgramListChunk(halChunk); + + expect.withMessage("Purged state of the converted invalid program list chunk") + .that(chunk.isPurge()).isEqualTo(purge); + expect.withMessage("Completion state of the converted invalid program list chunk") + .that(chunk.isComplete()).isEqualTo(complete); + expect.withMessage("Modified program info in the converted invalid program list chunk") + .that(chunk.getModified()).isEmpty(); + expect.withMessage("Removed program ids in the converted invalid program list chunk") + .that(chunk.getRemoved()).containsExactly(TEST_FM_FREQUENCY_ID); + } + + @Test + public void programSelectorMeetsSdkVersionRequirement_withLowerVersionId_returnsFalse() { + expect.withMessage("Selector %s without required SDK version", TEST_DAB_SELECTOR) + .that(ConversionUtils.programSelectorMeetsSdkVersionRequirement(TEST_DAB_SELECTOR, + Build.VERSION_CODES.TIRAMISU)).isFalse(); + } + + @Test + public void programSelectorMeetsSdkVersionRequirement_withRequiredVersionId_returnsTrue() { + expect.withMessage("Selector %s with required SDK version", TEST_FM_SELECTOR) + .that(ConversionUtils.programSelectorMeetsSdkVersionRequirement(TEST_FM_SELECTOR, + Build.VERSION_CODES.TIRAMISU)).isTrue(); + } + + @Test + public void programInfoMeetsSdkVersionRequirement_withLowerVersionId_returnsFalse() { + RadioManager.ProgramInfo dabProgramInfo = AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR, + TEST_DAB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY); + + expect.withMessage("Program info %s without required SDK version", dabProgramInfo) + .that(ConversionUtils.programInfoMeetsSdkVersionRequirement(dabProgramInfo, + Build.VERSION_CODES.TIRAMISU)).isFalse(); + } + + @Test + public void programInfoMeetsSdkVersionRequirement_withRequiredVersionId_returnsTrue() { + RadioManager.ProgramInfo fmProgramInfo = AidlTestUtils.makeProgramInfo(TEST_FM_SELECTOR, + TEST_SIGNAL_QUALITY); + + expect.withMessage("Program info %s with required SDK version", fmProgramInfo) + .that(ConversionUtils.programInfoMeetsSdkVersionRequirement(fmProgramInfo, + Build.VERSION_CODES.TIRAMISU)).isTrue(); + } + + @Test + public void convertChunkToTargetSdkVersion_withLowerSdkVersion() { + RadioManager.ProgramInfo dabProgramInfo = AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR, + TEST_DAB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY); + RadioManager.ProgramInfo fmProgramInfo = AidlTestUtils.makeProgramInfo(TEST_FM_SELECTOR, + TEST_SIGNAL_QUALITY); + ProgramList.Chunk chunk = new ProgramList.Chunk(/* purge= */ true, + /* complete= */ true, Set.of(dabProgramInfo, fmProgramInfo), + Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID)); + + ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk, + Build.VERSION_CODES.TIRAMISU); + + expect.withMessage( + "Purged state of the converted program list chunk with lower SDK version") + .that(convertedChunk.isPurge()).isEqualTo(chunk.isPurge()); + expect.withMessage( + "Completion state of the converted program list chunk with lower SDK version") + .that(convertedChunk.isComplete()).isEqualTo(chunk.isComplete()); + expect.withMessage( + "Modified program info in the converted program list chunk with lower SDK version") + .that(convertedChunk.getModified()).containsExactly(fmProgramInfo); + expect.withMessage( + "Removed program ids in the converted program list chunk with lower SDK version") + .that(convertedChunk.getRemoved()) + .containsExactly(TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID); + } + + @Test + public void convertChunkToTargetSdkVersion_withRequiredSdkVersion() { + RadioManager.ProgramInfo dabProgramInfo = AidlTestUtils.makeProgramInfo(TEST_DAB_SELECTOR, + TEST_DAB_SID_EXT_ID, TEST_DAB_FREQUENCY_ID, TEST_SIGNAL_QUALITY); + RadioManager.ProgramInfo fmProgramInfo = AidlTestUtils.makeProgramInfo(TEST_FM_SELECTOR, + TEST_SIGNAL_QUALITY); + ProgramList.Chunk chunk = new ProgramList.Chunk(/* purge= */ true, + /* complete= */ true, Set.of(dabProgramInfo, fmProgramInfo), + Set.of(TEST_DAB_SID_EXT_ID, TEST_DAB_ENSEMBLE_ID, TEST_VENDOR_ID)); + + ProgramList.Chunk convertedChunk = ConversionUtils.convertChunkToTargetSdkVersion(chunk, + Build.VERSION_CODES.CUR_DEVELOPMENT); + + expect.withMessage("Converted program list chunk with required SDK version") + .that(convertedChunk).isEqualTo(chunk); + } + + @Test public void announcementFromHalAnnouncement_typesMatch() { expect.withMessage("Announcement type") .that(ANNOUNCEMENT.getType()).isEqualTo(TEST_ENABLED_TYPE); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java index c5c6349df07e..d7723acf6f05 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java @@ -26,7 +26,9 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -43,6 +45,8 @@ import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioTuner; import android.os.Build; +import android.os.ParcelableException; +import android.os.RemoteException; import android.os.ServiceSpecificException; import android.util.ArrayMap; import android.util.ArraySet; @@ -57,7 +61,6 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.verification.VerificationWithTimeout; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -71,10 +74,10 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { timeout(/* millis= */ 200); private static final int SIGNAL_QUALITY = 1; private static final long AM_FM_FREQUENCY_SPACING = 500; - private static final long[] AM_FM_FREQUENCY_LIST = {97500, 98100, 99100}; + private static final long[] AM_FM_FREQUENCY_LIST = {97_500, 98_100, 99_100}; private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR = new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM, - /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 100, + /* lowerLimit= */ 87_500, /* upperLimit= */ 108_000, /* spacing= */ 100, /* stereo= */ false, /* rds= */ false, /* ta= */ false, /* af= */ false, /* ea= */ false); private static final RadioManager.BandConfig FM_BAND_CONFIG = @@ -200,6 +203,17 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void setConfiguration_forNonCurrentUser_doesNotInvokesCallback() throws Exception { + openAidlClients(/* numClients= */ 1); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].setConfiguration(FM_BAND_CONFIG); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)) + .onConfigurationChanged(FM_BAND_CONFIG); + } + + @Test public void getConfiguration() throws Exception { openAidlClients(/* numClients= */ 1); mTunerSessions[0].setConfiguration(FM_BAND_CONFIG); @@ -330,10 +344,17 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { @Test public void tune_withUnsupportedSelector_throwsException() throws Exception { + ProgramSelector.Identifier dabPrimaryId = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT, + /* value= */ 0xA000000111L); + ProgramSelector.Identifier[] dabSecondaryIds = new ProgramSelector.Identifier[]{ + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, + /* value= */ 1337), + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, + /* value= */ 225648)}; + ProgramSelector unsupportedSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, + dabPrimaryId, dabSecondaryIds, /* vendorIds= */ null); openAidlClients(/* numClients= */ 1); - ProgramSelector unsupportedSelector = AidlTestUtils.makeProgramSelector( - ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier( - ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300)); UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class, () -> mTunerSessions[0].tune(unsupportedSelector)); @@ -343,7 +364,22 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test - public void tune_forCurrentUser_doesNotTune() throws Exception { + public void tune_withInvalidSelector_throwsIllegalArgumentException() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector.Identifier invalidDabId = new ProgramSelector.Identifier( + ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, /* value= */ 0x1001); + ProgramSelector invalidSel = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, + invalidDabId, new ProgramSelector.Identifier[0], new long[0]); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mTunerSessions[0].tune(invalidSel)); + + assertWithMessage("Exception for tuning on DAB selector without DAB_SID_EXT primary id") + .that(thrown).hasMessageThat().contains("tune: INVALID_ARGUMENTS"); + } + + @Test + public void tune_forNonCurrentUser_doesNotTune() throws Exception { openAidlClients(/* numClients= */ 1); doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); @@ -357,6 +393,21 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void tune_withHalHasUnknownError_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector sel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); + doThrow(new ServiceSpecificException(Result.UNKNOWN_ERROR)) + .when(mBroadcastRadioMock).tune(any()); + + ParcelableException thrown = assertThrows(ParcelableException.class, () -> { + mTunerSessions[0].tune(sel); + }); + + assertWithMessage("Exception for tuning when HAL has unknown error") + .that(thrown).hasMessageThat().contains("UNKNOWN_ERROR"); + } + + @Test public void step_withDirectionUp() throws Exception { long initFreq = AM_FM_FREQUENCY_LIST[1]; ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq); @@ -391,6 +442,35 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void step_forNonCurrentUser_doesNotStep() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[1]; + ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo( + ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)) + .onCurrentProgramInfoChanged(any()); + } + + @Test + public void step_withHalInInvalidState_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + doThrow(new ServiceSpecificException(Result.INVALID_STATE)) + .when(mBroadcastRadioMock).step(anyBoolean()); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false); + }); + + assertWithMessage("Exception for stepping when HAL is in invalid state") + .that(thrown).hasMessageThat().contains("INVALID_STATE"); + } + + @Test public void seek_withDirectionUp() throws Exception { long initFreq = AM_FM_FREQUENCY_LIST[2]; ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq); @@ -432,11 +512,44 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY); mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) .onCurrentProgramInfoChanged(seekUpInfo); } @Test + public void seek_forNonCurrentUser_doesNotSeek() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[2]; + ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq); + RadioManager.ProgramInfo seekUpInfo = AidlTestUtils.makeProgramInfo( + AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)), + SIGNAL_QUALITY); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo( + ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)) + .onCurrentProgramInfoChanged(seekUpInfo); + } + + @Test + public void seek_withHalHasInternalError_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + doThrow(new ServiceSpecificException(Result.INTERNAL_ERROR)) + .when(mBroadcastRadioMock).seek(anyBoolean(), anyBoolean()); + + ParcelableException thrown = assertThrows(ParcelableException.class, () -> { + mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); + }); + + assertWithMessage("Exception for seeking when HAL has internal error") + .that(thrown).hasMessageThat().contains("INTERNAL_ERROR"); + } + + @Test public void cancel() throws Exception { openAidlClients(/* numClients= */ 1); ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); @@ -448,6 +561,32 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void cancel_forNonCurrentUser_doesNotCancel() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); + mTunerSessions[0].tune(initialSel); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].cancel(); + + verify(mBroadcastRadioMock, never()).cancel(); + } + + @Test + public void cancel_whenHalThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + String exceptionMessage = "HAL service died."; + doThrow(new RemoteException(exceptionMessage)).when(mBroadcastRadioMock).cancel(); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].cancel(); + }); + + assertWithMessage("Exception for canceling when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void getImage_withInvalidId_throwsIllegalArgumentException() throws Exception { openAidlClients(/* numClients= */ 1); int imageId = IBroadcastRadio.INVALID_IMAGE; @@ -471,6 +610,21 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void getImage_whenHalThrowsException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + String exceptionMessage = "HAL service died."; + when(mBroadcastRadioMock.getImage(anyInt())) + .thenThrow(new RemoteException(exceptionMessage)); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].getImage(/* id= */ 1); + }); + + assertWithMessage("Exception for getting image when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void startBackgroundScan() throws Exception { openAidlClients(/* numClients= */ 1); @@ -480,6 +634,16 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void startBackgroundScan_forNonCurrentUser_doesNotInvokesCallback() throws Exception { + openAidlClients(/* numClients= */ 1); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].startBackgroundScan(); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)).onBackgroundScanComplete(); + } + + @Test public void stopProgramListUpdates() throws Exception { openAidlClients(/* numClients= */ 1); ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), @@ -492,6 +656,19 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void stopProgramListUpdates_forNonCurrentUser_doesNotStopUpdates() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + mTunerSessions[0].startProgramListUpdates(aidlFilter); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].stopProgramListUpdates(); + + verify(mBroadcastRadioMock, never()).stopProgramListUpdates(); + } + + @Test public void isConfigFlagSupported_withUnsupportedFlag_returnsFalse() throws Exception { openAidlClients(/* numClients= */ 1); int flag = UNSUPPORTED_CONFIG_FLAG; @@ -547,6 +724,17 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void setConfigFlag_forNonCurrentUser_doesNotSetConfigFlag() throws Exception { + openAidlClients(/* numClients= */ 1); + int flag = UNSUPPORTED_CONFIG_FLAG + 1; + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].setConfigFlag(flag, /* value= */ true); + + verify(mBroadcastRadioMock, never()).setConfigFlag(flag, /* value= */ true); + } + + @Test public void isConfigFlagSet_withUnsupportedFlag_throwsRuntimeException() throws Exception { openAidlClients(/* numClients= */ 1); @@ -556,7 +744,7 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { mTunerSessions[0].isConfigFlagSet(flag); }); - assertWithMessage("Exception for check if unsupported flag %s is set", flag) + assertWithMessage("Exception for checking if unsupported flag %s is set", flag) .that(thrown).hasMessageThat().contains("isConfigFlagSet: NOT_SUPPORTED"); } @@ -574,6 +762,20 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void isConfigFlagSet_whenHalThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + int flag = UNSUPPORTED_CONFIG_FLAG + 1; + doThrow(new RemoteException()).when(mBroadcastRadioMock).isConfigFlagSet(anyInt()); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].isConfigFlagSet(flag); + }); + + assertWithMessage("Exception for checking config flag when HAL throws remote exception") + .that(thrown).hasMessageThat().contains("Failed to check flag"); + } + + @Test public void setParameters_withMockParameters() throws Exception { openAidlClients(/* numClients= */ 1); Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1", @@ -586,16 +788,58 @@ public final class TunerSessionTest extends ExtendedRadioMockitoTestCase { } @Test + public void setParameters_forNonCurrentUser_doesNotSetParameters() throws Exception { + openAidlClients(/* numClients= */ 1); + Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1", + "mockParam2", "mockValue2"); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].setParameters(parametersSet); + + verify(mBroadcastRadioMock, never()).setParameters(any()); + } + + @Test + public void setParameters_whenHalThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1", + "mockParam2", "mockValue2"); + String exceptionMessage = "HAL service died."; + when(mBroadcastRadioMock.setParameters(any())) + .thenThrow(new RemoteException(exceptionMessage)); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].setParameters(parametersSet); + }); + + assertWithMessage("Exception for setting parameters when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void getParameters_withMockKeys() throws Exception { openAidlClients(/* numClients= */ 1); - List<String> parameterKeys = new ArrayList<>(2); - parameterKeys.add("mockKey1"); - parameterKeys.add("mockKey2"); + List<String> parameterKeys = List.of("mockKey1", "mockKey2"); mTunerSessions[0].getParameters(parameterKeys); - verify(mBroadcastRadioMock).getParameters( - parameterKeys.toArray(new String[0])); + verify(mBroadcastRadioMock).getParameters(parameterKeys.toArray(new String[0])); + } + + @Test + public void getParameters_whenServiceThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + List<String> parameterKeys = List.of("mockKey1", "mockKey2"); + String exceptionMessage = "HAL service died."; + when(mBroadcastRadioMock.getParameters(any())) + .thenThrow(new RemoteException(exceptionMessage)); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].getParameters(parameterKeys); + }); + + assertWithMessage("Exception for getting parameters when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); } @Test diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java index db16c0362e7c..6e54dcf3d451 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java @@ -324,10 +324,17 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { @Test public void tune_withUnsupportedSelector_throwsException() throws Exception { + ProgramSelector.Identifier dabPrimaryId = + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, + /* value= */ 0xA00111); + ProgramSelector.Identifier[] dabSecondaryIds = new ProgramSelector.Identifier[]{ + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, + /* value= */ 1337), + new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, + /* value= */ 225648)}; + ProgramSelector unsupportedSelector = new ProgramSelector(ProgramSelector.PROGRAM_TYPE_DAB, + dabPrimaryId, dabSecondaryIds, /* vendorIds= */ null); openAidlClients(/* numClients= */ 1); - ProgramSelector unsupportedSelector = TestUtils.makeProgramSelector( - ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, new ProgramSelector.Identifier( - ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, /* value= */ 300)); UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class, () -> mTunerSessions[0].tune(unsupportedSelector)); diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index 401193357f6b..abbbb2f516b2 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -16,6 +16,7 @@ package android.app.activity; +import static android.companion.virtual.VirtualDeviceManager.DEVICE_ID_INVALID; import static android.content.Intent.ACTION_EDIT; import static android.content.Intent.ACTION_VIEW; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; @@ -204,7 +205,8 @@ public class ActivityThreadTest { try { // Send process level config change. ClientTransaction transaction = newTransaction(activityThread, null); - transaction.addCallback(ConfigurationChangeItem.obtain(new Configuration(newConfig))); + transaction.addCallback(ConfigurationChangeItem.obtain( + new Configuration(newConfig), DEVICE_ID_INVALID)); appThread.scheduleTransaction(transaction); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); @@ -413,12 +415,14 @@ public class ActivityThreadTest { activity.mTestLatch = new CountDownLatch(1); ClientTransaction transaction = newTransaction(activityThread, null); - transaction.addCallback(ConfigurationChangeItem.obtain(processConfigLandscape)); + transaction.addCallback(ConfigurationChangeItem.obtain( + processConfigLandscape, DEVICE_ID_INVALID)); appThread.scheduleTransaction(transaction); transaction = newTransaction(activityThread, activity.getActivityToken()); transaction.addCallback(ActivityConfigurationChangeItem.obtain(activityConfigLandscape)); - transaction.addCallback(ConfigurationChangeItem.obtain(processConfigPortrait)); + transaction.addCallback(ConfigurationChangeItem.obtain( + processConfigPortrait, DEVICE_ID_INVALID)); transaction.addCallback(ActivityConfigurationChangeItem.obtain(activityConfigPortrait)); appThread.scheduleTransaction(transaction); @@ -530,7 +534,7 @@ public class ActivityThreadTest { ? ORIENTATION_LANDSCAPE : ORIENTATION_PORTRAIT; activityThread.updatePendingConfiguration(newAppConfig); - activityThread.handleConfigurationChanged(newAppConfig); + activityThread.handleConfigurationChanged(newAppConfig, DEVICE_ID_INVALID); try { assertEquals("Virtual display orientation must not change when process" @@ -548,7 +552,7 @@ public class ActivityThreadTest { private static void restoreConfig(ActivityThread thread, Configuration originalConfig) { thread.getConfiguration().seq = originalConfig.seq - 1; ResourcesManager.getInstance().getConfiguration().seq = originalConfig.seq - 1; - thread.handleConfigurationChanged(originalConfig); + thread.handleConfigurationChanged(originalConfig, DEVICE_ID_INVALID); } @Test @@ -626,7 +630,7 @@ public class ActivityThreadTest { newAppConfig.seq++; final ActivityThread activityThread = activity.getActivityThread(); - activityThread.handleConfigurationChanged(newAppConfig); + activityThread.handleConfigurationChanged(newAppConfig, DEVICE_ID_INVALID); // Verify that application config update was applied, but didn't change activity config. assertEquals("Activity config must not change if the process config changes", @@ -771,7 +775,8 @@ public class ActivityThreadTest { final ClientTransactionItem callbackItem = ActivityRelaunchItem.obtain(null, null, 0, new MergedConfiguration(), false /* preserveWindow */); final ResumeActivityItem resumeStateRequest = - ResumeActivityItem.obtain(true /* isForward */); + ResumeActivityItem.obtain(true /* isForward */, + false /* shouldSendCompatFakeFocus*/); final ClientTransaction transaction = newTransaction(activity); transaction.addCallback(callbackItem); @@ -782,7 +787,8 @@ public class ActivityThreadTest { private static ClientTransaction newResumeTransaction(Activity activity) { final ResumeActivityItem resumeStateRequest = - ResumeActivityItem.obtain(true /* isForward */); + ResumeActivityItem.obtain(true /* isForward */, + false /* shouldSendCompatFakeFocus */); final ClientTransaction transaction = newTransaction(activity); transaction.setLifecycleStateRequest(resumeStateRequest); diff --git a/core/tests/coretests/src/android/app/backup/BackupManagerTest.java b/core/tests/coretests/src/android/app/backup/BackupManagerTest.java index cbf167c49f95..27ee82e99376 100644 --- a/core/tests/coretests/src/android/app/backup/BackupManagerTest.java +++ b/core/tests/coretests/src/android/app/backup/BackupManagerTest.java @@ -83,6 +83,13 @@ public class BackupManagerTest { () -> mBackupManager.getBackupRestoreEventLogger(agent)); } + @Test + public void testGetDelayedRestoreLogger_returnsRestoreLogger() { + BackupRestoreEventLogger logger = mBackupManager.getDelayedRestoreLogger(); + + assertThat(logger.getOperationType()).isEqualTo(OperationType.RESTORE); + } + private static BackupAgent getTestAgent() { return new BackupAgent() { @Override diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java index 993ecf66c25b..ca6735bc6c32 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java @@ -98,15 +98,15 @@ public class ObjectPoolTests { @Test public void testRecycleConfigurationChangeItem() { - ConfigurationChangeItem emptyItem = ConfigurationChangeItem.obtain(null); - ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config()); + ConfigurationChangeItem emptyItem = ConfigurationChangeItem.obtain(null, 0); + ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config(), 1); assertNotSame(item, emptyItem); assertFalse(item.equals(emptyItem)); item.recycle(); assertEquals(item, emptyItem); - ConfigurationChangeItem item2 = ConfigurationChangeItem.obtain(config()); + ConfigurationChangeItem item2 = ConfigurationChangeItem.obtain(config(), 1); assertSame(item, item2); assertFalse(item2.equals(emptyItem)); } @@ -147,6 +147,7 @@ public class ObjectPoolTests { persistableBundle.putInt("k", 4); IBinder assistToken = new Binder(); IBinder shareableActivityToken = new Binder(); + int deviceId = 3; Supplier<LaunchActivityItem> itemSupplier = () -> new LaunchActivityItemBuilder() .setIntent(intent).setIdent(ident).setInfo(activityInfo).setCurConfig(config()) @@ -155,7 +156,7 @@ public class ObjectPoolTests { .setPendingResults(resultInfoList()).setPendingNewIntents(referrerIntentList()) .setIsForward(true).setAssistToken(assistToken) .setShareableActivityToken(shareableActivityToken) - .setTaskFragmentToken(new Binder()).build(); + .setTaskFragmentToken(new Binder()).setDeviceId(deviceId).build(); LaunchActivityItem emptyItem = new LaunchActivityItemBuilder().build(); LaunchActivityItem item = itemSupplier.get(); @@ -236,15 +237,15 @@ public class ObjectPoolTests { @Test public void testRecycleResumeActivityItem() { - ResumeActivityItem emptyItem = ResumeActivityItem.obtain(false); - ResumeActivityItem item = ResumeActivityItem.obtain(3, true); + ResumeActivityItem emptyItem = ResumeActivityItem.obtain(false, false); + ResumeActivityItem item = ResumeActivityItem.obtain(3, true, false); assertNotSame(item, emptyItem); assertFalse(item.equals(emptyItem)); item.recycle(); assertEquals(item, emptyItem); - ResumeActivityItem item2 = ResumeActivityItem.obtain(2, true); + ResumeActivityItem item2 = ResumeActivityItem.obtain(2, true, false); assertSame(item, item2); assertFalse(item2.equals(emptyItem)); } diff --git a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java index 2cd890cd9d18..0ed6a29cdd9d 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TestUtils.java +++ b/core/tests/coretests/src/android/app/servertransaction/TestUtils.java @@ -95,6 +95,7 @@ class TestUtils { private ActivityInfo mInfo; private Configuration mCurConfig; private Configuration mOverrideConfig; + private int mDeviceId; private String mReferrer; private IVoiceInteractor mVoiceInteractor; private int mProcState; @@ -135,6 +136,11 @@ class TestUtils { return this; } + LaunchActivityItemBuilder setDeviceId(int deviceId) { + mDeviceId = deviceId; + return this; + } + LaunchActivityItemBuilder setReferrer(String referrer) { mReferrer = referrer; return this; @@ -207,7 +213,7 @@ class TestUtils { LaunchActivityItem build() { return LaunchActivityItem.obtain(mIntent, mIdent, mInfo, - mCurConfig, mOverrideConfig, mReferrer, mVoiceInteractor, + mCurConfig, mOverrideConfig, mDeviceId, mReferrer, mVoiceInteractor, mProcState, mState, mPersistentState, mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo, mAssistToken, null /* activityClientController */, mShareableActivityToken, diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java index a0ed0266d24b..48a824915015 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java @@ -25,53 +25,26 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.app.ActivityOptions; -import android.app.ContentProviderHolder; -import android.app.IApplicationThread; -import android.app.IInstrumentationWatcher; -import android.app.IUiAutomationConnection; -import android.app.ProfilerInfo; import android.app.servertransaction.TestUtils.LaunchActivityItemBuilder; -import android.content.AutofillOptions; -import android.content.ComponentName; -import android.content.ContentCaptureOptions; -import android.content.IIntentReceiver; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.pm.ParceledListSlice; -import android.content.pm.ProviderInfo; -import android.content.pm.ProviderInfoList; -import android.content.pm.ServiceInfo; -import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.Binder; import android.os.Bundle; -import android.os.Debug; -import android.os.IBinder; import android.os.Parcel; -import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.PersistableBundle; -import android.os.RemoteCallback; -import android.os.RemoteException; -import android.os.SharedMemory; import android.platform.test.annotations.Presubmit; -import android.view.autofill.AutofillId; -import android.view.translation.TranslationSpec; -import android.view.translation.UiTranslationSpec; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.internal.app.IVoiceInteractor; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; -import java.util.List; -import java.util.Map; /** * Test parcelling and unparcelling of transactions and transaction items. @@ -97,7 +70,7 @@ public class TransactionParcelTests { @Test public void testConfigurationChange() { // Write to parcel - ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config()); + ConfigurationChangeItem item = ConfigurationChangeItem.obtain(config(), 1 /* deviceId */); writeAndPrepareForReading(item); // Read from parcel and assert @@ -249,7 +222,7 @@ public class TransactionParcelTests { public void testResume() { // Write to parcel ResumeActivityItem item = ResumeActivityItem.obtain(27 /* procState */, - true /* isForward */); + true /* isForward */, false /* shouldSendCompatFakeFocus */); writeAndPrepareForReading(item); // Read from parcel and assert diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 1d0644897fc2..2afbb476bc16 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -297,6 +297,20 @@ public class ViewRootImplTest { } } + @Test + public void whenDispatchFakeFocus_focusDoesNotPersist() throws Exception { + View view = new View(sContext); + attachViewToWindow(view); + view.clearFocus(); + + assertThat(view.hasWindowFocus()).isFalse(); + + mViewRootImpl = view.getViewRootImpl(); + + mViewRootImpl.dispatchCompatFakeFocus(); + assertThat(view.hasWindowFocus()).isFalse(); + } + /** * When window doesn't have focus, keys should be dropped. */ diff --git a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java index 88b2de7137fd..539eb6253f4d 100644 --- a/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java +++ b/core/tests/mockingcoretests/src/android/app/activity/ActivityThreadClientTest.java @@ -302,7 +302,7 @@ public class ActivityThreadClientTest { private void resumeActivity(ActivityClientRecord r) { mThread.handleResumeActivity(r, true /* finalStateRequest */, - true /* isForward */, "test"); + true /* isForward */, false /* shouldSendCompatFakeFocus */, "test"); } private void pauseActivity(ActivityClientRecord r) { @@ -347,7 +347,7 @@ public class ActivityThreadClientTest { doNothing().when(packageInfo).updateApplicationInfo(any(), any()); return new ActivityClientRecord(mock(IBinder.class), Intent.makeMainActivity(component), - 0 /* ident */, info, new Configuration(), null /* referrer */, + 0 /* ident */, info, new Configuration(), 0 /*deviceId */, null /* referrer */, null /* voiceInteractor */, null /* state */, null /* persistentState */, null /* pendingResults */, null /* pendingNewIntents */, null /* activityOptions */, true /* isForward */, null /* profilerInfo */, diff --git a/data/keyboards/Vendor_054c_Product_0df2.kl b/data/keyboards/Vendor_054c_Product_0df2.kl new file mode 100644 index 000000000000..a47b310328ab --- /dev/null +++ b/data/keyboards/Vendor_054c_Product_0df2.kl @@ -0,0 +1,73 @@ +# Copyright (C) 2022 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Sony Playstation(R) DualSense Edge Controller +# + +# Only use this key layout if we have HID_PLAYSTATION! +requires_kernel_config CONFIG_HID_PLAYSTATION + +# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html + +# Square +key 0x134 BUTTON_X +# Cross +key 0x130 BUTTON_A +# Circle +key 0x131 BUTTON_B +# Triangle +key 0x133 BUTTON_Y + +key 0x136 BUTTON_L1 +key 0x137 BUTTON_R1 +key 0x138 BUTTON_L2 +key 0x139 BUTTON_R2 + +# L2 axis +axis 0x02 LTRIGGER +# R2 axis +axis 0x05 RTRIGGER + +# Left Analog Stick +axis 0x00 X +axis 0x01 Y +# Right Analog Stick +axis 0x03 Z +axis 0x04 RZ + +# Left stick click +key 0x13d BUTTON_THUMBL +# Right stick click +key 0x13e BUTTON_THUMBR + +# Hat +axis 0x10 HAT_X +axis 0x11 HAT_Y + +# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt +# Share / "half-sun" +key 0x13a BUTTON_SELECT +# Options / three horizontal lines +key 0x13b BUTTON_START +# PS key +key 0x13c BUTTON_MODE + +# SENSORs +sensor 0x00 ACCELEROMETER X +sensor 0x01 ACCELEROMETER Y +sensor 0x02 ACCELEROMETER Z +sensor 0x03 GYROSCOPE X +sensor 0x04 GYROSCOPE Y +sensor 0x05 GYROSCOPE Z diff --git a/data/keyboards/Vendor_054c_Product_0df2_fallback.kl b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl new file mode 100644 index 000000000000..bfebb179a30f --- /dev/null +++ b/data/keyboards/Vendor_054c_Product_0df2_fallback.kl @@ -0,0 +1,75 @@ +# Copyright (C) 2022 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# +# Sony Playstation(R) DualSense Edge Controller +# + +# Use this if HID_PLAYSTATION is not available + +# Mapping according to https://developer.android.com/training/game-controllers/controller-input.html + +# Square +key 304 BUTTON_X +# Cross +key 305 BUTTON_A +# Circle +key 306 BUTTON_B +# Triangle +key 307 BUTTON_Y + +key 308 BUTTON_L1 +key 309 BUTTON_R1 +key 310 BUTTON_L2 +key 311 BUTTON_R2 + +# L2 axis +axis 0x03 LTRIGGER +# R2 axis +axis 0x04 RTRIGGER + +# Left Analog Stick +axis 0x00 X +axis 0x01 Y +# Right Analog Stick +axis 0x02 Z +axis 0x05 RZ + +# Left stick click +key 314 BUTTON_THUMBL +# Right stick click +key 315 BUTTON_THUMBR + +# Hat +axis 0x10 HAT_X +axis 0x11 HAT_Y + +# Mapping according to https://www.kernel.org/doc/Documentation/input/gamepad.txt +# Share / "half-sun" +key 312 BUTTON_SELECT +# Options / three horizontal lines +key 313 BUTTON_START +# PS key +key 316 BUTTON_MODE + +# Touchpad press +key 317 BUTTON_1 + +# SENSORs +sensor 0x00 ACCELEROMETER X +sensor 0x01 ACCELEROMETER Y +sensor 0x02 ACCELEROMETER Z +sensor 0x03 GYROSCOPE X +sensor 0x04 GYROSCOPE Y +sensor 0x05 GYROSCOPE Z diff --git a/graphics/java/android/graphics/BaseCanvas.java b/graphics/java/android/graphics/BaseCanvas.java index e62ac466ac43..00ffd09eb29e 100644 --- a/graphics/java/android/graphics/BaseCanvas.java +++ b/graphics/java/android/graphics/BaseCanvas.java @@ -668,12 +668,19 @@ public abstract class BaseCanvas { } /** - * @hide + * Draws a mesh object to the screen. + * + * @param mesh {@link Mesh} object that will be drawn to the screen + * @param blendMode {@link BlendMode} used to blend mesh primitives with the Paint color/shader + * @param paint {@link Paint} used to provide a color/shader/blend mode. */ - public void drawMesh(Mesh mesh, BlendMode blendMode, Paint paint) { + public void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) { if (!isHardwareAccelerated() && onHwFeatureInSwMode()) { throw new RuntimeException("software rendering doesn't support meshes"); } + if (blendMode == null) { + blendMode = BlendMode.MODULATE; + } nDrawMesh(this.mNativeCanvasWrapper, mesh.getNativeWrapperInstance(), blendMode.getXfermode().porterDuffMode, paint.getNativeInstance()); } diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java index eeff694eb3ac..2ec4524e1241 100644 --- a/graphics/java/android/graphics/BaseRecordingCanvas.java +++ b/graphics/java/android/graphics/BaseRecordingCanvas.java @@ -607,7 +607,10 @@ public class BaseRecordingCanvas extends Canvas { } @Override - public final void drawMesh(Mesh mesh, BlendMode blendMode, Paint paint) { + public final void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) { + if (blendMode == null) { + blendMode = BlendMode.MODULATE; + } nDrawMesh(mNativeCanvasWrapper, mesh.getNativeWrapperInstance(), blendMode.getXfermode().porterDuffMode, paint.getNativeInstance()); } diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 318cd32d19fe..3c654d6dec48 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -1810,10 +1810,15 @@ public final class Bitmap implements Parcelable { * {@code ColorSpace}.</p> * * @throws IllegalArgumentException If the specified color space is {@code null}, not - * {@link ColorSpace.Model#RGB RGB}, has a transfer function that is not an - * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}, or whose - * components min/max values reduce the numerical range compared to the - * previously assigned color space. + * {@link ColorSpace.Model#RGB RGB}, or whose components min/max values reduce + * the numerical range compared to the previously assigned color space. + * Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * <code>IllegalArgumentException</code> will also be thrown + * if the specified color space has a transfer function that is not an + * {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. Starting from + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the color spaces with non + * ICC parametric curve transfer function are allowed. + * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}. * * @throws IllegalArgumentException If the {@code Config} (returned by {@link #getConfig()}) * is {@link Config#ALPHA_8}. diff --git a/graphics/java/android/graphics/BitmapFactory.java b/graphics/java/android/graphics/BitmapFactory.java index ef1e7bfc6651..701e20c499da 100644 --- a/graphics/java/android/graphics/BitmapFactory.java +++ b/graphics/java/android/graphics/BitmapFactory.java @@ -161,11 +161,17 @@ public class BitmapFactory { * be thrown by the decode methods when setting a non-RGB color space * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p> * - * <p class="note">The specified color space's transfer function must be + * <p class="note"> + * Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * the specified color space's transfer function must be * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An * <code>IllegalArgumentException</code> will be thrown by the decode methods * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the - * specified color space returns null.</p> + * specified color space returns null. + * + * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * non ICC parametric curve transfer function is allowed. + * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}.</p> * * <p>After decode, the bitmap's color space is stored in * {@link #outColorSpace}.</p> @@ -458,7 +464,11 @@ public class BitmapFactory { throw new IllegalArgumentException("The destination color space must use the " + "RGB color model"); } - if (((ColorSpace.Rgb) opts.inPreferredColorSpace).getTransferParameters() == null) { + if (!opts.inPreferredColorSpace.equals(ColorSpace.get(ColorSpace.Named.BT2020_HLG)) + && !opts.inPreferredColorSpace.equals( + ColorSpace.get(ColorSpace.Named.BT2020_PQ)) + && ((ColorSpace.Rgb) opts.inPreferredColorSpace) + .getTransferParameters() == null) { throw new IllegalArgumentException("The destination color space must use an " + "ICC parametric transfer function"); } diff --git a/graphics/java/android/graphics/ColorSpace.java b/graphics/java/android/graphics/ColorSpace.java index 31df474eb10c..2427dec169d6 100644 --- a/graphics/java/android/graphics/ColorSpace.java +++ b/graphics/java/android/graphics/ColorSpace.java @@ -199,6 +199,8 @@ public abstract class ColorSpace { private static final float[] SRGB_PRIMARIES = { 0.640f, 0.330f, 0.300f, 0.600f, 0.150f, 0.060f }; private static final float[] NTSC_1953_PRIMARIES = { 0.67f, 0.33f, 0.21f, 0.71f, 0.14f, 0.08f }; + private static final float[] BT2020_PRIMARIES = + { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }; /** * A gray color space does not have meaningful primaries, so we use this arbitrary set. */ @@ -208,6 +210,12 @@ public abstract class ColorSpace { private static final Rgb.TransferParameters SRGB_TRANSFER_PARAMETERS = new Rgb.TransferParameters(1 / 1.055, 0.055 / 1.055, 1 / 12.92, 0.04045, 2.4); + private static final Rgb.TransferParameters BT2020_HLG_TRANSFER_PARAMETERS = + new Rgb.TransferParameters(2.0f, 2.0f, 1 / 0.17883277f, + 0.28466892f, 0.5599107f, 0.0f, -3.0f, true); + private static final Rgb.TransferParameters BT2020_PQ_TRANSFER_PARAMETERS = + new Rgb.TransferParameters(107 / 128.0f, 1.0f, 32 / 2523.0f, + 2413 / 128.0f, -2392 / 128.0f, 8192 / 1305.0f, -2.0f, true); // See static initialization block next to #get(Named) private static final ColorSpace[] sNamedColorSpaces = new ColorSpace[Named.values().length]; @@ -703,7 +711,29 @@ public abstract class ColorSpace { * <tr><td>Range</td><td colspan="4">\(L: [0.0, 100.0], a: [-128, 128], b: [-128, 128]\)</td></tr> * </table> */ - CIE_LAB + CIE_LAB, + /** + * <p>{@link ColorSpace.Rgb RGB} color space BT.2100 standardized as + * Hybrid Log Gamma encoding.</p> + * <table summary="Color space definition"> + * <tr><th>Property</th><th colspan="4">Value</th></tr> + * <tr><td>Name</td><td colspan="4">Hybrid Log Gamma encoding</td></tr> + * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> + * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> + * </table> + */ + BT2020_HLG, + /** + * <p>{@link ColorSpace.Rgb RGB} color space BT.2100 standardized as + * Perceptual Quantizer encoding.</p> + * <table summary="Color space definition"> + * <tr><th>Property</th><th colspan="4">Value</th></tr> + * <tr><td>Name</td><td colspan="4">Perceptual Quantizer encoding</td></tr> + * <tr><td>CIE standard illuminant</td><td colspan="4">D65</td></tr> + * <tr><td>Range</td><td colspan="4">\([0..1]\)</td></tr> + * </table> + */ + BT2020_PQ // Update the initialization block next to #get(Named) when adding new values } @@ -1534,7 +1564,7 @@ public abstract class ColorSpace { sDataToColorSpaces.put(DataSpace.DATASPACE_BT709, Named.BT709.ordinal()); sNamedColorSpaces[Named.BT2020.ordinal()] = new ColorSpace.Rgb( "Rec. ITU-R BT.2020-1", - new float[] { 0.708f, 0.292f, 0.170f, 0.797f, 0.131f, 0.046f }, + BT2020_PRIMARIES, ILLUMINANT_D65, null, new Rgb.TransferParameters(1 / 1.0993, 0.0993 / 1.0993, 1 / 4.5, 0.08145, 1 / 0.45), @@ -1616,6 +1646,70 @@ public abstract class ColorSpace { "Generic L*a*b*", Named.CIE_LAB.ordinal() ); + sNamedColorSpaces[Named.BT2020_HLG.ordinal()] = new ColorSpace.Rgb( + "Hybrid Log Gamma encoding", + BT2020_PRIMARIES, + ILLUMINANT_D65, + null, + x -> transferHLGOETF(x), + x -> transferHLGEOTF(x), + 0.0f, 1.0f, + BT2020_HLG_TRANSFER_PARAMETERS, + Named.BT2020_HLG.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020_HLG, Named.BT2020_HLG.ordinal()); + sNamedColorSpaces[Named.BT2020_PQ.ordinal()] = new ColorSpace.Rgb( + "Perceptual Quantizer encoding", + BT2020_PRIMARIES, + ILLUMINANT_D65, + null, + x -> transferST2048OETF(x), + x -> transferST2048EOTF(x), + 0.0f, 1.0f, + BT2020_PQ_TRANSFER_PARAMETERS, + Named.BT2020_PQ.ordinal() + ); + sDataToColorSpaces.put(DataSpace.DATASPACE_BT2020_PQ, Named.BT2020_PQ.ordinal()); + } + + private static double transferHLGOETF(double x) { + double a = 0.17883277; + double b = 0.28466892; + double c = 0.55991073; + double r = 0.5; + return x > 1.0 ? a * Math.log(x - b) + c : r * Math.sqrt(x); + } + + private static double transferHLGEOTF(double x) { + double a = 0.17883277; + double b = 0.28466892; + double c = 0.55991073; + double r = 0.5; + return x <= 0.5 ? (x * x) / (r * r) : Math.exp((x - c) / a + b); + } + + private static double transferST2048OETF(double x) { + double m1 = (2610.0 / 4096.0) / 4.0; + double m2 = (2523.0 / 4096.0) * 128.0; + double c1 = (3424.0 / 4096.0); + double c2 = (2413.0 / 4096.0) * 32.0; + double c3 = (2392.0 / 4096.0) * 32.0; + + double tmp = Math.pow(x, m1); + tmp = (c1 + c2 * tmp) / (1.0 + c3 * tmp); + return Math.pow(tmp, m2); + } + + private static double transferST2048EOTF(double x) { + double m1 = (2610.0 / 4096.0) / 4.0; + double m2 = (2523.0 / 4096.0) * 128.0; + double c1 = (3424.0 / 4096.0); + double c2 = (2413.0 / 4096.0) * 32.0; + double c3 = (2392.0 / 4096.0) * 32.0; + + double tmp = Math.pow(Math.min(Math.max(x, 0.0), 1.0), 1.0 / m2); + tmp = Math.max(tmp - c1, 0.0) / (c2 - c3 * tmp); + return Math.pow(tmp, 1.0 / m1); } // Reciprocal piecewise gamma response @@ -2197,6 +2291,58 @@ public abstract class ColorSpace { /** Variable \(g\) in the equation of the EOTF described above. */ public final double g; + private TransferParameters(double a, double b, double c, double d, double e, + double f, double g, boolean nonCurveTransferParameters) { + // nonCurveTransferParameters correspondes to a "special" transfer function + if (!nonCurveTransferParameters) { + if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) + || Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) + || Double.isNaN(g)) { + throw new IllegalArgumentException("Parameters cannot be NaN"); + } + + // Next representable float after 1.0 + // We use doubles here but the representation inside our native code + // is often floats + if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) { + throw new IllegalArgumentException( + "Parameter d must be in the range [0..1], " + "was " + d); + } + + if (d == 0.0 && (a == 0.0 || g == 0.0)) { + throw new IllegalArgumentException( + "Parameter a or g is zero, the transfer function is constant"); + } + + if (d >= 1.0 && c == 0.0) { + throw new IllegalArgumentException( + "Parameter c is zero, the transfer function is constant"); + } + + if ((a == 0.0 || g == 0.0) && c == 0.0) { + throw new IllegalArgumentException("Parameter a or g is zero," + + " and c is zero, the transfer function is constant"); + } + + if (c < 0.0) { + throw new IllegalArgumentException( + "The transfer function must be increasing"); + } + + if (a < 0.0 || g < 0.0) { + throw new IllegalArgumentException( + "The transfer function must be positive or increasing"); + } + } + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + this.g = g; + } + /** * <p>Defines the parameters for the ICC parametric curve type 3, as * defined in ICC.1:2004-10, section 10.15.</p> @@ -2219,7 +2365,7 @@ public abstract class ColorSpace { * @throws IllegalArgumentException If the parameters form an invalid transfer function */ public TransferParameters(double a, double b, double c, double d, double g) { - this(a, b, c, d, 0.0, 0.0, g); + this(a, b, c, d, 0.0, 0.0, g, false); } /** @@ -2238,51 +2384,7 @@ public abstract class ColorSpace { */ public TransferParameters(double a, double b, double c, double d, double e, double f, double g) { - - if (Double.isNaN(a) || Double.isNaN(b) || Double.isNaN(c) || - Double.isNaN(d) || Double.isNaN(e) || Double.isNaN(f) || - Double.isNaN(g)) { - throw new IllegalArgumentException("Parameters cannot be NaN"); - } - - // Next representable float after 1.0 - // We use doubles here but the representation inside our native code is often floats - if (!(d >= 0.0 && d <= 1.0f + Math.ulp(1.0f))) { - throw new IllegalArgumentException("Parameter d must be in the range [0..1], " + - "was " + d); - } - - if (d == 0.0 && (a == 0.0 || g == 0.0)) { - throw new IllegalArgumentException( - "Parameter a or g is zero, the transfer function is constant"); - } - - if (d >= 1.0 && c == 0.0) { - throw new IllegalArgumentException( - "Parameter c is zero, the transfer function is constant"); - } - - if ((a == 0.0 || g == 0.0) && c == 0.0) { - throw new IllegalArgumentException("Parameter a or g is zero," + - " and c is zero, the transfer function is constant"); - } - - if (c < 0.0) { - throw new IllegalArgumentException("The transfer function must be increasing"); - } - - if (a < 0.0 || g < 0.0) { - throw new IllegalArgumentException("The transfer function must be " + - "positive or increasing"); - } - - this.a = a; - this.b = b; - this.c = c; - this.d = d; - this.e = e; - this.f = f; - this.g = g; + this(a, b, c, d, e, f, g, false); } @SuppressWarnings("SimplifiableIfStatement") @@ -2357,6 +2459,36 @@ public abstract class ColorSpace { private static native long nativeCreate(float a, float b, float c, float d, float e, float f, float g, float[] xyz); + private static DoubleUnaryOperator generateOETF(TransferParameters function) { + boolean isNonCurveTransferParameters = function.equals(BT2020_HLG_TRANSFER_PARAMETERS) + || function.equals(BT2020_PQ_TRANSFER_PARAMETERS); + if (isNonCurveTransferParameters) { + return function.f == 0.0 && function.g < 0.0 ? x -> transferHLGOETF(x) + : x -> transferST2048OETF(x); + } else { + return function.e == 0.0 && function.f == 0.0 + ? x -> rcpResponse(x, function.a, function.b, + function.c, function.d, function.g) + : x -> rcpResponse(x, function.a, function.b, function.c, + function.d, function.e, function.f, function.g); + } + } + + private static DoubleUnaryOperator generateEOTF(TransferParameters function) { + boolean isNonCurveTransferParameters = function.equals(BT2020_HLG_TRANSFER_PARAMETERS) + || function.equals(BT2020_PQ_TRANSFER_PARAMETERS); + if (isNonCurveTransferParameters) { + return function.f == 0.0 && function.g < 0.0 ? x -> transferHLGEOTF(x) + : x -> transferST2048EOTF(x); + } else { + return function.e == 0.0 && function.f == 0.0 + ? x -> response(x, function.a, function.b, + function.c, function.d, function.g) + : x -> response(x, function.a, function.b, function.c, + function.d, function.e, function.f, function.g); + } + } + /** * <p>Creates a new RGB color space using a 3x3 column-major transform matrix. * The transform matrix must convert from the RGB space to the profile connection @@ -2553,16 +2685,8 @@ public abstract class ColorSpace { @NonNull TransferParameters function, @IntRange(from = MIN_ID, to = MAX_ID) int id) { this(name, primaries, whitePoint, transform, - function.e == 0.0 && function.f == 0.0 ? - x -> rcpResponse(x, function.a, function.b, - function.c, function.d, function.g) : - x -> rcpResponse(x, function.a, function.b, function.c, - function.d, function.e, function.f, function.g), - function.e == 0.0 && function.f == 0.0 ? - x -> response(x, function.a, function.b, - function.c, function.d, function.g) : - x -> response(x, function.a, function.b, function.c, - function.d, function.e, function.f, function.g), + generateOETF(function), + generateEOTF(function), 0.0f, 1.0f, function, id); } @@ -3063,7 +3187,12 @@ public abstract class ColorSpace { */ @Nullable public TransferParameters getTransferParameters() { - return mTransferParameters; + if (mTransferParameters != null + && !mTransferParameters.equals(BT2020_PQ_TRANSFER_PARAMETERS) + && !mTransferParameters.equals(BT2020_HLG_TRANSFER_PARAMETERS)) { + return mTransferParameters; + } + return null; } @Override diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 239621eeed1e..51f99ec637da 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -1682,11 +1682,16 @@ public final class ImageDecoder implements AutoCloseable { * {@link #decodeBitmap decodeBitmap} when setting a non-RGB color space * such as {@link ColorSpace.Named#CIE_LAB Lab}.</p> * - * <p class="note">The specified color space's transfer function must be + * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * the specified color space's transfer function must be * an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}. An * <code>IllegalArgumentException</code> will be thrown by the decode methods * if calling {@link ColorSpace.Rgb#getTransferParameters()} on the - * specified color space returns null.</p> + * specified color space returns null. + * Starting from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * the color spaces with non ICC parametric curve transfer function are allowed. + * E.g., {@link ColorSpace.Named#BT2020_HLG BT2020_HLG}. + * </p> * * <p>Like all setters on ImageDecoder, this must be called inside * {@link OnHeaderDecodedListener#onHeaderDecoded onHeaderDecoded}.</p> diff --git a/graphics/java/android/graphics/Mesh.java b/graphics/java/android/graphics/Mesh.java index 1f693166ecf0..a599b2c17604 100644 --- a/graphics/java/android/graphics/Mesh.java +++ b/graphics/java/android/graphics/Mesh.java @@ -16,6 +16,9 @@ package android.graphics; +import android.annotation.IntDef; +import android.annotation.NonNull; + import libcore.util.NativeAllocationRegistry; import java.nio.Buffer; @@ -25,23 +28,32 @@ import java.nio.ShortBuffer; * Class representing a mesh object. * * This class generates Mesh objects via the - * {@link #make(MeshSpecification, Mode, Buffer, int, Rect)} and - * {@link #makeIndexed(MeshSpecification, Mode, Buffer, int, ShortBuffer, Rect)} methods, + * {@link #make(MeshSpecification, int, Buffer, int, Rect)} and + * {@link #makeIndexed(MeshSpecification, int, Buffer, int, ShortBuffer, Rect)} methods, * where a {@link MeshSpecification} is required along with various attributes for * detailing the mesh object, including a mode, vertex buffer, optional index buffer, and bounds * for the mesh. Once generated, a mesh object can be drawn through * {@link Canvas#drawMesh(Mesh, BlendMode, Paint)} - * - * @hide */ public class Mesh { private long mNativeMeshWrapper; private boolean mIsIndexed; /** - * Enum to determine how the mesh is represented. + * Determines how the mesh is represented and will be drawn. + */ + @IntDef({TRIANGLES, TRIANGLE_STRIP}) + private @interface Mode {} + + /** + * The mesh will be drawn with triangles without utilizing shared vertices. */ - public enum Mode {Triangles, TriangleStrip} + public static final int TRIANGLES = 0; + + /** + * The mesh will be drawn with triangles utilizing shared vertices. + */ + public static final int TRIANGLE_STRIP = 1; private static class MeshHolder { public static final NativeAllocationRegistry MESH_SPECIFICATION_REGISTRY = @@ -53,7 +65,8 @@ public class Mesh { * Generates a {@link Mesh} object. * * @param meshSpec {@link MeshSpecification} used when generating the mesh. - * @param mode {@link Mode} enum + * @param mode Determines what mode to draw the mesh in. Must be one of + * {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP} * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data * for all attributes provided within the meshSpec for every vertex. That * is, a vertex buffer should be (attributes size * number of vertices) in @@ -63,9 +76,13 @@ public class Mesh { * @param bounds bounds of the mesh object. * @return a new Mesh object. */ - public static Mesh make(MeshSpecification meshSpec, Mode mode, Buffer vertexBuffer, - int vertexCount, Rect bounds) { - long nativeMesh = nativeMake(meshSpec.mNativeMeshSpec, mode.ordinal(), vertexBuffer, + @NonNull + public static Mesh make(@NonNull MeshSpecification meshSpec, @Mode int mode, + @NonNull Buffer vertexBuffer, int vertexCount, @NonNull Rect bounds) { + if (mode != TRIANGLES && mode != TRIANGLE_STRIP) { + throw new IllegalArgumentException("Invalid value passed in for mode parameter"); + } + long nativeMesh = nativeMake(meshSpec.mNativeMeshSpec, mode, vertexBuffer, vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), bounds.left, bounds.top, bounds.right, bounds.bottom); if (nativeMesh == 0) { @@ -78,7 +95,8 @@ public class Mesh { * Generates a {@link Mesh} object. * * @param meshSpec {@link MeshSpecification} used when generating the mesh. - * @param mode {@link Mode} enum + * @param mode Determines what mode to draw the mesh in. Must be one of + * {@link Mesh#TRIANGLES} or {@link Mesh#TRIANGLE_STRIP} * @param vertexBuffer vertex buffer representing through {@link Buffer}. This provides the data * for all attributes provided within the meshSpec for every vertex. That * is, a vertex buffer should be (attributes size * number of vertices) in @@ -92,9 +110,14 @@ public class Mesh { * @param bounds bounds of the mesh object. * @return a new Mesh object. */ - public static Mesh makeIndexed(MeshSpecification meshSpec, Mode mode, Buffer vertexBuffer, - int vertexCount, ShortBuffer indexBuffer, Rect bounds) { - long nativeMesh = nativeMakeIndexed(meshSpec.mNativeMeshSpec, mode.ordinal(), vertexBuffer, + @NonNull + public static Mesh makeIndexed(@NonNull MeshSpecification meshSpec, @Mode int mode, + @NonNull Buffer vertexBuffer, int vertexCount, @NonNull ShortBuffer indexBuffer, + @NonNull Rect bounds) { + if (mode != TRIANGLES && mode != TRIANGLE_STRIP) { + throw new IllegalArgumentException("Invalid value passed in for mode parameter"); + } + long nativeMesh = nativeMakeIndexed(meshSpec.mNativeMeshSpec, mode, vertexBuffer, vertexBuffer.isDirect(), vertexCount, vertexBuffer.position(), indexBuffer, indexBuffer.isDirect(), indexBuffer.capacity(), indexBuffer.position(), bounds.left, bounds.top, bounds.right, bounds.bottom); @@ -114,7 +137,7 @@ public class Mesh { * @param color the provided sRGB color will be converted into the shader program's output * colorspace and be available as a vec4 uniform in the program. */ - public void setColorUniform(String uniformName, int color) { + public void setColorUniform(@NonNull String uniformName, int color) { setUniform(uniformName, Color.valueOf(color).getComponents(), true); } @@ -128,7 +151,7 @@ public class Mesh { * @param color the provided sRGB color will be converted into the shader program's output * colorspace and be available as a vec4 uniform in the program. */ - public void setColorUniform(String uniformName, long color) { + public void setColorUniform(@NonNull String uniformName, long color) { Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); setUniform(uniformName, exSRGB.getComponents(), true); } @@ -143,7 +166,7 @@ public class Mesh { * @param color the provided sRGB color will be converted into the shader program's output * colorspace and will be made available as a vec4 uniform in the program. */ - public void setColorUniform(String uniformName, Color color) { + public void setColorUniform(@NonNull String uniformName, @NonNull Color color) { if (color == null) { throw new NullPointerException("The color parameter must not be null"); } @@ -160,7 +183,7 @@ public class Mesh { * @param uniformName name matching the float uniform declared in the shader program. * @param value float value corresponding to the float uniform with the given name. */ - public void setFloatUniform(String uniformName, float value) { + public void setFloatUniform(@NonNull String uniformName, float value) { setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1); } @@ -173,7 +196,7 @@ public class Mesh { * @param value1 first float value corresponding to the float uniform with the given name. * @param value2 second float value corresponding to the float uniform with the given name. */ - public void setFloatUniform(String uniformName, float value1, float value2) { + public void setFloatUniform(@NonNull String uniformName, float value1, float value2) { setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2); } @@ -188,7 +211,8 @@ public class Mesh { * @param value3 third float value corresponding to the float unifiform with the given * name. */ - public void setFloatUniform(String uniformName, float value1, float value2, float value3) { + public void setFloatUniform( + @NonNull String uniformName, float value1, float value2, float value3) { setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3); } @@ -204,7 +228,7 @@ public class Mesh { * @param value4 fourth float value corresponding to the float uniform with the given name. */ public void setFloatUniform( - String uniformName, float value1, float value2, float value3, float value4) { + @NonNull String uniformName, float value1, float value2, float value3, float value4) { setFloatUniform(uniformName, value1, value2, value3, value4, 4); } @@ -217,7 +241,7 @@ public class Mesh { * @param uniformName name matching the float uniform declared in the shader program. * @param values float value corresponding to the vec4 float uniform with the given name. */ - public void setFloatUniform(String uniformName, float[] values) { + public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) { setUniform(uniformName, values, false); } @@ -249,7 +273,7 @@ public class Mesh { * @param uniformName name matching the int uniform delcared in the shader program. * @param value value corresponding to the int uniform with the given name. */ - public void setIntUniform(String uniformName, int value) { + public void setIntUniform(@NonNull String uniformName, int value) { setIntUniform(uniformName, value, 0, 0, 0, 1); } @@ -262,7 +286,7 @@ public class Mesh { * @param value1 first value corresponding to the int uniform with the given name. * @param value2 second value corresponding to the int uniform with the given name. */ - public void setIntUniform(String uniformName, int value1, int value2) { + public void setIntUniform(@NonNull String uniformName, int value1, int value2) { setIntUniform(uniformName, value1, value2, 0, 0, 2); } @@ -276,7 +300,7 @@ public class Mesh { * @param value2 second value corresponding to the int uniform with the given name. * @param value3 third value corresponding to the int uniform with the given name. */ - public void setIntUniform(String uniformName, int value1, int value2, int value3) { + public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) { setIntUniform(uniformName, value1, value2, value3, 0, 3); } @@ -291,7 +315,8 @@ public class Mesh { * @param value3 third value corresponding to the int uniform with the given name. * @param value4 fourth value corresponding to the int uniform with the given name. */ - public void setIntUniform(String uniformName, int value1, int value2, int value3, int value4) { + public void setIntUniform( + @NonNull String uniformName, int value1, int value2, int value3, int value4) { setIntUniform(uniformName, value1, value2, value3, value4, 4); } @@ -304,7 +329,7 @@ public class Mesh { * @param uniformName name matching the int uniform delcared in the shader program. * @param values int values corresponding to the vec4 int uniform with the given name. */ - public void setIntUniform(String uniformName, int[] values) { + public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) { if (uniformName == null) { throw new NullPointerException("The uniformName parameter must not be null"); } diff --git a/graphics/java/android/graphics/MeshSpecification.java b/graphics/java/android/graphics/MeshSpecification.java index dd8fb7ab2370..52b40029a0eb 100644 --- a/graphics/java/android/graphics/MeshSpecification.java +++ b/graphics/java/android/graphics/MeshSpecification.java @@ -17,15 +17,18 @@ package android.graphics; import android.annotation.IntDef; +import android.annotation.NonNull; import libcore.util.NativeAllocationRegistry; +import java.util.List; + /** * Class responsible for holding specifications for {@link Mesh} creations. This class * generates a {@link MeshSpecification} via the Make method, where multiple parameters to set up * the mesh are supplied, including attributes, vertex stride, varyings, and * vertex/fragment shaders. There are also additional methods to provide an optional - * {@link ColorSpace} as well as an {@link AlphaType}. + * {@link ColorSpace} as well as an alpha type. * * Note that there are several limitations on various mesh specifications: * 1. The max amount of attributes allowed is 8. @@ -35,19 +38,16 @@ import libcore.util.NativeAllocationRegistry; * * These should be kept in mind when generating a mesh specification, as exceeding them will * lead to errors. - * - * @hide */ public class MeshSpecification { long mNativeMeshSpec; /** - * Constants for {@link #make(Attribute[], int, Varying[], String, String, ColorSpace, int)} + * Constants for {@link #make(List, int, List, String, String)} * to determine alpha type. Describes how to interpret the alpha component of a pixel. */ @IntDef({UNKNOWN, OPAQUE, PREMUL, UNPREMULT}) - public @interface AlphaType { - } + private @interface AlphaType {} /** * uninitialized. @@ -73,8 +73,7 @@ public class MeshSpecification { * Constants for {@link Attribute} and {@link Varying} for determining the data type. */ @IntDef({FLOAT, FLOAT2, FLOAT3, FLOAT4, UBYTE4}) - public @interface Type { - } + private @interface Type {} /** * Represents one float. Its equivalent shader type is float. @@ -118,7 +117,7 @@ public class MeshSpecification { private int mOffset; private String mName; - public Attribute(@Type int type, int offset, String name) { + public Attribute(@Type int type, int offset, @NonNull String name) { mType = type; mOffset = offset; mName = name; @@ -134,7 +133,7 @@ public class MeshSpecification { private int mType; private String mName; - public Varying(@Type int type, String name) { + public Varying(@Type int type, @NonNull String name) { mType = type; mName = name; } @@ -162,10 +161,13 @@ public class MeshSpecification { * @param fragmentShader fragment shader to be supplied to the mesh. * @return {@link MeshSpecification} object for use when creating {@link Mesh} */ - public static MeshSpecification make(Attribute[] attributes, int vertexStride, - Varying[] varyings, String vertexShader, String fragmentShader) { - long nativeMeshSpec = - nativeMake(attributes, vertexStride, varyings, vertexShader, fragmentShader); + @NonNull + public static MeshSpecification make(@NonNull List<Attribute> attributes, int vertexStride, + @NonNull List<Varying> varyings, @NonNull String vertexShader, + @NonNull String fragmentShader) { + long nativeMeshSpec = nativeMake(attributes.toArray(new Attribute[attributes.size()]), + vertexStride, varyings.toArray(new Varying[varyings.size()]), vertexShader, + fragmentShader); if (nativeMeshSpec == 0) { throw new IllegalArgumentException("MeshSpecification construction failed"); } @@ -189,9 +191,12 @@ public class MeshSpecification { * @param colorSpace {@link ColorSpace} to tell what color space to work in. * @return {@link MeshSpecification} object for use when creating {@link Mesh} */ - public static MeshSpecification make(Attribute[] attributes, int vertexStride, - Varying[] varyings, String vertexShader, String fragmentShader, ColorSpace colorSpace) { - long nativeMeshSpec = nativeMakeWithCS(attributes, vertexStride, varyings, vertexShader, + @NonNull + public static MeshSpecification make(@NonNull List<Attribute> attributes, int vertexStride, + @NonNull List<Varying> varyings, @NonNull String vertexShader, + @NonNull String fragmentShader, @NonNull ColorSpace colorSpace) { + long nativeMeshSpec = nativeMakeWithCS(attributes.toArray(new Attribute[attributes.size()]), + vertexStride, varyings.toArray(new Varying[varyings.size()]), vertexShader, fragmentShader, colorSpace.getNativeInstance()); if (nativeMeshSpec == 0) { throw new IllegalArgumentException("MeshSpecification construction failed"); @@ -215,14 +220,22 @@ public class MeshSpecification { * @param fragmentShader fragment shader to be supplied to the mesh. * @param colorSpace {@link ColorSpace} to tell what color space to work in. * @param alphaType Describes how to interpret the alpha component for a pixel. Must be - * one of {@link AlphaType} values. + * one of + * {@link MeshSpecification#UNKNOWN}, + * {@link MeshSpecification#OPAQUE}, + * {@link MeshSpecification#PREMUL}, or + * {@link MeshSpecification#UNPREMULT} * @return {@link MeshSpecification} object for use when creating {@link Mesh} */ - public static MeshSpecification make(Attribute[] attributes, int vertexStride, - Varying[] varyings, String vertexShader, String fragmentShader, ColorSpace colorSpace, + @NonNull + public static MeshSpecification make(@NonNull List<Attribute> attributes, int vertexStride, + @NonNull List<Varying> varyings, @NonNull String vertexShader, + @NonNull String fragmentShader, @NonNull ColorSpace colorSpace, @AlphaType int alphaType) { - long nativeMeshSpec = nativeMakeWithAlpha(attributes, vertexStride, varyings, vertexShader, - fragmentShader, colorSpace.getNativeInstance(), alphaType); + long nativeMeshSpec = + nativeMakeWithAlpha(attributes.toArray(new Attribute[attributes.size()]), + vertexStride, varyings.toArray(new Varying[varyings.size()]), vertexShader, + fragmentShader, colorSpace.getNativeInstance(), alphaType); if (nativeMeshSpec == 0) { throw new IllegalArgumentException("MeshSpecification construction failed"); } diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index 23db2335ecc0..774f6c6379b2 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -115,4 +115,10 @@ <!-- Components support to launch multiple instances into split-screen --> <string-array name="config_appsSupportMultiInstancesSplit"> </string-array> + + <!-- Whether the extended restart dialog is enabled --> + <bool name="config_letterboxIsRestartDialogEnabled">false</bool> + + <!-- Whether the additional education about reachability is enabled --> + <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool> </resources> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java new file mode 100644 index 000000000000..4f33a71b80d5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import android.content.Context; +import android.provider.DeviceConfig; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.R; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.dagger.WMSingleton; + +import javax.inject.Inject; + +/** + * Configuration flags for the CompatUX implementation + */ +@WMSingleton +public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedListener { + + static final String KEY_ENABLE_LETTERBOX_RESTART_DIALOG = "enable_letterbox_restart_dialog"; + + static final String KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION = + "enable_letterbox_reachability_education"; + + // Whether the extended restart dialog is enabled + private boolean mIsRestartDialogEnabled; + + // Whether the additional education about reachability is enabled + private boolean mIsReachabilityEducationEnabled; + + // Whether the extended restart dialog is enabled + private boolean mIsRestartDialogOverrideEnabled; + + // Whether the additional education about reachability is enabled + private boolean mIsReachabilityEducationOverrideEnabled; + + // Whether the extended restart dialog is allowed from backend + private boolean mIsLetterboxRestartDialogAllowed; + + // Whether the additional education about reachability is allowed from backend + private boolean mIsLetterboxReachabilityEducationAllowed; + + @Inject + public CompatUIConfiguration(Context context, @ShellMainThread ShellExecutor mainExecutor) { + mIsRestartDialogEnabled = context.getResources().getBoolean( + R.bool.config_letterboxIsRestartDialogEnabled); + mIsReachabilityEducationEnabled = context.getResources().getBoolean( + R.bool.config_letterboxIsReachabilityEducationEnabled); + mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, false); + mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, + false); + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_APP_COMPAT, mainExecutor, + this); + } + + /** + * @return {@value true} if the restart dialog is enabled. + */ + boolean isRestartDialogEnabled() { + return mIsRestartDialogOverrideEnabled || (mIsRestartDialogEnabled + && mIsLetterboxRestartDialogAllowed); + } + + /** + * Enables/Disables the restart education dialog + */ + void setIsRestartDialogOverrideEnabled(boolean enabled) { + mIsRestartDialogOverrideEnabled = enabled; + } + + /** + * @return {@value true} if the reachability education is enabled. + */ + boolean isReachabilityEducationEnabled() { + return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled + && mIsLetterboxReachabilityEducationAllowed); + } + + /** + * Enables/Disables the reachability education + */ + void setIsReachabilityEducationOverrideEnabled(boolean enabled) { + mIsReachabilityEducationOverrideEnabled = enabled; + } + + @Override + public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) { + // TODO(b/263349751): Update flag and default value to true + if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_RESTART_DIALOG)) { + mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, + false); + } + if (properties.getKeyset().contains(KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION)) { + mIsLetterboxReachabilityEducationAllowed = DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, + KEY_ENABLE_LETTERBOX_REACHABILITY_EDUCATION, false); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java new file mode 100644 index 000000000000..4fb18e27b145 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIShellCommandHandler.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.sysui.ShellCommandHandler; + +import java.io.PrintWriter; +import java.util.function.Consumer; + +import javax.inject.Inject; + +/** + * Handles the shell commands for the CompatUX. + * + * <p> Use with {@code adb shell dumpsys activity service SystemUIService WMShell compatui + * <command>}. + */ +@WMSingleton +public final class CompatUIShellCommandHandler implements + ShellCommandHandler.ShellCommandActionHandler { + + private final CompatUIConfiguration mCompatUIConfiguration; + private final ShellCommandHandler mShellCommandHandler; + + @Inject + public CompatUIShellCommandHandler(ShellCommandHandler shellCommandHandler, + CompatUIConfiguration compatUIConfiguration) { + mShellCommandHandler = shellCommandHandler; + mCompatUIConfiguration = compatUIConfiguration; + } + + void onInit() { + mShellCommandHandler.addCommandCallback("compatui", this, this); + } + + @Override + public boolean onShellCommand(String[] args, PrintWriter pw) { + if (args.length != 2) { + pw.println("Invalid command: " + args[0]); + return false; + } + switch (args[0]) { + case "restartDialogEnabled": + return invokeOrError(args[1], pw, + mCompatUIConfiguration::setIsRestartDialogOverrideEnabled); + case "reachabilityEducationEnabled": + return invokeOrError(args[1], pw, + mCompatUIConfiguration::setIsReachabilityEducationOverrideEnabled); + default: + pw.println("Invalid command: " + args[0]); + return false; + } + } + + @Override + public void printShellCommandHelp(PrintWriter pw, String prefix) { + pw.println(prefix + "restartDialogEnabled [0|false|1|true]"); + pw.println(prefix + " Enable/Disable the restart education dialog for Size Compat Mode"); + pw.println(prefix + "reachabilityEducationEnabled [0|false|1|true]"); + pw.println(prefix + + " Enable/Disable the restart education dialog for letterbox reachability"); + pw.println(prefix + " Disable the restart education dialog for letterbox reachability"); + } + + private static boolean invokeOrError(String input, PrintWriter pw, + Consumer<Boolean> setter) { + Boolean asBoolean = strToBoolean(input); + if (asBoolean == null) { + pw.println("Error: expected true, 1, false, 0."); + return false; + } + setter.accept(asBoolean); + return true; + } + + // Converts a String to boolean if possible or it returns null otherwise + private static Boolean strToBoolean(String str) { + switch(str) { + case "1": + case "true": + return true; + case "0": + case "false": + return false; + } + return null; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java index 3061eab17d24..7475feac5b12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogAnimationController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.compatui.letterboxedu; +package com.android.wm.shell.compatui; import static com.android.internal.R.styleable.WindowAnimation_windowEnterAnimation; import static com.android.internal.R.styleable.WindowAnimation_windowExitAnimation; @@ -38,10 +38,15 @@ import android.view.animation.Animation; import com.android.internal.policy.TransitionAnimation; /** - * Controls the enter/exit animations of the letterbox education. + * Controls the enter/exit a dialog. + * + * @param <T> The {@link DialogContainerSupplier} to use */ -class LetterboxEduAnimationController { - private static final String TAG = "LetterboxEduAnimation"; +public class DialogAnimationController<T extends DialogContainerSupplier> { + + // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque). + // 204 is simply 255 * 0.8. + static final int BACKGROUND_DIM_ALPHA = 204; // If shell transitions are enabled, startEnterAnimation will be called after all transitions // have finished, and therefore the start delay should be shorter. @@ -49,6 +54,7 @@ class LetterboxEduAnimationController { private final TransitionAnimation mTransitionAnimation; private final String mPackageName; + private final String mTag; @AnyRes private final int mAnimStyleResId; @@ -57,23 +63,24 @@ class LetterboxEduAnimationController { @Nullable private Animator mBackgroundDimAnimator; - LetterboxEduAnimationController(Context context) { - mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, TAG); + public DialogAnimationController(Context context, String tag) { + mTransitionAnimation = new TransitionAnimation(context, /* debug= */ false, tag); mAnimStyleResId = (new ContextThemeWrapper(context, android.R.style.ThemeOverlay_Material_Dialog).getTheme()).obtainStyledAttributes( com.android.internal.R.styleable.Window).getResourceId( com.android.internal.R.styleable.Window_windowAnimationStyle, 0); mPackageName = context.getPackageName(); + mTag = tag; } /** * Starts both background dim fade-in animation and the dialog enter animation. */ - void startEnterAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) { + public void startEnterAnimation(@NonNull T layout, Runnable endCallback) { // Cancel any previous animation if it's still running. cancelAnimation(); - final View dialogContainer = layout.getDialogContainer(); + final View dialogContainer = layout.getDialogContainerView(); mDialogAnimation = loadAnimation(WindowAnimation_windowEnterAnimation); if (mDialogAnimation == null) { endCallback.run(); @@ -86,8 +93,8 @@ class LetterboxEduAnimationController { endCallback.run(); })); - mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), - /* endAlpha= */ LetterboxEduDialogLayout.BACKGROUND_DIM_ALPHA, + mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(), + /* endAlpha= */ BACKGROUND_DIM_ALPHA, mDialogAnimation.getDuration()); mBackgroundDimAnimator.addListener(getDimAnimatorListener()); @@ -101,11 +108,11 @@ class LetterboxEduAnimationController { /** * Starts both the background dim fade-out animation and the dialog exit animation. */ - void startExitAnimation(@NonNull LetterboxEduDialogLayout layout, Runnable endCallback) { + public void startExitAnimation(@NonNull T layout, Runnable endCallback) { // Cancel any previous animation if it's still running. cancelAnimation(); - final View dialogContainer = layout.getDialogContainer(); + final View dialogContainer = layout.getDialogContainerView(); mDialogAnimation = loadAnimation(WindowAnimation_windowExitAnimation); if (mDialogAnimation == null) { endCallback.run(); @@ -119,8 +126,8 @@ class LetterboxEduAnimationController { endCallback.run(); })); - mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDim(), /* endAlpha= */ 0, - mDialogAnimation.getDuration()); + mBackgroundDimAnimator = getAlphaAnimator(layout.getBackgroundDimDrawable(), + /* endAlpha= */ 0, mDialogAnimation.getDuration()); mBackgroundDimAnimator.addListener(getDimAnimatorListener()); dialogContainer.startAnimation(mDialogAnimation); @@ -130,7 +137,7 @@ class LetterboxEduAnimationController { /** * Cancels all animations and resets the state of the controller. */ - void cancelAnimation() { + public void cancelAnimation() { if (mDialogAnimation != null) { mDialogAnimation.cancel(); mDialogAnimation = null; @@ -145,7 +152,7 @@ class LetterboxEduAnimationController { Animation animation = mTransitionAnimation.loadAnimationAttr(mPackageName, mAnimStyleResId, animAttr, /* translucent= */ false); if (animation == null) { - Log.e(TAG, "Failed to load animation " + animAttr); + Log.e(mTag, "Failed to load animation " + animAttr); } return animation; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java new file mode 100644 index 000000000000..7eea446fce26 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/DialogContainerSupplier.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui; + +import android.graphics.drawable.Drawable; +import android.view.View; + +/** + * A component which can provide a {@link View} to use as a container for a Dialog + */ +public interface DialogContainerSupplier { + + /** + * @return The {@link View} to use as a container for a Dialog + */ + View getDialogContainerView(); + + /** + * @return The {@link Drawable} to use as background of the dialog. + */ + Drawable getBackgroundDimDrawable(); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java index 2e0b09e9d230..9232f36cf939 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayout.java @@ -26,6 +26,7 @@ import android.widget.TextView; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.wm.shell.R; +import com.android.wm.shell.compatui.DialogContainerSupplier; /** * Container for Letterbox Education Dialog and background dim. @@ -33,11 +34,7 @@ import com.android.wm.shell.R; * <p>This layout should fill the entire task and the background around the dialog acts as the * background dim which dismisses the dialog when clicked. */ -class LetterboxEduDialogLayout extends ConstraintLayout { - - // The alpha of a background is a number between 0 (fully transparent) to 255 (fully opaque). - // 204 is simply 255 * 0.8. - static final int BACKGROUND_DIM_ALPHA = 204; +class LetterboxEduDialogLayout extends ConstraintLayout implements DialogContainerSupplier { private View mDialogContainer; private TextView mDialogTitle; @@ -60,16 +57,18 @@ class LetterboxEduDialogLayout extends ConstraintLayout { super(context, attrs, defStyleAttr, defStyleRes); } - View getDialogContainer() { + @Override + public View getDialogContainerView() { return mDialogContainer; } - TextView getDialogTitle() { - return mDialogTitle; + @Override + public Drawable getBackgroundDimDrawable() { + return mBackgroundDim; } - Drawable getBackgroundDim() { - return mBackgroundDim; + TextView getDialogTitle() { + return mDialogTitle; } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java index 867d0ef732ac..c14c009721a1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManager.java @@ -37,6 +37,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIWindowManagerAbstract; +import com.android.wm.shell.compatui.DialogAnimationController; import com.android.wm.shell.transition.Transitions; /** @@ -63,7 +64,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { */ private final SharedPreferences mSharedPreferences; - private final LetterboxEduAnimationController mAnimationController; + private final DialogAnimationController<LetterboxEduDialogLayout> mAnimationController; private final Transitions mTransitions; @@ -96,14 +97,17 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback, DockStateReader dockStateReader) { this(context, taskInfo, syncQueue, taskListener, displayLayout, transitions, - onDismissCallback, new LetterboxEduAnimationController(context), dockStateReader); + onDismissCallback, + new DialogAnimationController<>(context, /* tag */ "LetterboxEduWindowManager"), + dockStateReader); } @VisibleForTesting LetterboxEduWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, Transitions transitions, Runnable onDismissCallback, - LetterboxEduAnimationController animationController, DockStateReader dockStateReader) { + DialogAnimationController<LetterboxEduDialogLayout> animationController, + DockStateReader dockStateReader) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mTransitions = transitions; mOnDismissCallback = onDismissCallback; @@ -160,7 +164,7 @@ public class LetterboxEduWindowManager extends CompatUIWindowManagerAbstract { if (mLayout == null) { return; } - final View dialogContainer = mLayout.getDialogContainer(); + final View dialogContainer = mLayout.getDialogContainerView(); MarginLayoutParams marginParams = (MarginLayoutParams) dialogContainer.getLayoutParams(); final Rect taskBounds = getTaskBounds(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 701a3a42fadd..d3b9fa5e628d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -93,7 +93,7 @@ import com.android.wm.shell.unfold.animation.SplitTaskUnfoldAnimator; import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator; import com.android.wm.shell.unfold.qualifier.UnfoldShellTransition; import com.android.wm.shell.unfold.qualifier.UnfoldTransition; -import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import java.util.ArrayList; @@ -192,7 +192,7 @@ public abstract class WMShellModule { SyncTransactionQueue syncQueue, Optional<DesktopModeController> desktopModeController, Optional<DesktopTasksController> desktopTasksController) { - return new CaptionWindowDecorViewModel( + return new DesktopModeWindowDecorViewModel( context, mainHandler, mainChoreographer, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java index 6b59e313b01b..d7cb490ed0cb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java @@ -16,8 +16,6 @@ package com.android.wm.shell.unfold; -import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; - import android.annotation.NonNull; import android.app.ActivityManager.RunningTaskInfo; import android.app.TaskInfo; @@ -56,6 +54,12 @@ public class UnfoldAnimationController implements UnfoldListener { private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>(); private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>(); + /** + * Indicates whether we're in stage change process. This should be set to {@code true} in + * {@link #onStateChangeStarted()} and {@code false} in {@link #onStateChangeFinished()}. + */ + private boolean mIsInStageChange; + public UnfoldAnimationController( @NonNull ShellInit shellInit, @NonNull TransactionPool transactionPool, @@ -123,7 +127,7 @@ public class UnfoldAnimationController implements UnfoldListener { animator.onTaskChanged(taskInfo); } else { // Became inapplicable - resetTask(animator, taskInfo); + maybeResetTask(animator, taskInfo); animator.onTaskVanished(taskInfo); mAnimatorsByTaskId.remove(taskInfo.taskId); } @@ -154,7 +158,7 @@ public class UnfoldAnimationController implements UnfoldListener { final boolean isCurrentlyApplicable = animator != null; if (isCurrentlyApplicable) { - resetTask(animator, taskInfo); + maybeResetTask(animator, taskInfo); animator.onTaskVanished(taskInfo); mAnimatorsByTaskId.remove(taskInfo.taskId); } @@ -166,6 +170,7 @@ public class UnfoldAnimationController implements UnfoldListener { return; } + mIsInStageChange = true; SurfaceControl.Transaction transaction = null; for (int i = 0; i < mAnimators.size(); i++) { final UnfoldTaskAnimator animator = mAnimators.get(i); @@ -219,11 +224,12 @@ public class UnfoldAnimationController implements UnfoldListener { transaction.apply(); mTransactionPool.release(transaction); + mIsInStageChange = false; } - private void resetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) { - if (taskInfo.getWindowingMode() == WINDOWING_MODE_PINNED) { - // PiP task has its own cleanup path, ignore surface reset to avoid conflict. + private void maybeResetTask(UnfoldTaskAnimator animator, TaskInfo taskInfo) { + if (!mIsInStageChange) { + // No need to resetTask if there is no ongoing state change. return; } final SurfaceControl.Transaction transaction = mTransactionPool.acquire(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 299284f5bda6..b500f5fb0155 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -61,12 +61,12 @@ import java.util.Optional; /** * View model for the window decoration with a caption and shadows. Works with - * {@link CaptionWindowDecoration}. + * {@link DesktopModeWindowDecoration}. */ -public class CaptionWindowDecorViewModel implements WindowDecorViewModel { - private static final String TAG = "CaptionViewModel"; - private final CaptionWindowDecoration.Factory mCaptionWindowDecorFactory; +public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { + private static final String TAG = "DesktopModeWindowDecorViewModel"; + private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory; private final ActivityTaskManager mActivityTaskManager; private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; @@ -81,11 +81,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); - private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); + private final SparseArray<DesktopModeWindowDecoration> mWindowDecorByTaskId = + new SparseArray<>(); private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); private InputMonitorFactory mInputMonitorFactory; - public CaptionWindowDecorViewModel( + public DesktopModeWindowDecorViewModel( Context context, Handler mainHandler, Choreographer mainChoreographer, @@ -103,12 +104,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { syncQueue, desktopModeController, desktopTasksController, - new CaptionWindowDecoration.Factory(), + new DesktopModeWindowDecoration.Factory(), new InputMonitorFactory()); } @VisibleForTesting - CaptionWindowDecorViewModel( + DesktopModeWindowDecorViewModel( Context context, Handler mainHandler, Choreographer mainChoreographer, @@ -117,7 +118,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Optional<DesktopModeController> desktopModeController, Optional<DesktopTasksController> desktopTasksController, - CaptionWindowDecoration.Factory captionWindowDecorFactory, + DesktopModeWindowDecoration.Factory desktopModeWindowDecorFactory, InputMonitorFactory inputMonitorFactory) { mContext = context; mMainHandler = mainHandler; @@ -129,7 +130,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeController = desktopModeController; mDesktopTasksController = desktopTasksController; - mCaptionWindowDecorFactory = captionWindowDecorFactory; + mDesktopModeWindowDecorFactory = desktopModeWindowDecorFactory; mInputMonitorFactory = inputMonitorFactory; } @@ -151,7 +152,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { @Override public void onTaskInfoChanged(RunningTaskInfo taskInfo) { - final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (decoration == null) return; @@ -170,7 +171,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { SurfaceControl taskSurface, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (!shouldShowWindowDecor(taskInfo)) { if (decoration != null) { @@ -191,7 +192,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (decoration == null) return; decoration.relayout(taskInfo, startT, finishT); @@ -199,7 +200,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { @Override public void destroyWindowDecoration(RunningTaskInfo taskInfo) { - final CaptionWindowDecoration decoration = + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.removeReturnOld(taskInfo.taskId); if (decoration == null) return; @@ -232,7 +233,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { @Override public void onClick(View v) { - CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); final int id = v.getId(); if (id == R.id.close_window) { WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -373,7 +374,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { boolean handled = false; if (event instanceof MotionEvent) { handled = true; - CaptionWindowDecorViewModel.this + DesktopModeWindowDecorViewModel.this .handleReceivedMotionEvent((MotionEvent) event, mInputMonitor); } finishInputEvent(event, handled); @@ -433,7 +434,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { */ private void handleReceivedMotionEvent(MotionEvent ev, InputMonitor inputMonitor) { if (DesktopModeStatus.isProto2Enabled()) { - CaptionWindowDecoration focusedDecor = getFocusedDecor(); + DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor == null || focusedDecor.mTaskInfo.getWindowingMode() != WINDOWING_MODE_FREEFORM) { handleCaptionThroughStatusBar(ev); @@ -460,7 +461,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private void handleEventOutsideFocusedCaption(MotionEvent ev) { int action = ev.getActionMasked(); if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - CaptionWindowDecoration focusedDecor = getFocusedDecor(); + DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor == null) { return; } @@ -480,7 +481,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: { // Begin drag through status bar if applicable. - CaptionWindowDecoration focusedDecor = getFocusedDecor(); + DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor != null) { boolean dragFromStatusBarAllowed = false; if (DesktopModeStatus.isProto2Enabled()) { @@ -499,7 +500,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { break; } case MotionEvent.ACTION_UP: { - CaptionWindowDecoration focusedDecor = getFocusedDecor(); + DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); if (focusedDecor == null) { mTransitionDragActive = false; return; @@ -529,11 +530,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } @Nullable - private CaptionWindowDecoration getFocusedDecor() { + private DesktopModeWindowDecoration getFocusedDecor() { int size = mWindowDecorByTaskId.size(); - CaptionWindowDecoration focusedDecor = null; + DesktopModeWindowDecoration focusedDecor = null; for (int i = 0; i < size; i++) { - CaptionWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); + DesktopModeWindowDecoration decor = mWindowDecorByTaskId.valueAt(i); if (decor != null && decor.isFocused()) { focusedDecor = decor; break; @@ -571,13 +572,13 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { SurfaceControl taskSurface, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - CaptionWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); + DesktopModeWindowDecoration oldDecoration = mWindowDecorByTaskId.get(taskInfo.taskId); if (oldDecoration != null) { // close the old decoration if it exists to avoid two window decorations being added oldDecoration.close(); } - final CaptionWindowDecoration windowDecoration = - mCaptionWindowDecorFactory.create( + final DesktopModeWindowDecoration windowDecoration = + mDesktopModeWindowDecorFactory.create( mContext, mDisplayController, mTaskOrganizer, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index f7c7a87e6659..467f374f2110 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -43,12 +43,12 @@ import com.android.wm.shell.desktopmode.DesktopModeStatus; /** * Defines visuals and behaviors of a window decoration of a caption bar and shadows. It works with - * {@link CaptionWindowDecorViewModel}. The caption bar contains a handle, back button, and close - * button. + * {@link DesktopModeWindowDecorViewModel}. The caption bar contains a handle, back button, and + * close button. * * The shadow's thickness is 20dp when the window is in focus and 5dp when the window isn't. */ -public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { +public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private final Handler mHandler; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -69,7 +69,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private AdditionalWindow mHandleMenu; - CaptionWindowDecoration( + DesktopModeWindowDecoration( Context context, DisplayController displayController, ShellTaskOrganizer taskOrganizer, @@ -424,7 +424,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL static class Factory { - CaptionWindowDecoration create( + DesktopModeWindowDecoration create( Context context, DisplayController displayController, ShellTaskOrganizer taskOrganizer, @@ -433,7 +433,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL Handler handler, Choreographer choreographer, SyncTransactionQueue syncQueue) { - return new CaptionWindowDecoration( + return new DesktopModeWindowDecoration( context, displayController, taskOrganizer, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java index 1dee88c43806..a58620dfc6dc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduDialogLayoutTest.java @@ -68,11 +68,11 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { @Test public void testOnFinishInflate() { - assertEquals(mLayout.getDialogContainer(), + assertEquals(mLayout.getDialogContainerView(), mLayout.findViewById(R.id.letterbox_education_dialog_container)); assertEquals(mLayout.getDialogTitle(), mLayout.findViewById(R.id.letterbox_education_dialog_title)); - assertEquals(mLayout.getBackgroundDim(), mLayout.getBackground()); + assertEquals(mLayout.getBackgroundDimDrawable(), mLayout.getBackground()); assertEquals(mLayout.getBackground().getAlpha(), 0); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java index 16517c0a0010..14190f18929c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterboxedu/LetterboxEduWindowManagerTest.java @@ -56,6 +56,7 @@ import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.DialogAnimationController; import com.android.wm.shell.transition.Transitions; import org.junit.After; @@ -98,7 +99,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { @Captor private ArgumentCaptor<Runnable> mRunOnIdleCaptor; - @Mock private LetterboxEduAnimationController mAnimationController; + @Mock private DialogAnimationController<LetterboxEduDialogLayout> mAnimationController; @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private ShellTaskOrganizer.TaskListener mTaskListener; @Mock private SurfaceControlViewHost mViewHost; @@ -366,7 +367,7 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { assertThat(params.width).isEqualTo(expectedWidth); assertThat(params.height).isEqualTo(expectedHeight); MarginLayoutParams dialogParams = - (MarginLayoutParams) layout.getDialogContainer().getLayoutParams(); + (MarginLayoutParams) layout.getDialogContainerView().getLayoutParams(); int verticalMargin = (int) mContext.getResources().getDimension( R.dimen.letterbox_education_dialog_margin); assertThat(dialogParams.topMargin).isEqualTo(verticalMargin + expectedExtraTopMargin); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java index 487583262011..a5e3a2e76ce5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java @@ -60,14 +60,14 @@ import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -/** Tests of {@link CaptionWindowDecorViewModel} */ +/** Tests of {@link DesktopModeWindowDecorViewModel} */ @SmallTest -public class CaptionWindowDecorViewModelTests extends ShellTestCase { +public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { - private static final String TAG = "CaptionWindowDecorViewModelTests"; + private static final String TAG = "DesktopModeWindowDecorViewModelTests"; - @Mock private CaptionWindowDecoration mCaptionWindowDecoration; - @Mock private CaptionWindowDecoration.Factory mCaptionWindowDecorFactory; + @Mock private DesktopModeWindowDecoration mDesktopModeWindowDecoration; + @Mock private DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory; @Mock private Handler mMainHandler; @Mock private Choreographer mMainChoreographer; @@ -79,17 +79,17 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { @Mock private InputMonitor mInputMonitor; @Mock private InputManager mInputManager; - @Mock private CaptionWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory; + @Mock private DesktopModeWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory; private final List<InputManager> mMockInputManagers = new ArrayList<>(); - private CaptionWindowDecorViewModel mCaptionWindowDecorViewModel; + private DesktopModeWindowDecorViewModel mDesktopModeWindowDecorViewModel; @Before public void setUp() { mMockInputManagers.add(mInputManager); - mCaptionWindowDecorViewModel = - new CaptionWindowDecorViewModel( + mDesktopModeWindowDecorViewModel = + new DesktopModeWindowDecorViewModel( mContext, mMainHandler, mMainChoreographer, @@ -98,12 +98,12 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { mSyncQueue, Optional.of(mDesktopModeController), Optional.of(mDesktopTasksController), - mCaptionWindowDecorFactory, + mDesktopModeWindowDecorFactory, mMockInputMonitorFactory ); - doReturn(mCaptionWindowDecoration) - .when(mCaptionWindowDecorFactory) + doReturn(mDesktopModeWindowDecoration) + .when(mDesktopModeWindowDecorFactory) .create(any(), any(), any(), any(), any(), any(), any(), any()); when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor); @@ -123,13 +123,15 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); + mDesktopModeWindowDecorViewModel.onTaskOpening( + taskInfo, surfaceControl, startT, finishT); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED); taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); - mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + mDesktopModeWindowDecorViewModel.onTaskChanging( + taskInfo, surfaceControl, startT, finishT); }); - verify(mCaptionWindowDecorFactory) + verify(mDesktopModeWindowDecorFactory) .create( mContext, mDisplayController, @@ -139,7 +141,7 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { mMainHandler, mMainChoreographer, mSyncQueue); - verify(mCaptionWindowDecoration).close(); + verify(mDesktopModeWindowDecoration).close(); } @Test @@ -153,14 +155,16 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); - mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + mDesktopModeWindowDecorViewModel.onTaskChanging( + taskInfo, surfaceControl, startT, finishT); taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); - mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + mDesktopModeWindowDecorViewModel.onTaskChanging( + taskInfo, surfaceControl, startT, finishT); }); - verify(mCaptionWindowDecorFactory, times(1)) + verify(mDesktopModeWindowDecorFactory, times(1)) .create( mContext, mDisplayController, @@ -183,9 +187,10 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); + mDesktopModeWindowDecorViewModel.onTaskOpening( + taskInfo, surfaceControl, startT, finishT); - mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo); + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo); }); verify(mMockInputMonitorFactory).create(any(), any()); verify(mInputMonitor).dispose(); @@ -214,13 +219,14 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); - mCaptionWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl, + mDesktopModeWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, + finishT); + mDesktopModeWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl, startT, finishT); - mCaptionWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl, + mDesktopModeWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl, startT, finishT); - mCaptionWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo); - mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo); + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo); + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo); }); verify(mMockInputMonitorFactory, times(2)).create(any(), any()); verify(mInputMonitor, times(1)).dispose(); diff --git a/libs/hwui/AndroidTest.xml b/libs/hwui/AndroidTest.xml index 911315f81a8a..75f61f5f7f9d 100644 --- a/libs/hwui/AndroidTest.xml +++ b/libs/hwui/AndroidTest.xml @@ -21,6 +21,7 @@ <option name="push" value="hwuimacro->/data/local/tmp/benchmarktest/hwuimacro" /> </target_preparer> <option name="test-suite-tag" value="apct" /> + <option name="not-shardable" value="true" /> <test class="com.android.tradefed.testtype.GTest" > <option name="native-test-device-path" value="/data/local/tmp/nativetest" /> <option name="module-name" value="hwui_unit_tests" /> diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp index 6a3bc8fe1152..c8358497ad62 100644 --- a/libs/hwui/jni/Graphics.cpp +++ b/libs/hwui/jni/Graphics.cpp @@ -576,14 +576,22 @@ jobject GraphicsJNI::getColorSpace(JNIEnv* env, SkColorSpace* decodeColorSpace, LOG_ALWAYS_FATAL_IF(!decodeColorSpace->toXYZD50(&xyzMatrix)); skcms_TransferFunction transferParams; - // We can only handle numerical transfer functions at the moment - LOG_ALWAYS_FATAL_IF(!decodeColorSpace->isNumericalTransferFn(&transferParams)); - - jobject params = env->NewObject(gTransferParameters_class, - gTransferParameters_constructorMethodID, - transferParams.a, transferParams.b, transferParams.c, - transferParams.d, transferParams.e, transferParams.f, - transferParams.g); + decodeColorSpace->transferFn(&transferParams); + auto res = skcms_TransferFunction_getType(&transferParams); + LOG_ALWAYS_FATAL_IF(res == skcms_TFType_HLGinvish || res == skcms_TFType_Invalid); + + jobject params; + if (res == skcms_TFType_PQish || res == skcms_TFType_HLGish) { + params = env->NewObject(gTransferParameters_class, gTransferParameters_constructorMethodID, + transferParams.a, transferParams.b, transferParams.c, + transferParams.d, transferParams.e, transferParams.f, + transferParams.g, true); + } else { + params = env->NewObject(gTransferParameters_class, gTransferParameters_constructorMethodID, + transferParams.a, transferParams.b, transferParams.c, + transferParams.d, transferParams.e, transferParams.f, + transferParams.g, false); + } jfloatArray xyzArray = env->NewFloatArray(9); jfloat xyz[9] = { @@ -808,8 +816,8 @@ int register_android_graphics_Graphics(JNIEnv* env) gTransferParameters_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/graphics/ColorSpace$Rgb$TransferParameters")); - gTransferParameters_constructorMethodID = GetMethodIDOrDie(env, gTransferParameters_class, - "<init>", "(DDDDDDD)V"); + gTransferParameters_constructorMethodID = + GetMethodIDOrDie(env, gTransferParameters_class, "<init>", "(DDDDDDDZ)V"); gFontMetrics_class = FindClassOrDie(env, "android/graphics/Paint$FontMetrics"); gFontMetrics_class = MakeGlobalRefOrDie(env, gFontMetrics_class); diff --git a/libs/hwui/jni/Mesh.cpp b/libs/hwui/jni/Mesh.cpp index 7b9a93fe0fba..3aac48dd08b1 100644 --- a/libs/hwui/jni/Mesh.cpp +++ b/libs/hwui/jni/Mesh.cpp @@ -44,10 +44,16 @@ static jlong make(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobject verte sk_sp<SkMesh::VertexBuffer> skVertexBuffer = genVertexBuffer(env, vertexBuffer, vertexCount * skMeshSpec->stride(), isDirect); auto skRect = SkRect::MakeLTRB(left, top, right, bottom); - auto mesh = SkMesh::Make(skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount, - vertexOffset, nullptr, skRect) - .mesh; - auto meshPtr = std::make_unique<MeshWrapper>(MeshWrapper{mesh, MeshUniformBuilder(skMeshSpec)}); + auto meshResult = SkMesh::Make( + skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount, vertexOffset, + SkData::MakeWithCopy(skMeshSpec->uniforms().data(), skMeshSpec->uniformSize()), skRect); + + if (!meshResult.error.isEmpty()) { + jniThrowException(env, "java/lang/IllegalArgumentException", meshResult.error.c_str()); + } + + auto meshPtr = std::make_unique<MeshWrapper>( + MeshWrapper{meshResult.mesh, MeshUniformBuilder(skMeshSpec)}); return reinterpret_cast<jlong>(meshPtr.release()); } @@ -61,11 +67,17 @@ static jlong makeIndexed(JNIEnv* env, jobject, jlong meshSpec, jint mode, jobjec sk_sp<SkMesh::IndexBuffer> skIndexBuffer = genIndexBuffer(env, indexBuffer, indexCount * gIndexByteSize, isIndexDirect); auto skRect = SkRect::MakeLTRB(left, top, right, bottom); - auto mesh = SkMesh::MakeIndexed(skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount, - vertexOffset, skIndexBuffer, indexCount, indexOffset, nullptr, - skRect) - .mesh; - auto meshPtr = std::make_unique<MeshWrapper>(MeshWrapper{mesh, MeshUniformBuilder(skMeshSpec)}); + + auto meshResult = SkMesh::MakeIndexed( + skMeshSpec, SkMesh::Mode(mode), skVertexBuffer, vertexCount, vertexOffset, + skIndexBuffer, indexCount, indexOffset, + SkData::MakeWithCopy(skMeshSpec->uniforms().data(), skMeshSpec->uniformSize()), skRect); + + if (!meshResult.error.isEmpty()) { + jniThrowException(env, "java/lang/IllegalArgumentException", meshResult.error.c_str()); + } + auto meshPtr = std::make_unique<MeshWrapper>( + MeshWrapper{meshResult.mesh, MeshUniformBuilder(skMeshSpec)}); return reinterpret_cast<jlong>(meshPtr.release()); } @@ -139,22 +151,22 @@ static void nativeUpdateFloatUniforms(JNIEnv* env, MeshUniformBuilder* builder, } } -static void updateFloatUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring uniformName, +static void updateFloatUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName, jfloat value1, jfloat value2, jfloat value3, jfloat value4, jint count) { - auto* builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder); + auto* wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper); ScopedUtfChars name(env, uniformName); const float values[4] = {value1, value2, value3, value4}; - nativeUpdateFloatUniforms(env, builder, name.c_str(), values, count, false); + nativeUpdateFloatUniforms(env, &wrapper->builder, name.c_str(), values, count, false); } -static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring jUniformName, +static void updateFloatArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring jUniformName, jfloatArray jvalues, jboolean isColor) { - auto builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder); + auto wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper); ScopedUtfChars name(env, jUniformName); AutoJavaFloatArray autoValues(env, jvalues, 0, kRO_JNIAccess); - nativeUpdateFloatUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length(), - isColor); + nativeUpdateFloatUniforms(env, &wrapper->builder, name.c_str(), autoValues.ptr(), + autoValues.length(), isColor); } static void nativeUpdateIntUniforms(JNIEnv* env, MeshUniformBuilder* builder, @@ -171,20 +183,21 @@ static void nativeUpdateIntUniforms(JNIEnv* env, MeshUniformBuilder* builder, } } -static void updateIntUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring uniformName, +static void updateIntUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName, jint value1, jint value2, jint value3, jint value4, jint count) { - auto builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder); + auto wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper); ScopedUtfChars name(env, uniformName); const int values[4] = {value1, value2, value3, value4}; - nativeUpdateIntUniforms(env, builder, name.c_str(), values, count); + nativeUpdateIntUniforms(env, &wrapper->builder, name.c_str(), values, count); } -static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong uniBuilder, jstring uniformName, +static void updateIntArrayUniforms(JNIEnv* env, jobject, jlong meshWrapper, jstring uniformName, jintArray values) { - auto builder = reinterpret_cast<MeshUniformBuilder*>(uniBuilder); + auto wrapper = reinterpret_cast<MeshWrapper*>(meshWrapper); ScopedUtfChars name(env, uniformName); AutoJavaIntArray autoValues(env, values, 0); - nativeUpdateIntUniforms(env, builder, name.c_str(), autoValues.ptr(), autoValues.length()); + nativeUpdateIntUniforms(env, &wrapper->builder, name.c_str(), autoValues.ptr(), + autoValues.length()); } static void MeshWrapper_destroy(MeshWrapper* wrapper) { diff --git a/libs/hwui/jni/Mesh.h b/libs/hwui/jni/Mesh.h index aa014a5e4f61..7a73f2d5c470 100644 --- a/libs/hwui/jni/Mesh.h +++ b/libs/hwui/jni/Mesh.h @@ -239,6 +239,7 @@ public: explicit MeshUniformBuilder(sk_sp<SkMeshSpecification> meshSpec) { fMeshSpec = sk_sp(meshSpec); + fUniforms = (SkData::MakeZeroInitialized(meshSpec->uniformSize())); } sk_sp<SkData> fUniforms; diff --git a/libs/hwui/jni/MeshSpecification.cpp b/libs/hwui/jni/MeshSpecification.cpp index 619a3ed552d8..ae9792df3d82 100644 --- a/libs/hwui/jni/MeshSpecification.cpp +++ b/libs/hwui/jni/MeshSpecification.cpp @@ -78,7 +78,6 @@ static jlong Make(JNIEnv* env, jobject thiz, jobjectArray attributeArray, jint v auto meshSpecResult = SkMeshSpecification::Make(attributes, vertexStride, varyings, SkString(skVertexShader.c_str()), SkString(skFragmentShader.c_str())); - if (meshSpecResult.specification.get() == nullptr) { jniThrowException(env, "java/lang/IllegalArgumentException", meshSpecResult.error.c_str()); } diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 64839d0147f8..78ae5cf3d93b 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -475,6 +475,7 @@ void CanvasContext::stopDrawing() { void CanvasContext::notifyFramePending() { ATRACE_CALL(); mRenderThread.pushBackFrameCallback(this); + sendLoadResetHint(); } void CanvasContext::draw() { diff --git a/location/java/android/location/GnssMeasurementsEvent.java b/location/java/android/location/GnssMeasurementsEvent.java index a8b0dc235b43..8b96974f4d2e 100644 --- a/location/java/android/location/GnssMeasurementsEvent.java +++ b/location/java/android/location/GnssMeasurementsEvent.java @@ -37,11 +37,14 @@ import java.util.List; * Events are delivered to registered instances of {@link Callback}. */ public final class GnssMeasurementsEvent implements Parcelable { + private final int mFlag; private final GnssClock mClock; private final List<GnssMeasurement> mMeasurements; private final List<GnssAutomaticGainControl> mGnssAgcs; private final boolean mIsFullTracking; + private static final int HAS_FULL_TRACKING = 1; + /** * Used for receiving GNSS satellite measurements from the GNSS engine. * Each measurement contains raw and computed data identifying a satellite. @@ -123,10 +126,12 @@ public final class GnssMeasurementsEvent implements Parcelable { /** * Create a {@link GnssMeasurementsEvent} instance with a full list of parameters. */ - private GnssMeasurementsEvent(@NonNull GnssClock clock, + private GnssMeasurementsEvent(int flag, + @NonNull GnssClock clock, @NonNull List<GnssMeasurement> measurements, @NonNull List<GnssAutomaticGainControl> agcs, boolean isFullTracking) { + mFlag = flag; mMeasurements = measurements; mGnssAgcs = agcs; mClock = clock; @@ -168,22 +173,32 @@ public final class GnssMeasurementsEvent implements Parcelable { * * False indicates that the GNSS chipset may optimize power via duty cycling, constellations and * frequency limits, etc. + * + * <p>The value is only available if {@link #hasFullTracking()} is {@code true}. */ - public boolean getIsFullTracking() { + public boolean isFullTracking() { return mIsFullTracking; } + /** + * Return {@code true} if {@link #isFullTracking()} is available, {@code false} otherwise. + */ + public boolean hasFullTracking() { + return (mFlag & HAS_FULL_TRACKING) == HAS_FULL_TRACKING; + } + public static final @android.annotation.NonNull Creator<GnssMeasurementsEvent> CREATOR = new Creator<GnssMeasurementsEvent>() { @Override public GnssMeasurementsEvent createFromParcel(Parcel in) { + int flag = in.readInt(); GnssClock clock = in.readParcelable(getClass().getClassLoader(), android.location.GnssClock.class); List<GnssMeasurement> measurements = in.createTypedArrayList(GnssMeasurement.CREATOR); List<GnssAutomaticGainControl> agcs = in.createTypedArrayList( GnssAutomaticGainControl.CREATOR); boolean isFullTracking = in.readBoolean(); - return new GnssMeasurementsEvent(clock, measurements, agcs, isFullTracking); + return new GnssMeasurementsEvent(flag, clock, measurements, agcs, isFullTracking); } @Override @@ -199,6 +214,7 @@ public final class GnssMeasurementsEvent implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(mFlag); parcel.writeParcelable(mClock, flags); parcel.writeTypedList(mMeasurements); parcel.writeTypedList(mGnssAgcs); @@ -211,13 +227,16 @@ public final class GnssMeasurementsEvent implements Parcelable { builder.append(mClock); builder.append(' ').append(mMeasurements.toString()); builder.append(' ').append(mGnssAgcs.toString()); - builder.append(" isFullTracking=").append(mIsFullTracking); + if (hasFullTracking()) { + builder.append(" isFullTracking=").append(mIsFullTracking); + } builder.append("]"); return builder.toString(); } /** Builder for {@link GnssMeasurementsEvent} */ public static final class Builder { + private int mFlag; private GnssClock mClock; private List<GnssMeasurement> mMeasurements; private List<GnssAutomaticGainControl> mGnssAgcs; @@ -237,10 +256,11 @@ public final class GnssMeasurementsEvent implements Parcelable { * {@link GnssMeasurementsEvent}. */ public Builder(@NonNull GnssMeasurementsEvent event) { + mFlag = event.mFlag; mClock = event.getClock(); mMeasurements = (List<GnssMeasurement>) event.getMeasurements(); mGnssAgcs = (List<GnssAutomaticGainControl>) event.getGnssAutomaticGainControls(); - mIsFullTracking = event.getIsFullTracking(); + mIsFullTracking = event.isFullTracking(); } /** @@ -313,15 +333,26 @@ public final class GnssMeasurementsEvent implements Parcelable { * and frequency limits, etc. */ @NonNull - public Builder setIsFullTracking(boolean isFullTracking) { + public Builder setFullTracking(boolean isFullTracking) { + mFlag |= HAS_FULL_TRACKING; mIsFullTracking = isFullTracking; return this; } + /** + * Clears the full tracking mode indicator. + */ + @NonNull + public Builder clearFullTracking() { + mFlag &= ~HAS_FULL_TRACKING; + return this; + } + /** Builds a {@link GnssMeasurementsEvent} instance as specified by this builder. */ @NonNull public GnssMeasurementsEvent build() { - return new GnssMeasurementsEvent(mClock, mMeasurements, mGnssAgcs, mIsFullTracking); + return new GnssMeasurementsEvent(mFlag, mClock, mMeasurements, mGnssAgcs, + mIsFullTracking); } } } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index fdd623300cdb..19610a936408 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -1841,8 +1841,7 @@ public class AudioManager { * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)} * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)} * @param strategy the strategy to query - * @return the preferred device for that strategy, or null if none was ever set or if the - * strategy is invalid + * @return list of the preferred devices for that strategy */ @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) @@ -1859,6 +1858,76 @@ public class AudioManager { /** * @hide + * Set a device as non-default for a given strategy, i.e. the audio routing to be avoided by + * this audio strategy. + * <p>Use + * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)} + * to cancel setting this preference for this strategy.</p> + * @param strategy the audio strategy whose routing will be affected + * @param device the audio device to not route to when available + * @return true if the operation was successful, false otherwise + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public boolean setDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy, + @NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(strategy); + Objects.requireNonNull(device); + try { + final int status = + getService().setDeviceAsNonDefaultForStrategy(strategy.getId(), device); + return status == AudioSystem.SUCCESS; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Removes the audio device(s) from the non-default device list previously set with + * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)} + * @param strategy the audio strategy whose routing will be affected + * @param device the audio device to remove from the non-default device list + * @return true if the operation was successful, false otherwise (invalid strategy, or no + * device set for example) + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public boolean removeDeviceAsNonDefaultForStrategy(@NonNull AudioProductStrategy strategy, + @NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(strategy); + Objects.requireNonNull(device); + try { + final int status = + getService().removeDeviceAsNonDefaultForStrategy(strategy.getId(), device); + return status == AudioSystem.SUCCESS; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Gets the audio device(s) from the non-default device list previously set with + * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)} + * @param strategy the audio strategy to query + * @return list of non-default devices for the strategy + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + @NonNull + public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy( + @NonNull AudioProductStrategy strategy) { + Objects.requireNonNull(strategy); + try { + return getService().getNonDefaultDevicesForStrategy(strategy.getId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * @hide * Interface to be notified of changes in the preferred audio device set for a given audio * strategy. * <p>Note that this listener will only be invoked whenever @@ -1892,9 +1961,11 @@ public class AudioManager { * Interface to be notified of changes in the preferred audio devices set for a given audio * strategy. * <p>Note that this listener will only be invoked whenever - * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)} or - * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)} - * {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in + * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)}, + * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}, + * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}, + * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)} + * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in * preferred device(s). It will not be invoked directly after registration with * {@link #addOnPreferredDevicesForStrategyChangedListener( * Executor, OnPreferredDevicesForStrategyChangedListener)} @@ -1902,7 +1973,6 @@ public class AudioManager { * @see #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes) * @see #setPreferredDevicesForStrategy(AudioProductStrategy, List) * @see #removePreferredDeviceForStrategy(AudioProductStrategy) - * @see #getPreferredDeviceForStrategy(AudioProductStrategy) * @see #getPreferredDevicesForStrategy(AudioProductStrategy) */ @SystemApi @@ -1966,30 +2036,9 @@ public class AudioManager { throws SecurityException { Objects.requireNonNull(executor); Objects.requireNonNull(listener); - synchronized (mPrefDevListenerLock) { - if (hasPrefDevListener(listener)) { - throw new IllegalArgumentException( - "attempt to call addOnPreferredDevicesForStrategyChangedListener() " - + "on a previously registered listener"); - } - // lazy initialization of the list of strategy-preferred device listener - if (mPrefDevListeners == null) { - mPrefDevListeners = new ArrayList<>(); - } - final int oldCbCount = mPrefDevListeners.size(); - mPrefDevListeners.add(new PrefDevListenerInfo(listener, executor)); - if (oldCbCount == 0 && mPrefDevListeners.size() > 0) { - // register binder for callbacks - if (mPrefDevDispatcherStub == null) { - mPrefDevDispatcherStub = new StrategyPreferredDevicesDispatcherStub(); - } - try { - getService().registerStrategyPreferredDevicesDispatcher(mPrefDevDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - } + mPrefDevListenerMgr.addListener( + executor, listener, "addOnPreferredDevicesForStrategyChangedListener", + () -> new StrategyPreferredDevicesDispatcherStub()); } /** @@ -2002,106 +2051,145 @@ public class AudioManager { public void removeOnPreferredDevicesForStrategyChangedListener( @NonNull OnPreferredDevicesForStrategyChangedListener listener) { Objects.requireNonNull(listener); - synchronized (mPrefDevListenerLock) { - if (!removePrefDevListener(listener)) { - throw new IllegalArgumentException( - "attempt to call removeOnPreferredDeviceForStrategyChangedListener() " - + "on an unregistered listener"); - } - if (mPrefDevListeners.size() == 0) { - // unregister binder for callbacks - try { - getService().unregisterStrategyPreferredDevicesDispatcher( - mPrefDevDispatcherStub); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } finally { - mPrefDevDispatcherStub = null; - mPrefDevListeners = null; - } - } - } + mPrefDevListenerMgr.removeListener( + listener, "removeOnPreferredDevicesForStrategyChangedListener"); } + /** + * @hide + * Interface to be notified of changes in the non-default audio devices set for a given audio + * strategy. + * <p>Note that this listener will only be invoked whenever + * {@link #setPreferredDeviceForStrategy(AudioProductStrategy, AudioDeviceAttributes)}, + * {@link #setPreferredDevicesForStrategy(AudioProductStrategy, List<AudioDeviceAttributes>)}, + * {@link #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)}, + * {@link #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes)} + * or {@link #removePreferredDeviceForStrategy(AudioProductStrategy)} causes a change in + * non-default device(s). It will not be invoked directly after registration with + * {@link #addOnNonDefaultDevicesForStrategyChangedListener( + * Executor, OnNonDefaultDevicesForStrategyChangedListener)} + * to indicate which strategies had preferred devices at the time of registration.</p> + * @see #setDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes) + * @see #removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, AudioDeviceAttributes) + */ + @SystemApi + public interface OnNonDefaultDevicesForStrategyChangedListener { + /** + * Called on the listener to indicate that the non-default audio devices for the given + * strategy has changed. + * @param strategy the {@link AudioProductStrategy} whose non-default device changed + * @param devices a list of newly set non-default audio devices + */ + void onNonDefaultDevicesForStrategyChanged(@NonNull AudioProductStrategy strategy, + @NonNull List<AudioDeviceAttributes> devices); + } - private final Object mPrefDevListenerLock = new Object(); /** - * List of listeners for preferred device for strategy and their associated Executor. - * List is lazy-initialized on first registration + * @hide + * Adds a listener for being notified of changes to the non-default audio devices for + * strategies. + * @param executor + * @param listener + * @throws SecurityException if the caller doesn't hold the required permission */ - @GuardedBy("mPrefDevListenerLock") - private @Nullable ArrayList<PrefDevListenerInfo> mPrefDevListeners; + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void addOnNonDefaultDevicesForStrategyChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnNonDefaultDevicesForStrategyChangedListener listener) + throws SecurityException { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); - private static class PrefDevListenerInfo { - final @NonNull OnPreferredDevicesForStrategyChangedListener mListener; - final @NonNull Executor mExecutor; - PrefDevListenerInfo(OnPreferredDevicesForStrategyChangedListener listener, Executor exe) { - mListener = listener; - mExecutor = exe; - } + mNonDefDevListenerMgr.addListener( + executor, listener, "addOnNonDefaultDevicesForStrategyChangedListener", + () -> new StrategyNonDefaultDevicesDispatcherStub()); + } + + /** + * @hide + * Removes a previously added listener of changes to the non-default audio device for + * strategies. + * @param listener + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void removeOnNonDefaultDevicesForStrategyChangedListener( + @NonNull OnNonDefaultDevicesForStrategyChangedListener listener) { + Objects.requireNonNull(listener); + mNonDefDevListenerMgr.removeListener( + listener, "removeOnNonDefaultDevicesForStrategyChangedListener"); } - @GuardedBy("mPrefDevListenerLock") - private StrategyPreferredDevicesDispatcherStub mPrefDevDispatcherStub; + /** + * Manages the OnPreferredDevicesForStrategyChangedListener listeners and the + * StrategyPreferredDevicesDispatcherStub + */ + private final CallbackUtil.LazyListenerManager<OnPreferredDevicesForStrategyChangedListener> + mPrefDevListenerMgr = new CallbackUtil.LazyListenerManager(); + + /** + * Manages the OnNonDefaultDevicesForStrategyChangedListener listeners and the + * StrategyNonDefaultDevicesDispatcherStub + */ + private final CallbackUtil.LazyListenerManager<OnNonDefaultDevicesForStrategyChangedListener> + mNonDefDevListenerMgr = new CallbackUtil.LazyListenerManager(); private final class StrategyPreferredDevicesDispatcherStub - extends IStrategyPreferredDevicesDispatcher.Stub { + extends IStrategyPreferredDevicesDispatcher.Stub + implements CallbackUtil.DispatcherStub { @Override public void dispatchPrefDevicesChanged(int strategyId, @NonNull List<AudioDeviceAttributes> devices) { - // make a shallow copy of listeners so callback is not executed under lock - final ArrayList<PrefDevListenerInfo> prefDevListeners; - synchronized (mPrefDevListenerLock) { - if (mPrefDevListeners == null || mPrefDevListeners.size() == 0) { - return; - } - prefDevListeners = (ArrayList<PrefDevListenerInfo>) mPrefDevListeners.clone(); - } final AudioProductStrategy strategy = AudioProductStrategy.getAudioProductStrategyWithId(strategyId); - final long ident = Binder.clearCallingIdentity(); + + mPrefDevListenerMgr.callListeners( + (listener) -> listener.onPreferredDevicesForStrategyChanged(strategy, devices)); + } + + @Override + public void register(boolean register) { try { - for (PrefDevListenerInfo info : prefDevListeners) { - info.mExecutor.execute(() -> - info.mListener.onPreferredDevicesForStrategyChanged(strategy, devices)); + if (register) { + getService().registerStrategyPreferredDevicesDispatcher(this); + } else { + getService().unregisterStrategyPreferredDevicesDispatcher(this); } - } finally { - Binder.restoreCallingIdentity(ident); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); } } } - @GuardedBy("mPrefDevListenerLock") - private @Nullable PrefDevListenerInfo getPrefDevListenerInfo( - OnPreferredDevicesForStrategyChangedListener listener) { - if (mPrefDevListeners == null) { - return null; - } - for (PrefDevListenerInfo info : mPrefDevListeners) { - if (info.mListener == listener) { - return info; - } - } - return null; - } + private final class StrategyNonDefaultDevicesDispatcherStub + extends IStrategyNonDefaultDevicesDispatcher.Stub + implements CallbackUtil.DispatcherStub { - @GuardedBy("mPrefDevListenerLock") - private boolean hasPrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) { - return getPrefDevListenerInfo(listener) != null; - } + @Override + public void dispatchNonDefDevicesChanged(int strategyId, + @NonNull List<AudioDeviceAttributes> devices) { + final AudioProductStrategy strategy = + AudioProductStrategy.getAudioProductStrategyWithId(strategyId); - @GuardedBy("mPrefDevListenerLock") - /** - * @return true if the listener was removed from the list - */ - private boolean removePrefDevListener(OnPreferredDevicesForStrategyChangedListener listener) { - final PrefDevListenerInfo infoToRemove = getPrefDevListenerInfo(listener); - if (infoToRemove != null) { - mPrefDevListeners.remove(infoToRemove); - return true; + mNonDefDevListenerMgr.callListeners( + (listener) -> listener.onNonDefaultDevicesForStrategyChanged( + strategy, devices)); + } + + @Override + public void register(boolean register) { + try { + if (register) { + getService().registerStrategyNonDefaultDevicesDispatcher(this); + } else { + getService().unregisterStrategyNonDefaultDevicesDispatcher(this); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } } - return false; } //==================================================================== diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index a7435868f98e..9339c3d69ba2 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -2063,12 +2063,46 @@ public class AudioSystem /** * @hide + * Remove device as role for product strategy. + * @param strategy the id of the strategy to configure + * @param role the role of the devices + * @param devices the list of devices to be removed as role for the given strategy + * @return {@link #SUCCESS} if successfully set + */ + public static int removeDevicesRoleForStrategy( + int strategy, int role, @NonNull List<AudioDeviceAttributes> devices) { + if (devices.isEmpty()) { + return BAD_VALUE; + } + int[] types = new int[devices.size()]; + String[] addresses = new String[devices.size()]; + for (int i = 0; i < devices.size(); ++i) { + types[i] = devices.get(i).getInternalType(); + addresses[i] = devices.get(i).getAddress(); + } + return removeDevicesRoleForStrategy(strategy, role, types, addresses); + } + + /** + * @hide * Remove devices as role for the strategy * @param strategy the id of the strategy to configure * @param role the role of the devices + * @param types all device types + * @param addresses all device addresses + * @return {@link #SUCCESS} if successfully removed + */ + public static native int removeDevicesRoleForStrategy( + int strategy, int role, @NonNull int[] types, @NonNull String[] addresses); + + /** + * @hide + * Remove all devices as role for the strategy + * @param strategy the id of the strategy to configure + * @param role the role of the devices * @return {@link #SUCCESS} if successfully removed */ - public static native int removeDevicesRoleForStrategy(int strategy, int role); + public static native int clearDevicesRoleForStrategy(int strategy, int role); /** * @hide diff --git a/media/java/android/media/CallbackUtil.java b/media/java/android/media/CallbackUtil.java index 2b5fd25c49a9..f0280dae865e 100644 --- a/media/java/android/media/CallbackUtil.java +++ b/media/java/android/media/CallbackUtil.java @@ -183,7 +183,7 @@ import java.util.concurrent.Executor; if (!removeListener(listener, listeners)) { throw new IllegalArgumentException("attempt to call " + methodName - + "on an unregistered listener"); + + " on an unregistered listener"); } if (listeners.size() == 0) { unregisterStub.accept(dispatchStub); diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 0f63cc40ca36..4b36237093c0 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -42,6 +42,7 @@ import android.media.IPreferredMixerAttributesDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.IStrategyPreferredDevicesDispatcher; +import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.ISpatializerCallback; import android.media.ISpatializerHeadTrackerAvailableCallback; import android.media.ISpatializerHeadTrackingModeCallback; @@ -330,7 +331,8 @@ interface IAudioService { boolean isCallScreeningModeSupported(); - int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> device); + @EnforcePermission("MODIFY_AUDIO_ROUTING") + int setPreferredDevicesForStrategy(in int strategy, in List<AudioDeviceAttributes> devices); @EnforcePermission("MODIFY_AUDIO_ROUTING") int removePreferredDevicesForStrategy(in int strategy); @@ -338,6 +340,15 @@ interface IAudioService { @EnforcePermission("MODIFY_AUDIO_ROUTING") List<AudioDeviceAttributes> getPreferredDevicesForStrategy(in int strategy); + @EnforcePermission("MODIFY_AUDIO_ROUTING") + int setDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device); + + @EnforcePermission("MODIFY_AUDIO_ROUTING") + int removeDeviceAsNonDefaultForStrategy(in int strategy, in AudioDeviceAttributes device); + + @EnforcePermission("MODIFY_AUDIO_ROUTING") + List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(in int strategy); + List<AudioDeviceAttributes> getDevicesForAttributes(in AudioAttributes attributes); List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes); @@ -351,6 +362,12 @@ interface IAudioService { oneway void unregisterStrategyPreferredDevicesDispatcher( IStrategyPreferredDevicesDispatcher dispatcher); + void registerStrategyNonDefaultDevicesDispatcher( + IStrategyNonDefaultDevicesDispatcher dispatcher); + + oneway void unregisterStrategyNonDefaultDevicesDispatcher( + IStrategyNonDefaultDevicesDispatcher dispatcher); + oneway void setRttEnabled(in boolean rttEnabled); @EnforcePermission("MODIFY_AUDIO_ROUTING") diff --git a/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl new file mode 100644 index 000000000000..59239cb9aae1 --- /dev/null +++ b/media/java/android/media/IStrategyNonDefaultDevicesDispatcher.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media; + +import android.media.AudioDeviceAttributes; + +/** + * AIDL for AudioService to signal non-daefault devices updates for audio strategies. + * + * {@hide} + */ +oneway interface IStrategyNonDefaultDevicesDispatcher { + + void dispatchNonDefDevicesChanged(int strategyId, in List<AudioDeviceAttributes> devices); + +} + diff --git a/media/java/android/media/RouteListingPreference.java b/media/java/android/media/RouteListingPreference.java index d74df7ad31d8..6a5b29055cfe 100644 --- a/media/java/android/media/RouteListingPreference.java +++ b/media/java/android/media/RouteListingPreference.java @@ -17,6 +17,7 @@ package android.media; import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; @@ -198,19 +199,22 @@ public final class RouteListingPreference implements Parcelable { @NonNull private final String mRouteId; @Flags private final int mFlags; @DisableReason private final int mDisableReason; + private final int mSessionParticipantCount; private Item(@NonNull Builder builder) { mRouteId = builder.mRouteId; mFlags = builder.mFlags; mDisableReason = builder.mDisableReason; + mSessionParticipantCount = builder.mSessionParticipantCount; } private Item(Parcel in) { - String routeId = in.readString(); - Preconditions.checkArgument(!TextUtils.isEmpty(routeId)); - mRouteId = routeId; + mRouteId = in.readString(); + Preconditions.checkArgument(!TextUtils.isEmpty(mRouteId)); mFlags = in.readInt(); mDisableReason = in.readInt(); + mSessionParticipantCount = in.readInt(); + Preconditions.checkArgument(mSessionParticipantCount >= 0); } /** Returns the id of the route that corresponds to this route listing preference item. */ @@ -244,6 +248,17 @@ public final class RouteListingPreference implements Parcelable { return mDisableReason; } + /** + * Returns a non-negative number of participants in the ongoing session (if any) on the + * corresponding route. + * + * <p>The system ignores this value if zero, or if {@link #getFlags()} does not include + * {@link #FLAG_ONGOING_SESSION}. + */ + public int getSessionParticipantCount() { + return mSessionParticipantCount; + } + // Item Parcelable implementation. @Override @@ -256,6 +271,7 @@ public final class RouteListingPreference implements Parcelable { dest.writeString(mRouteId); dest.writeInt(mFlags); dest.writeInt(mDisableReason); + dest.writeInt(mSessionParticipantCount); } // Equals and hashCode. @@ -271,12 +287,13 @@ public final class RouteListingPreference implements Parcelable { Item item = (Item) other; return mRouteId.equals(item.mRouteId) && mFlags == item.mFlags - && mDisableReason == item.mDisableReason; + && mDisableReason == item.mDisableReason + && mSessionParticipantCount == item.mSessionParticipantCount; } @Override public int hashCode() { - return Objects.hash(mRouteId, mFlags, mDisableReason); + return Objects.hash(mRouteId, mFlags, mDisableReason, mSessionParticipantCount); } /** Builder for {@link Item}. */ @@ -285,6 +302,7 @@ public final class RouteListingPreference implements Parcelable { private final String mRouteId; private int mFlags; private int mDisableReason; + private int mSessionParticipantCount; /** * Constructor. @@ -311,6 +329,17 @@ public final class RouteListingPreference implements Parcelable { return this; } + /** See {@link Item#getSessionParticipantCount()}. */ + @NonNull + public Builder setSessionParticipantCount( + @IntRange(from = 0) int sessionParticipantCount) { + Preconditions.checkArgument( + sessionParticipantCount >= 0, + "sessionParticipantCount must be non-negative."); + mSessionParticipantCount = sessionParticipantCount; + return this; + } + /** Creates and returns a new {@link Item} with the given parameters. */ @NonNull public Item build() { diff --git a/media/java/android/media/tv/TvInputInfo.java b/media/java/android/media/tv/TvInputInfo.java index e60d5378f88c..667a9aef59f3 100644 --- a/media/java/android/media/tv/TvInputInfo.java +++ b/media/java/android/media/tv/TvInputInfo.java @@ -946,6 +946,10 @@ public final class TvInputInfo implements Parcelable { id = generateInputId(componentName, mTvInputHardwareInfo); type = sHardwareTypeToTvInputType.get(mTvInputHardwareInfo.getType(), TYPE_TUNER); isHardwareInput = true; + if (mTvInputHardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_HDMI) { + mHdmiDeviceInfo = HdmiDeviceInfo.hardwarePort( + HdmiDeviceInfo.PATH_INVALID, mTvInputHardwareInfo.getHdmiPortId()); + } } else { id = generateInputId(componentName); type = TYPE_TUNER; diff --git a/media/java/android/media/tv/TvRecordingInfo.aidl b/media/java/android/media/tv/TvRecordingInfo.aidl new file mode 100644 index 000000000000..64cf3c33840b --- /dev/null +++ b/media/java/android/media/tv/TvRecordingInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv; + +parcelable TvRecordingInfo;
\ No newline at end of file diff --git a/media/java/android/media/tv/TvRecordingInfo.java b/media/java/android/media/tv/TvRecordingInfo.java new file mode 100644 index 000000000000..8de42f32a75d --- /dev/null +++ b/media/java/android/media/tv/TvRecordingInfo.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.tv; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +/** + @hide + */ +public final class TvRecordingInfo implements Parcelable { + /* + * Indicates that getTvRecordingInfoList should return scheduled recordings. + */ + public static final int RECORDING_SCHEDULED = 1; + /* + * Indicates that getTvRecordingInfoList should return in-progress recordings. + */ + public static final int RECORDING_IN_PROGRESS = 2; + /* + * Indicates that getTvRecordingInfoList should return all recordings. + */ + public static final int RECORDING_ALL = 3; + /** + * Indicates which recordings should be returned by getTvRecordingList + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "RECORDING_" }, value = { + RECORDING_SCHEDULED, + RECORDING_IN_PROGRESS, + RECORDING_ALL, + }) + public @interface TvRecordingListType {} + + private String mRecordingId; + private int mStartPadding; + private int mEndPadding; + private int mRepeatDays; + private String mName; + private String mDescription; + private int mScheduledStartTime; + private int mScheduledDuration; + private Uri mChannelUri; + private Uri mProgramId; + private List<String> mParentalRatings; + private String mRecordingUri; + private int mRecordingStartTime; + private int mRecordingDuration; + + public TvRecordingInfo( + @NonNull String recordingId, @NonNull int startPadding, @NonNull int endPadding, + @NonNull int repeatDays, @NonNull int scheduledStartTime, + @NonNull int scheduledDuration, @NonNull Uri channelUri, @Nullable Uri programId, + @NonNull List<String> parentalRatings, @NonNull String recordingUri, + @NonNull int recordingStartTime, @NonNull int recordingDuration) { + mRecordingId = recordingId; + mStartPadding = startPadding; + mEndPadding = endPadding; + mRepeatDays = repeatDays; + mScheduledStartTime = scheduledStartTime; + mScheduledDuration = scheduledDuration; + mChannelUri = channelUri; + mScheduledDuration = scheduledDuration; + mChannelUri = channelUri; + mProgramId = programId; + mParentalRatings = parentalRatings; + mRecordingUri = recordingUri; + mRecordingStartTime = recordingStartTime; + mRecordingDuration = recordingDuration; + } + @NonNull + public String getRecordingId() { + return mRecordingId; + } + @NonNull + public int getStartPadding() { + return mStartPadding; + } + @NonNull + public int getEndPadding() { + return mEndPadding; + } + @NonNull + public int getRepeatDays() { + return mRepeatDays; + } + @NonNull + public String getName() { + return mName; + } + @NonNull + public void setName(String name) { + mName = name; + } + @NonNull + public String getDescription() { + return mDescription; + } + @NonNull + public void setDescription(String description) { + mDescription = description; + } + @NonNull + public int getScheduledStartTime() { + return mScheduledStartTime; + } + @NonNull + public int getScheduledDuration() { + return mScheduledDuration; + } + @NonNull + public Uri getChannelUri() { + return mChannelUri; + } + @Nullable + public Uri getProgramId() { + return mProgramId; + } + @NonNull + public List<String> getParentalRatings() { + return mParentalRatings; + } + @NonNull + public String getRecordingUri() { + return mRecordingUri; + } + @NonNull + public int getRecordingStartTime() { + return mRecordingStartTime; + } + @NonNull + public int getRecordingDuration() { + return mRecordingDuration; + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Used to package this object into a {@link Parcel}. + * + * @param dest The {@link Parcel} to be written. + * @param flags The flags used for parceling. + */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mRecordingId); + dest.writeInt(mStartPadding); + dest.writeInt(mEndPadding); + dest.writeInt(mRepeatDays); + dest.writeString(mName); + dest.writeString(mDescription); + dest.writeInt(mScheduledStartTime); + dest.writeInt(mScheduledDuration); + dest.writeString(mChannelUri == null ? null : mChannelUri.toString()); + dest.writeString(mProgramId == null ? null : mProgramId.toString()); + dest.writeStringList(mParentalRatings); + dest.writeString(mRecordingUri); + dest.writeInt(mRecordingDuration); + dest.writeInt(mRecordingStartTime); + } + + private TvRecordingInfo(Parcel in) { + mRecordingId = in.readString(); + mStartPadding = in.readInt(); + mEndPadding = in.readInt(); + mRepeatDays = in.readInt(); + mName = in.readString(); + mDescription = in.readString(); + mScheduledStartTime = in.readInt(); + mScheduledDuration = in.readInt(); + mChannelUri = Uri.parse(in.readString()); + mProgramId = Uri.parse(in.readString()); + in.readStringList(mParentalRatings); + mRecordingUri = in.readString(); + mRecordingDuration = in.readInt(); + mRecordingStartTime = in.readInt(); + } + + + public static final @android.annotation.NonNull Parcelable.Creator<TvRecordingInfo> CREATOR = + new Parcelable.Creator<>() { + @Override + @NonNull + public TvRecordingInfo createFromParcel(Parcel in) { + return new TvRecordingInfo(in); + } + + @Override + @NonNull + public TvRecordingInfo[] newArray(int size) { + return new TvRecordingInfo[size]; + } + }; +} diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index 98357fc5a883..537e71122cef 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.media.tv.AdBuffer; import android.media.tv.AdRequest; import android.media.tv.BroadcastInfoRequest; +import android.media.tv.TvRecordingInfo; import android.net.Uri; import android.os.Bundle; import android.view.InputChannel; @@ -48,6 +49,9 @@ oneway interface ITvInteractiveAppClient { void onRequestCurrentTvInputId(int seq); void onRequestStartRecording(in Uri programUri, int seq); void onRequestStopRecording(in String recordingId, int seq); + void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo, int seq); + void onRequestTvRecordingInfo(in String recordingId, int seq); + void onRequestTvRecordingInfoList(in int type, int seq); void onRequestSigning( in String id, in String algorithm, in String alias, in byte[] data, int seq); void onAdRequest(in AdRequest request, int Seq); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 8bfceee01a7f..3b272daa35ae 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -21,6 +21,7 @@ import android.media.tv.AdBuffer; import android.media.tv.AdResponse; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvTrackInfo; +import android.media.tv.TvRecordingInfo; import android.media.tv.interactive.AppLinkInfo; import android.media.tv.interactive.ITvInteractiveAppClient; import android.media.tv.interactive.ITvInteractiveAppManagerCallback; @@ -52,6 +53,9 @@ interface ITvInteractiveAppManager { void sendCurrentTvInputId(in IBinder sessionToken, in String inputId, int userId); void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result, int userId); + void sendTvRecordingInfo(in IBinder sessionToken, in TvRecordingInfo recordingInfo, int userId); + void sendTvRecordingInfoList(in IBinder sessionToken, + in List<TvRecordingInfo> recordingInfoList, int userId); void notifyError(in IBinder sessionToken, in String errMsg, in Bundle params, int userId); void createSession(in ITvInteractiveAppClient client, in String iAppServiceId, int type, int seq, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index 1953117b44b2..bc09cea1f24a 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -23,6 +23,7 @@ import android.media.tv.AdBuffer; import android.media.tv.AdResponse; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvTrackInfo; +import android.media.tv.TvRecordingInfo; import android.os.Bundle; import android.view.Surface; @@ -44,6 +45,8 @@ oneway interface ITvInteractiveAppSession { void sendTrackInfoList(in List<TvTrackInfo> tracks); void sendCurrentTvInputId(in String inputId); void sendSigningResult(in String signingId, in byte[] result); + void sendTvRecordingInfo(in TvRecordingInfo recordingInfo); + void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList); void notifyError(in String errMsg, in Bundle params); void release(); void notifyTuned(in Uri channelUri); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index cd4f410ad73c..c5dbd1941c90 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.media.tv.AdBuffer; import android.media.tv.AdRequest; import android.media.tv.BroadcastInfoRequest; +import android.media.tv.TvRecordingInfo; import android.media.tv.interactive.ITvInteractiveAppSession; import android.net.Uri; import android.os.Bundle; @@ -47,6 +48,9 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestCurrentTvInputId(); void onRequestStartRecording(in Uri programUri); void onRequestStopRecording(in String recordingId); + void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo); + void onRequestTvRecordingInfo(in String recordingId); + void onRequestTvRecordingInfoList(in int type); void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data); void onAdRequest(in AdRequest request); } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index b6463265d38e..af031dc21f92 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -24,6 +24,7 @@ import android.media.tv.AdBuffer; import android.media.tv.AdResponse; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvContentRating; +import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.media.tv.interactive.TvInteractiveAppService.Session; import android.net.Uri; @@ -85,6 +86,8 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_NOTIFY_RECORDING_STARTED = 30; private static final int DO_NOTIFY_RECORDING_STOPPED = 31; private static final int DO_NOTIFY_AD_BUFFER_CONSUMED = 32; + private static final int DO_SEND_RECORDING_INFO = 33; + private static final int DO_SEND_RECORDING_INFO_LIST = 34; private final HandlerCaller mCaller; private Session mSessionImpl; @@ -168,6 +171,14 @@ public class ITvInteractiveAppSessionWrapper mSessionImpl.sendCurrentTvInputId((String) msg.obj); break; } + case DO_SEND_RECORDING_INFO: { + mSessionImpl.sendTvRecordingInfo((TvRecordingInfo) msg.obj); + break; + } + case DO_SEND_RECORDING_INFO_LIST: { + mSessionImpl.sendTvRecordingInfoList((List<TvRecordingInfo>) msg.obj); + break; + } case DO_NOTIFY_RECORDING_STARTED: { mSessionImpl.notifyRecordingStarted((String) msg.obj); break; @@ -338,6 +349,18 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_RECORDING_INFO, recordingInfo)); + } + + @Override + public void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_RECORDING_INFO_LIST, recordingInfoList)); + } + + @Override public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) { mCaller.executeOrSendMessage( mCaller.obtainMessageOO(DO_SEND_SIGNING_RESULT, signingId, result)); diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index c57efc869427..fa60b66b1348 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -30,6 +30,7 @@ import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; +import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.net.Uri; import android.os.Bundle; @@ -512,6 +513,43 @@ public final class TvInteractiveAppManager { } @Override + public void onSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo, + int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postSetTvRecordingInfo(recordingId, recordingInfo); + } + } + + @Override + public void onRequestTvRecordingInfo(String recordingId, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestTvRecordingInfo(recordingId); + } + } + + @Override + public void onRequestTvRecordingInfoList(int type, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestTvRecordingInfoList(type); + } + } + + @Override public void onRequestSigning( String id, String algorithm, String alias, byte[] data, int seq) { synchronized (mSessionCallbackRecordMap) { @@ -1072,6 +1110,30 @@ public final class TvInteractiveAppManager { } } + void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendTvRecordingInfo(mToken, recordingInfo, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendTvRecordingInfoList(mToken, recordingInfoList, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + void notifyRecordingStarted(String recordingId) { if (mToken == null) { Log.w(TAG, "The session has been already released"); @@ -1799,6 +1861,33 @@ public final class TvInteractiveAppManager { }); } + void postRequestTvRecordingInfo(String recordingId) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestTvRecordingInfo(mSession, recordingId); + } + }); + } + + void postRequestTvRecordingInfoList(int type) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestTvRecordingInfoList(mSession, type); + } + }); + } + + void postSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSetTvRecordingInfo(mSession, recordingId, recordingInfo); + } + }); + } + void postAdRequest(final AdRequest request) { mHandler.post(new Runnable() { @Override @@ -1973,6 +2062,40 @@ public final class TvInteractiveAppManager { /** * This is called when + * {@link TvInteractiveAppService.Session#setTvRecordingInfo(String, TvRecordingInfo)} is + * called. + * + * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param recordingId The recordingId of the recording which will have the info set. + * @param recordingInfo The recording info to set to the recording. + */ + public void onSetTvRecordingInfo(Session session, String recordingId, + TvRecordingInfo recordingInfo) { + } + + /** + * This is called when {@link TvInteractiveAppService.Session#requestTvRecordingInfo} is + * called. + * + * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param recordingId The recordingId of the recording to be stopped. + */ + public void onRequestTvRecordingInfo(Session session, String recordingId) { + } + + /** + * This is called when {@link TvInteractiveAppService.Session#requestTvRecordingInfoList} is + * called. + * + * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param type The type of recordings to return + */ + public void onRequestTvRecordingInfoList(Session session, + @TvRecordingInfo.TvRecordingListType int type) { + } + + /** + * This is called when * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, byte[])} is * called. * diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 4ed7ca55eef3..1fa0aaa82a86 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -38,6 +38,7 @@ import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvContentRating; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; +import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback; @@ -456,6 +457,24 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Receives requested recording info. + * + * @param recordingInfo The requested recording info. Null if recording not found. + * @hide + */ + public void onTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) { + } + + /** + * Receives requested recording info. + * + * @param recordingInfoList The requested recording info list. Null if recording not found. + * @hide + */ + public void onTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) { + } + + /** * Receives started recording's ID. * * @param recordingId The ID of the recording started. The TV app should provide and @@ -988,6 +1007,71 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Sets the recording info for the specified recording + * + * @hide + */ + @CallSuper + public void setTvRecordingInfo(@NonNull String recordingId, + @NonNull TvRecordingInfo recordingInfo) { + executeOrPostRunnableOnMainThread(() -> { + try { + if (DEBUG) { + Log.d(TAG, "setTvRecordingInfo"); + } + if (mSessionCallback != null) { + mSessionCallback.onSetTvRecordingInfo(recordingId, recordingInfo); + } + } catch (RemoteException e) { + Log.w(TAG, "error in setTvRecordingInfo", e); + } + }); + } + + /** + * Gets the recording info for the specified recording + * + * @hide + */ + @CallSuper + public void requestTvRecordingInfo(@NonNull String recordingId) { + executeOrPostRunnableOnMainThread(() -> { + try { + if (DEBUG) { + Log.d(TAG, "requestTvRecordingInfo"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestTvRecordingInfo(recordingId); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestTvRecordingInfo", e); + } + }); + } + + /** + * Gets the recording info list for the specified recording type + * + * @hide + */ + @CallSuper + public void requestTvRecordingInfoList(@NonNull @TvRecordingInfo.TvRecordingListType + int type) { + executeOrPostRunnableOnMainThread(() -> { + try { + if (DEBUG) { + Log.d(TAG, "requestTvRecordingInfoList"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestTvRecordingInfoList(type); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestTvRecordingInfoList", e); + } + }); + } + + /** * Requests signing of the given data. * * <p>This is used when the corresponding server of the broadcast-independent interactive @@ -1097,6 +1181,14 @@ public abstract class TvInteractiveAppService extends Service { onCurrentTvInputId(inputId); } + void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) { + onTvRecordingInfo(recordingInfo); + } + + void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) { + onTvRecordingInfoList(recordingInfoList); + } + void sendSigningResult(String signingId, byte[] result) { onSigningResult(signingId, result); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 11776885169c..6777d1a3c1eb 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -26,6 +26,7 @@ import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.media.tv.TvInputManager; +import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.interactive.TvInteractiveAppManager.Session; @@ -581,6 +582,36 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Sends the requested {@link android.media.tv.TvRecordingInfo}. + * + * @param recordingInfo The recording info requested {@code null} if no recording found. + * @hide + */ + public void sendTvRecordingInfo(@Nullable TvRecordingInfo recordingInfo) { + if (DEBUG) { + Log.d(TAG, "sendTvRecordingInfo"); + } + if (mSession != null) { + mSession.sendTvRecordingInfo(recordingInfo); + } + } + + /** + * Sends the requested {@link android.media.tv.TvRecordingInfo}. + * + * @param recordingInfoList The list of recording info requested. + * @hide + */ + public void sendTvRecordingInfoList(@Nullable List<TvRecordingInfo> recordingInfoList) { + if (DEBUG) { + Log.d(TAG, "sendTvRecordingInfoList"); + } + if (mSession != null) { + mSession.sendTvRecordingInfoList(recordingInfoList); + } + } + + /** * Alerts the TV interactive app that a recording has been started. * * @param recordingId The ID of the recording started. This ID is created and maintained by the diff --git a/media/java/android/media/tv/tuner/filter/Filter.java b/media/java/android/media/tv/tuner/filter/Filter.java index 8568c4349dc4..7e9443b467ef 100644 --- a/media/java/android/media/tv/tuner/filter/Filter.java +++ b/media/java/android/media/tv/tuner/filter/Filter.java @@ -154,7 +154,8 @@ public class Filter implements AutoCloseable { /** @hide */ @IntDef(prefix = "STATUS_", - value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW}) + value = {STATUS_DATA_READY, STATUS_LOW_WATER, STATUS_HIGH_WATER, STATUS_OVERFLOW, + STATUS_NO_DATA}) @Retention(RetentionPolicy.SOURCE) public @interface Status {} @@ -183,6 +184,10 @@ public class Filter implements AutoCloseable { * discarded. */ public static final int STATUS_OVERFLOW = DemuxFilterStatus.OVERFLOW; + /** + * The status of a filter that the filter buffer is empty and no filtered data is coming. + */ + public static final int STATUS_NO_DATA = DemuxFilterStatus.NO_DATA; /** @hide */ @IntDef(prefix = "SCRAMBLING_STATUS_", diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml index 976a279ea2e4..88bb30b34ede 100644 --- a/packages/InputDevices/res/xml/keyboard_layouts.xml +++ b/packages/InputDevices/res/xml/keyboard_layouts.xml @@ -1,190 +1,328 @@ <?xml version="1.0" encoding="utf-8"?> <keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android"> - <keyboard-layout android:name="keyboard_layout_english_uk" - android:label="@string/keyboard_layout_english_uk_label" - android:keyboardLayout="@raw/keyboard_layout_english_uk" /> - - <keyboard-layout android:name="keyboard_layout_english_us" - android:label="@string/keyboard_layout_english_us_label" - android:keyboardLayout="@raw/keyboard_layout_english_us" /> - - <keyboard-layout android:name="keyboard_layout_english_us_intl" - android:label="@string/keyboard_layout_english_us_intl" - android:keyboardLayout="@raw/keyboard_layout_english_us_intl" /> - - <keyboard-layout android:name="keyboard_layout_english_us_colemak" - android:label="@string/keyboard_layout_english_us_colemak_label" - android:keyboardLayout="@raw/keyboard_layout_english_us_colemak" /> - - <keyboard-layout android:name="keyboard_layout_english_us_dvorak" - android:label="@string/keyboard_layout_english_us_dvorak_label" - android:keyboardLayout="@raw/keyboard_layout_english_us_dvorak" /> - - <keyboard-layout android:name="keyboard_layout_english_us_workman" - android:label="@string/keyboard_layout_english_us_workman_label" - android:keyboardLayout="@raw/keyboard_layout_english_us_workman" /> - - <keyboard-layout android:name="keyboard_layout_german" - android:label="@string/keyboard_layout_german_label" - android:keyboardLayout="@raw/keyboard_layout_german" /> - - <keyboard-layout android:name="keyboard_layout_french" - android:label="@string/keyboard_layout_french_label" - android:keyboardLayout="@raw/keyboard_layout_french" /> - - <keyboard-layout android:name="keyboard_layout_french_ca" - android:label="@string/keyboard_layout_french_ca_label" - android:keyboardLayout="@raw/keyboard_layout_french_ca" /> - - <keyboard-layout android:name="keyboard_layout_russian" - android:label="@string/keyboard_layout_russian_label" - android:keyboardLayout="@raw/keyboard_layout_russian" /> - - <keyboard-layout android:name="keyboard_layout_russian_mac" - android:label="@string/keyboard_layout_russian_mac_label" - android:keyboardLayout="@raw/keyboard_layout_russian_mac" /> - - <keyboard-layout android:name="keyboard_layout_spanish" - android:label="@string/keyboard_layout_spanish_label" - android:keyboardLayout="@raw/keyboard_layout_spanish" /> - - <keyboard-layout android:name="keyboard_layout_swiss_french" - android:label="@string/keyboard_layout_swiss_french_label" - android:keyboardLayout="@raw/keyboard_layout_swiss_french" /> - - <keyboard-layout android:name="keyboard_layout_swiss_german" - android:label="@string/keyboard_layout_swiss_german_label" - android:keyboardLayout="@raw/keyboard_layout_swiss_german" /> - - <keyboard-layout android:name="keyboard_layout_belgian" - android:label="@string/keyboard_layout_belgian" - android:keyboardLayout="@raw/keyboard_layout_belgian" /> - - <keyboard-layout android:name="keyboard_layout_bulgarian" - android:label="@string/keyboard_layout_bulgarian" - android:keyboardLayout="@raw/keyboard_layout_bulgarian" /> - - <keyboard-layout android:name="keyboard_layout_bulgarian_phonetic" - android:label="@string/keyboard_layout_bulgarian_phonetic" - android:keyboardLayout="@raw/keyboard_layout_bulgarian_phonetic" /> - - <keyboard-layout android:name="keyboard_layout_italian" - android:label="@string/keyboard_layout_italian" - android:keyboardLayout="@raw/keyboard_layout_italian" /> - - <keyboard-layout android:name="keyboard_layout_danish" - android:label="@string/keyboard_layout_danish" - android:keyboardLayout="@raw/keyboard_layout_danish" /> - - <keyboard-layout android:name="keyboard_layout_norwegian" - android:label="@string/keyboard_layout_norwegian" - android:keyboardLayout="@raw/keyboard_layout_norwegian" /> - - <keyboard-layout android:name="keyboard_layout_swedish" - android:label="@string/keyboard_layout_swedish" - android:keyboardLayout="@raw/keyboard_layout_swedish" /> - - <keyboard-layout android:name="keyboard_layout_finnish" - android:label="@string/keyboard_layout_finnish" - android:keyboardLayout="@raw/keyboard_layout_finnish" /> - - <keyboard-layout android:name="keyboard_layout_croatian" - android:label="@string/keyboard_layout_croatian" - android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian" /> - - <keyboard-layout android:name="keyboard_layout_czech" - android:label="@string/keyboard_layout_czech" - android:keyboardLayout="@raw/keyboard_layout_czech" /> - - <keyboard-layout android:name="keyboard_layout_czech_qwerty" - android:label="@string/keyboard_layout_czech_qwerty" - android:keyboardLayout="@raw/keyboard_layout_czech_qwerty" /> - - <keyboard-layout android:name="keyboard_layout_estonian" - android:label="@string/keyboard_layout_estonian" - android:keyboardLayout="@raw/keyboard_layout_estonian" /> - - <keyboard-layout android:name="keyboard_layout_hungarian" - android:label="@string/keyboard_layout_hungarian" - android:keyboardLayout="@raw/keyboard_layout_hungarian" /> - - <keyboard-layout android:name="keyboard_layout_icelandic" - android:label="@string/keyboard_layout_icelandic" - android:keyboardLayout="@raw/keyboard_layout_icelandic" /> - - <keyboard-layout android:name="keyboard_layout_brazilian" - android:label="@string/keyboard_layout_brazilian" - android:keyboardLayout="@raw/keyboard_layout_brazilian" /> - - <keyboard-layout android:name="keyboard_layout_portuguese" - android:label="@string/keyboard_layout_portuguese" - android:keyboardLayout="@raw/keyboard_layout_portuguese" /> - - <keyboard-layout android:name="keyboard_layout_slovak" - android:label="@string/keyboard_layout_slovak" - android:keyboardLayout="@raw/keyboard_layout_slovak" /> - - <keyboard-layout android:name="keyboard_layout_slovenian" - android:label="@string/keyboard_layout_slovenian" - android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian" /> - - <keyboard-layout android:name="keyboard_layout_turkish" - android:label="@string/keyboard_layout_turkish" - android:keyboardLayout="@raw/keyboard_layout_turkish" /> - - <keyboard-layout android:name="keyboard_layout_turkish_f" - android:label="@string/keyboard_layout_turkish_f" - android:keyboardLayout="@raw/keyboard_layout_turkish_f" /> - - <keyboard-layout android:name="keyboard_layout_ukrainian" - android:label="@string/keyboard_layout_ukrainian" - android:keyboardLayout="@raw/keyboard_layout_ukrainian" /> - - <keyboard-layout android:name="keyboard_layout_arabic" - android:label="@string/keyboard_layout_arabic" - android:keyboardLayout="@raw/keyboard_layout_arabic" /> - - <keyboard-layout android:name="keyboard_layout_greek" - android:label="@string/keyboard_layout_greek" - android:keyboardLayout="@raw/keyboard_layout_greek" /> - - <keyboard-layout android:name="keyboard_layout_hebrew" - android:label="@string/keyboard_layout_hebrew" - android:keyboardLayout="@raw/keyboard_layout_hebrew" /> - - <keyboard-layout android:name="keyboard_layout_lithuanian" - android:label="@string/keyboard_layout_lithuanian" - android:keyboardLayout="@raw/keyboard_layout_lithuanian" /> - - <keyboard-layout android:name="keyboard_layout_spanish_latin" - android:label="@string/keyboard_layout_spanish_latin" - android:keyboardLayout="@raw/keyboard_layout_spanish_latin" /> - - <keyboard-layout android:name="keyboard_layout_latvian" - android:label="@string/keyboard_layout_latvian" - android:keyboardLayout="@raw/keyboard_layout_latvian_qwerty" /> - - <keyboard-layout android:name="keyboard_layout_persian" - android:label="@string/keyboard_layout_persian" - android:keyboardLayout="@raw/keyboard_layout_persian" /> - - <keyboard-layout android:name="keyboard_layout_azerbaijani" - android:label="@string/keyboard_layout_azerbaijani" - android:keyboardLayout="@raw/keyboard_layout_azerbaijani" /> - - <keyboard-layout android:name="keyboard_layout_polish" - android:label="@string/keyboard_layout_polish" - android:keyboardLayout="@raw/keyboard_layout_polish" /> - - <keyboard-layout android:name="keyboard_layout_belarusian" - android:label="@string/keyboard_layout_belarusian" - android:keyboardLayout="@raw/keyboard_layout_belarusian" /> - - <keyboard-layout android:name="keyboard_layout_mongolian" - android:label="@string/keyboard_layout_mongolian" - android:keyboardLayout="@raw/keyboard_layout_mongolian" /> - - <keyboard-layout android:name="keyboard_layout_georgian" - android:label="@string/keyboard_layout_georgian" - android:keyboardLayout="@raw/keyboard_layout_georgian" /> + <keyboard-layout + android:name="keyboard_layout_english_uk" + android:label="@string/keyboard_layout_english_uk_label" + android:keyboardLayout="@raw/keyboard_layout_english_uk" + android:keyboardLocale="en-Latn-GB" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_english_us" + android:label="@string/keyboard_layout_english_us_label" + android:keyboardLayout="@raw/keyboard_layout_english_us" + android:keyboardLocale="en-Latn,en-Latn-US" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_english_us_intl" + android:label="@string/keyboard_layout_english_us_intl" + android:keyboardLayout="@raw/keyboard_layout_english_us_intl" + android:keyboardLocale="en-Latn-US" + android:keyboardLayoutType="extended" /> + + <keyboard-layout + android:name="keyboard_layout_english_us_colemak" + android:label="@string/keyboard_layout_english_us_colemak_label" + android:keyboardLayout="@raw/keyboard_layout_english_us_colemak" + android:keyboardLocale="en-Latn-US" + android:keyboardLayoutType="colemak" /> + + <keyboard-layout + android:name="keyboard_layout_english_us_dvorak" + android:label="@string/keyboard_layout_english_us_dvorak_label" + android:keyboardLayout="@raw/keyboard_layout_english_us_dvorak" + android:keyboardLocale="en-Latn-US" + android:keyboardLayoutType="dvorak" /> + + <keyboard-layout + android:name="keyboard_layout_english_us_workman" + android:label="@string/keyboard_layout_english_us_workman_label" + android:keyboardLayout="@raw/keyboard_layout_english_us_workman" + android:keyboardLocale="en-Latn-US" + android:keyboardLayoutType="workman" /> + + <keyboard-layout + android:name="keyboard_layout_german" + android:label="@string/keyboard_layout_german_label" + android:keyboardLayout="@raw/keyboard_layout_german" + android:keyboardLocale="de-Latn" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_french" + android:label="@string/keyboard_layout_french_label" + android:keyboardLayout="@raw/keyboard_layout_french" + android:keyboardLocale="fr-Latn-FR" + android:keyboardLayoutType="azerty" /> + + <keyboard-layout + android:name="keyboard_layout_french_ca" + android:label="@string/keyboard_layout_french_ca_label" + android:keyboardLayout="@raw/keyboard_layout_french_ca" + android:keyboardLocale="fr-Latn-CA" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_russian" + android:label="@string/keyboard_layout_russian_label" + android:keyboardLayout="@raw/keyboard_layout_russian" + android:keyboardLocale="ru-Cyrl" /> + + <keyboard-layout + android:name="keyboard_layout_russian_mac" + android:label="@string/keyboard_layout_russian_mac_label" + android:keyboardLayout="@raw/keyboard_layout_russian_mac" + android:keyboardLocale="ru-Cyrl" + android:keyboardLayoutType="extended" /> + + <keyboard-layout + android:name="keyboard_layout_spanish" + android:label="@string/keyboard_layout_spanish_label" + android:keyboardLayout="@raw/keyboard_layout_spanish" + android:keyboardLocale="es-Latn-ES" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_swiss_french" + android:label="@string/keyboard_layout_swiss_french_label" + android:keyboardLayout="@raw/keyboard_layout_swiss_french" + android:keyboardLocale="fr-Latn-CH" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_swiss_german" + android:label="@string/keyboard_layout_swiss_german_label" + android:keyboardLayout="@raw/keyboard_layout_swiss_german" + android:keyboardLocale="de-Latn-CH" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_belgian" + android:label="@string/keyboard_layout_belgian" + android:keyboardLayout="@raw/keyboard_layout_belgian" + android:keyboardLocale="fr-Latn-BE" + android:keyboardLayoutType="azerty" /> + + <keyboard-layout + android:name="keyboard_layout_bulgarian" + android:label="@string/keyboard_layout_bulgarian" + android:keyboardLayout="@raw/keyboard_layout_bulgarian" + android:keyboardLocale="bg-Cyrl" /> + + <keyboard-layout + android:name="keyboard_layout_bulgarian_phonetic" + android:label="@string/keyboard_layout_bulgarian_phonetic" + android:keyboardLayout="@raw/keyboard_layout_bulgarian_phonetic" + android:keyboardLocale="bg-Cyrl" + android:keyboardLayoutType="extended" /> + + <keyboard-layout + android:name="keyboard_layout_italian" + android:label="@string/keyboard_layout_italian" + android:keyboardLayout="@raw/keyboard_layout_italian" + android:keyboardLocale="it-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_danish" + android:label="@string/keyboard_layout_danish" + android:keyboardLayout="@raw/keyboard_layout_danish" + android:keyboardLocale="da-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_norwegian" + android:label="@string/keyboard_layout_norwegian" + android:keyboardLayout="@raw/keyboard_layout_norwegian" + android:keyboardLocale="nb-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_swedish" + android:label="@string/keyboard_layout_swedish" + android:keyboardLayout="@raw/keyboard_layout_swedish" + android:keyboardLocale="sv-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_finnish" + android:label="@string/keyboard_layout_finnish" + android:keyboardLayout="@raw/keyboard_layout_finnish" + android:keyboardLocale="fi-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_croatian" + android:label="@string/keyboard_layout_croatian" + android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian" + android:keyboardLocale="hr-Latn" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_czech" + android:label="@string/keyboard_layout_czech" + android:keyboardLayout="@raw/keyboard_layout_czech" + android:keyboardLocale="cs-Latn" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_czech_qwerty" + android:label="@string/keyboard_layout_czech_qwerty" + android:keyboardLayout="@raw/keyboard_layout_czech_qwerty" + android:keyboardLocale="cs-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_estonian" + android:label="@string/keyboard_layout_estonian" + android:keyboardLayout="@raw/keyboard_layout_estonian" + android:keyboardLocale="et-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_hungarian" + android:label="@string/keyboard_layout_hungarian" + android:keyboardLayout="@raw/keyboard_layout_hungarian" + android:keyboardLocale="hu-Latn" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_icelandic" + android:label="@string/keyboard_layout_icelandic" + android:keyboardLayout="@raw/keyboard_layout_icelandic" + android:keyboardLocale="is-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_brazilian" + android:label="@string/keyboard_layout_brazilian" + android:keyboardLayout="@raw/keyboard_layout_brazilian" + android:keyboardLocale="pt-Latn-BR" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_portuguese" + android:label="@string/keyboard_layout_portuguese" + android:keyboardLayout="@raw/keyboard_layout_portuguese" + android:keyboardLocale="pt-Latn-PT" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_slovak" + android:label="@string/keyboard_layout_slovak" + android:keyboardLayout="@raw/keyboard_layout_slovak" + android:keyboardLocale="sk-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_slovenian" + android:label="@string/keyboard_layout_slovenian" + android:keyboardLayout="@raw/keyboard_layout_croatian_and_slovenian" + android:keyboardLocale="sl-Latn" + android:keyboardLayoutType="qwertz" /> + + <keyboard-layout + android:name="keyboard_layout_turkish" + android:label="@string/keyboard_layout_turkish" + android:keyboardLayout="@raw/keyboard_layout_turkish" + android:keyboardLocale="tr-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_turkish" + android:label="@string/keyboard_layout_turkish" + android:keyboardLayout="@raw/keyboard_layout_turkish" + android:keyboardLocale="tr-Latn" + android:keyboardLayoutType="turkish_q" /> + + <keyboard-layout + android:name="keyboard_layout_turkish_f" + android:label="@string/keyboard_layout_turkish_f" + android:keyboardLayout="@raw/keyboard_layout_turkish_f" + android:keyboardLocale="tr-Latn" + android:keyboardLayoutType="turkish_f" /> + + <keyboard-layout + android:name="keyboard_layout_ukrainian" + android:label="@string/keyboard_layout_ukrainian" + android:keyboardLayout="@raw/keyboard_layout_ukrainian" + android:keyboardLocale="uk-Cyrl" /> + + <keyboard-layout + android:name="keyboard_layout_arabic" + android:label="@string/keyboard_layout_arabic" + android:keyboardLayout="@raw/keyboard_layout_arabic" + android:keyboardLocale="ar-Arab" /> + + <keyboard-layout + android:name="keyboard_layout_greek" + android:label="@string/keyboard_layout_greek" + android:keyboardLayout="@raw/keyboard_layout_greek" + android:keyboardLocale="el-Grek" /> + + <keyboard-layout + android:name="keyboard_layout_hebrew" + android:label="@string/keyboard_layout_hebrew" + android:keyboardLayout="@raw/keyboard_layout_hebrew" + android:keyboardLocale="iw-Hebr" /> + + <keyboard-layout + android:name="keyboard_layout_lithuanian" + android:label="@string/keyboard_layout_lithuanian" + android:keyboardLayout="@raw/keyboard_layout_lithuanian" + android:keyboardLocale="lt-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_spanish_latin" + android:label="@string/keyboard_layout_spanish_latin" + android:keyboardLayout="@raw/keyboard_layout_spanish_latin" + android:keyboardLocale="es-Latn-419" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_latvian" + android:label="@string/keyboard_layout_latvian" + android:keyboardLayout="@raw/keyboard_layout_latvian_qwerty" + android:keyboardLocale="lv-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_persian" + android:label="@string/keyboard_layout_persian" + android:keyboardLayout="@raw/keyboard_layout_persian" + android:keyboardLocale="fa-Arab" /> + + <keyboard-layout + android:name="keyboard_layout_azerbaijani" + android:label="@string/keyboard_layout_azerbaijani" + android:keyboardLayout="@raw/keyboard_layout_azerbaijani" + android:keyboardLocale="az-Latn-AZ" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_polish" + android:label="@string/keyboard_layout_polish" + android:keyboardLayout="@raw/keyboard_layout_polish" + android:keyboardLocale="pl-Latn" + android:keyboardLayoutType="qwerty" /> + + <keyboard-layout + android:name="keyboard_layout_belarusian" + android:label="@string/keyboard_layout_belarusian" + android:keyboardLayout="@raw/keyboard_layout_belarusian" + android:keyboardLocale="be-Cyrl" /> + + <keyboard-layout + android:name="keyboard_layout_mongolian" + android:label="@string/keyboard_layout_mongolian" + android:keyboardLayout="@raw/keyboard_layout_mongolian" + android:keyboardLocale="mn-Cyrl" /> + + <keyboard-layout + android:name="keyboard_layout_georgian" + android:label="@string/keyboard_layout_georgian" + android:keyboardLayout="@raw/keyboard_layout_georgian" + android:keyboardLocale="ka-Geor" /> </keyboard-layouts> diff --git a/packages/PackageInstaller/Android.bp b/packages/PackageInstaller/Android.bp index fe640ad5974e..fd982f5df2d0 100644 --- a/packages/PackageInstaller/Android.bp +++ b/packages/PackageInstaller/Android.bp @@ -39,12 +39,13 @@ android_app { certificate: "platform", privileged: true, - platform_apis: true, + platform_apis: false, + sdk_version: "system_current", rename_resources_package: false, - static_libs: [ "xz-java", "androidx.leanback_leanback", + "androidx.annotation_annotation", ], } @@ -56,7 +57,8 @@ android_app { certificate: "platform", privileged: true, - platform_apis: true, + platform_apis: false, + sdk_version: "system_current", rename_resources_package: false, overrides: ["PackageInstaller"], @@ -75,13 +77,15 @@ android_app { certificate: "platform", privileged: true, - platform_apis: true, + platform_apis: false, + sdk_version: "system_current", rename_resources_package: false, overrides: ["PackageInstaller"], static_libs: [ "xz-java", "androidx.leanback_leanback", + "androidx.annotation_annotation", ], aaptflags: ["--product tv"], } diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_bright.9.png Binary files differnew file mode 100644 index 000000000000..6e5fbb5ab262 --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_bright.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_dark.9.png Binary files differnew file mode 100644 index 000000000000..3434b2dc35a9 --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_dark.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_medium.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_medium.9.png Binary files differnew file mode 100644 index 000000000000..673a5095c44a --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_bottom_medium.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_center_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_center_bright.9.png Binary files differnew file mode 100644 index 000000000000..c2a739c42a8d --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_center_bright.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_center_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_center_dark.9.png Binary files differnew file mode 100644 index 000000000000..9d2bfb155d80 --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_center_dark.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_center_medium.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_center_medium.9.png Binary files differnew file mode 100644 index 000000000000..4375bf2d6b27 --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_center_medium.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_full_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_full_bright.9.png Binary files differnew file mode 100644 index 000000000000..6b8aa9d521be --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_full_bright.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_full_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_full_dark.9.png Binary files differnew file mode 100644 index 000000000000..2884abeabe18 --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_full_dark.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_top_bright.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_top_bright.9.png Binary files differnew file mode 100644 index 000000000000..76c35ec9e81b --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_top_bright.9.png diff --git a/packages/PackageInstaller/res/drawable-hdpi/popup_top_dark.9.png b/packages/PackageInstaller/res/drawable-hdpi/popup_top_dark.9.png Binary files differnew file mode 100644 index 000000000000..f3173301c22f --- /dev/null +++ b/packages/PackageInstaller/res/drawable-hdpi/popup_top_dark.9.png diff --git a/packages/PackageInstaller/res/drawable/ic_dialog_info.png b/packages/PackageInstaller/res/drawable/ic_dialog_info.png Binary files differnew file mode 100644 index 000000000000..efee1efa4709 --- /dev/null +++ b/packages/PackageInstaller/res/drawable/ic_dialog_info.png diff --git a/packages/PackageInstaller/res/layout-television/alert_dialog_button_bar_leanback.xml b/packages/PackageInstaller/res/layout-television/alert_dialog_button_bar_leanback.xml new file mode 100644 index 000000000000..3ced1db8a36b --- /dev/null +++ b/packages/PackageInstaller/res/layout-television/alert_dialog_button_bar_leanback.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/buttonPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:scrollbarAlwaysDrawVerticalTrack="true" + android:scrollIndicators="top|bottom" + android:fillViewport="true" + style="?android:attr/buttonBarStyle"> + <com.android.packageinstaller.ButtonBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layoutDirection="locale" + android:orientation="horizontal" + android:gravity="start"> + + <Button + android:id="@+id/button1" + style="?android:attr/buttonBarPositiveButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Button + android:id="@+id/button2" + style="?android:attr/buttonBarNegativeButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Space + android:id="@+id/spacer" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1" + android:visibility="invisible" /> + + <Button + android:id="@+id/button3" + style="?attr/buttonBarNeutralButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + </com.android.packageinstaller.ButtonBarLayout> +</ScrollView> diff --git a/packages/PackageInstaller/res/layout-television/alert_dialog_leanback.xml b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback.xml new file mode 100644 index 000000000000..0290624b9e6e --- /dev/null +++ b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<com.android.packageinstaller.AlertDialogLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/parentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start|top" + android:orientation="vertical"> + + <include layout="@layout/alert_dialog_title_material" /> + + <FrameLayout + android:id="@+id/contentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp"> + + <ScrollView + android:id="@+id/scrollView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <Space + android:id="@+id/textSpacerNoTitle" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="@dimen/dialog_padding_top_material" /> + + <TextView + android:id="@+id/message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingEnd="?android:attr/dialogPreferredPadding" + android:paddingStart="?android:attr/dialogPreferredPadding" + style="@android:style/TextAppearance.Material.Subhead" /> + + <Space + android:id="@+id/textSpacerNoButtons" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="@dimen/dialog_padding_top_material" /> + </LinearLayout> + </ScrollView> + </FrameLayout> + + <FrameLayout + android:id="@+id/customPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp"> + + <FrameLayout + android:id="@+id/custom" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </FrameLayout> + + <include + android:layout_width="match_parent" + android:layout_height="wrap_content" + layout="@layout/alert_dialog_button_bar_leanback" /> +</com.android.packageinstaller.AlertDialogLayout> diff --git a/packages/PackageInstaller/res/layout-television/alert_dialog_leanback_button_panel_side.xml b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback_button_panel_side.xml new file mode 100644 index 000000000000..f9668dd3e2ea --- /dev/null +++ b/packages/PackageInstaller/res/layout-television/alert_dialog_leanback_button_panel_side.xml @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/parentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <LinearLayout + android:id="@+id/leftPanel" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout android:id="@+id/topPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + <LinearLayout android:id="@+id/title_template" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical|start" + android:paddingStart="16dip" + android:paddingEnd="16dip" + android:paddingTop="16dip" + android:paddingBottom="8dip"> + <ImageView android:id="@+id/icon" + android:layout_width="32dip" + android:layout_height="32dip" + android:layout_marginEnd="8dip" + android:scaleType="fitCenter" + android:src="@null" /> + <com.android.packageinstaller.DialogTitle android:id="@+id/alertTitle" + style="?android:attr/windowTitleStyle" + android:singleLine="true" + android:ellipsize="end" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" /> + </LinearLayout> + <!-- If the client uses a customTitle, it will be added here. --> + </LinearLayout> + + <LinearLayout android:id="@+id/contentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="vertical" + android:minHeight="64dp"> + <ScrollView android:id="@+id/scrollView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false"> + <TextView android:id="@+id/message" + style="?android:attr/textAppearanceMedium" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="16dip" + android:paddingEnd="16dip" + android:paddingTop="16dip" + android:paddingBottom="16dip" /> + </ScrollView> + </LinearLayout> + + <FrameLayout android:id="@+id/customPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:minHeight="64dp"> + <FrameLayout android:id="@+id/custom" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </FrameLayout> + </LinearLayout> + + <LinearLayout android:id="@+id/buttonPanel" + style="?attr/buttonBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:orientation="vertical" + android:gravity="end"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layoutDirection="locale" + android:orientation="vertical"> + <Button android:id="@+id/button3" + style="?attr/buttonBarNeutralButtonStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:maxLines="2" + android:minHeight="@dimen/alert_dialog_button_bar_height" /> + <Button android:id="@+id/button2" + style="?attr/buttonBarNegativeButtonStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:maxLines="2" + android:minHeight="@dimen/alert_dialog_button_bar_height" /> + <Button android:id="@+id/button1" + style="?attr/buttonBarPositiveButtonStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:maxLines="2" + android:minHeight="@dimen/alert_dialog_button_bar_height" /> + </LinearLayout> + </LinearLayout> +</LinearLayout> diff --git a/packages/PackageInstaller/res/layout/alert_dialog_button_bar_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_button_bar_material.xml new file mode 100644 index 000000000000..e4977e7cea6f --- /dev/null +++ b/packages/PackageInstaller/res/layout/alert_dialog_button_bar_material.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/buttonPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:scrollbarAlwaysDrawVerticalTrack="true" + android:scrollIndicators="top|bottom" + android:fillViewport="true" + style="?android:attr/buttonBarStyle"> + <com.android.packageinstaller.ButtonBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layoutDirection="locale" + android:orientation="horizontal" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" + android:gravity="bottom"> + + <Button + android:id="@+id/button3" + style="?android:attr/buttonBarNeutralButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Space + android:id="@+id/spacer" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1" + android:visibility="invisible" /> + + <Button + android:id="@+id/button2" + style="?android:attr/buttonBarNegativeButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + + <Button + android:id="@+id/button1" + style="?android:attr/buttonBarPositiveButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + </com.android.packageinstaller.ButtonBarLayout> +</ScrollView> diff --git a/packages/PackageInstaller/res/layout/alert_dialog_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_material.xml new file mode 100644 index 000000000000..10e9149df94c --- /dev/null +++ b/packages/PackageInstaller/res/layout/alert_dialog_material.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<com.android.packageinstaller.AlertDialogLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/parentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start|top" + android:orientation="vertical"> + + <include layout="@layout/alert_dialog_title_material" /> + + <FrameLayout + android:id="@+id/contentPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp"> + + <ScrollView + android:id="@+id/scrollView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipToPadding="false"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <Space + android:id="@+id/textSpacerNoTitle" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="@dimen/dialog_padding_top_material" /> + + <TextView + android:id="@+id/message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingEnd="?android:attr/dialogPreferredPadding" + android:paddingStart="?android:attr/dialogPreferredPadding" + style="@android:style/TextAppearance.Material.Subhead" /> + + <Space + android:id="@+id/textSpacerNoButtons" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="@dimen/dialog_padding_top_material" /> + </LinearLayout> + </ScrollView> + </FrameLayout> + + <FrameLayout + android:id="@+id/customPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp"> + + <FrameLayout + android:id="@+id/custom" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </FrameLayout> + + <include + android:layout_width="match_parent" + android:layout_height="wrap_content" + layout="@layout/alert_dialog_button_bar_material" /> +</com.android.packageinstaller.AlertDialogLayout> diff --git a/packages/PackageInstaller/res/layout/alert_dialog_progress.xml b/packages/PackageInstaller/res/layout/alert_dialog_progress.xml new file mode 100644 index 000000000000..fe06b6543d60 --- /dev/null +++ b/packages/PackageInstaller/res/layout/alert_dialog_progress.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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. +--> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" android:layout_height="match_parent"> + <ProgressBar android:id="@+id/progress" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="12dip" + android:layout_marginBottom="1dip" + android:layout_marginStart="10dip" + android:layout_marginEnd="10dip" + android:layout_centerHorizontal="true" /> + <TextView + android:id="@+id/progress_percent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="12dip" + android:layout_marginStart="10dip" + android:layout_marginEnd="10dip" + android:layout_alignParentStart="true" + android:layout_below="@id/progress" + /> + <TextView + android:id="@+id/progress_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="12dip" + android:layout_marginStart="10dip" + android:layout_marginEnd="10dip" + android:layout_alignParentEnd="true" + android:layout_below="@id/progress" + /> +</RelativeLayout> diff --git a/packages/PackageInstaller/res/layout/alert_dialog_progress_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_progress_material.xml new file mode 100644 index 000000000000..fb9825975162 --- /dev/null +++ b/packages/PackageInstaller/res/layout/alert_dialog_progress_material.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:paddingStart="?attr/dialogPreferredPadding" + android:paddingTop="@dimen/dialog_padding_top_material" + android:paddingEnd="?attr/dialogPreferredPadding" + android:paddingBottom="@dimen/dialog_padding_top_material"> + <ProgressBar + android:id="@+id/progress" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" /> + <TextView + android:id="@+id/progress_percent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_below="@id/progress" /> + <TextView + android:id="@+id/progress_number" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentEnd="true" + android:layout_below="@id/progress" /> +</RelativeLayout> diff --git a/packages/PackageInstaller/res/layout/alert_dialog_title_material.xml b/packages/PackageInstaller/res/layout/alert_dialog_title_material.xml new file mode 100644 index 000000000000..45d9bb68fdf8 --- /dev/null +++ b/packages/PackageInstaller/res/layout/alert_dialog_title_material.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/topPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <!-- If the client uses a customTitle, it will be added here. --> + + <LinearLayout + android:id="@+id/title_template" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_vertical|start" + android:paddingStart="?android:attr/dialogPreferredPadding" + android:paddingEnd="?android:attr/dialogPreferredPadding" + android:paddingTop="@dimen/dialog_padding_top_material"> + + <ImageView + android:id="@+id/icon" + android:layout_width="32dip" + android:layout_height="32dip" + android:layout_marginEnd="8dip" + android:scaleType="fitCenter" + android:src="@null" /> + + <com.android.packageinstaller.DialogTitle + android:id="@+id/alertTitle" + android:singleLine="true" + android:ellipsize="end" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + style="?android:attr/windowTitleStyle" /> + </LinearLayout> + + <Space + android:id="@+id/titleDividerNoCustom" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="@dimen/dialog_title_divider_material" /> +</LinearLayout> diff --git a/packages/PackageInstaller/res/layout/progress_dialog.xml b/packages/PackageInstaller/res/layout/progress_dialog.xml new file mode 100644 index 000000000000..0d3cd4a9b43c --- /dev/null +++ b/packages/PackageInstaller/res/layout/progress_dialog.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* //device/apps/common/res/layout/alert_dialog.xml +** +** Copyright 2006, 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:layout_width="match_parent" + android:layout_height="wrap_content"> + + <LinearLayout android:id="@+id/body" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:baselineAligned="false" + android:paddingStart="8dip" + android:paddingTop="10dip" + android:paddingEnd="8dip" + android:paddingBottom="10dip"> + + <ProgressBar android:id="@android:id/progress" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:max="10000" + android:layout_marginEnd="12dip" /> + + <TextView android:id="@+id/message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" /> + </LinearLayout> +</FrameLayout> diff --git a/packages/PackageInstaller/res/layout/progress_dialog_material.xml b/packages/PackageInstaller/res/layout/progress_dialog_material.xml new file mode 100644 index 000000000000..241796599ee8 --- /dev/null +++ b/packages/PackageInstaller/res/layout/progress_dialog_material.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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:layout_width="match_parent" + android:layout_height="wrap_content"> + + <LinearLayout + android:id="@+id/body" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:baselineAligned="false" + android:paddingStart="?attr/dialogPreferredPadding" + android:paddingTop="@dimen/dialog_padding_top_material" + android:paddingEnd="?attr/dialogPreferredPadding" + android:paddingBottom="@dimen/dialog_padding_top_material"> + + <ProgressBar + android:id="@id/progress" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:max="10000" + android:layout_marginEnd="?attr/dialogPreferredPadding" /> + + <TextView + android:id="@+id/message" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" /> + </LinearLayout> +</FrameLayout> diff --git a/packages/PackageInstaller/res/layout/select_dialog_item_material.xml b/packages/PackageInstaller/res/layout/select_dialog_item_material.xml new file mode 100644 index 000000000000..b45edc6218ec --- /dev/null +++ b/packages/PackageInstaller/res/layout/select_dialog_item_material.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<!-- + This layout file is used by the AlertDialog when displaying a list of items. + This layout file is inflated and used as the TextView to display individual + items. +--> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:textColor="?android:attr/textColorAlertDialogListItem" + android:gravity="center_vertical" + android:paddingStart="?attr/listPreferredItemPaddingStart" + android:paddingEnd="?attr/listPreferredItemPaddingEnd" + android:ellipsize="marquee" /> diff --git a/packages/PackageInstaller/res/layout/select_dialog_material.xml b/packages/PackageInstaller/res/layout/select_dialog_material.xml new file mode 100644 index 000000000000..125b9b8a652e --- /dev/null +++ b/packages/PackageInstaller/res/layout/select_dialog_material.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<!-- + This layout file is used by the AlertDialog when displaying a list of items. + This layout file is inflated and used as the ListView to display the items. + Assign an ID so its state will be saved/restored. +--> +<view class="com.android.packageinstaller.AlertController$RecycleListView" + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@id/select_dialog_listview" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:cacheColorHint="@null" + android:divider="?attr/listDividerAlertDialog" + android:scrollbars="vertical" + android:overScrollMode="ifContentScrolls" + android:textAlignment="viewStart" + android:clipToPadding="false"/> + <!--android:paddingBottomNoButtons="@dimen/dialog_list_padding_bottom_no_buttons" + android:paddingTopNoTitle="@dimen/dialog_list_padding_top_no_title"/>--> diff --git a/packages/PackageInstaller/res/layout/select_dialog_multichoice_material.xml b/packages/PackageInstaller/res/layout/select_dialog_multichoice_material.xml new file mode 100644 index 000000000000..52f709e03ebc --- /dev/null +++ b/packages/PackageInstaller/res/layout/select_dialog_multichoice_material.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?attr/listPreferredItemHeightSmall" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?attr/textColorAlertDialogListItem" + android:gravity="center_vertical" + android:paddingStart="@dimen/select_dialog_padding_start_material" + android:paddingEnd="?attr/dialogPreferredPadding" + android:drawableStart="?android:attr/listChoiceIndicatorMultiple" + android:drawablePadding="@dimen/select_dialog_drawable_padding_start_material" + android:ellipsize="marquee" /> diff --git a/packages/PackageInstaller/res/layout/select_dialog_singlechoice_material.xml b/packages/PackageInstaller/res/layout/select_dialog_singlechoice_material.xml new file mode 100644 index 000000000000..8345b18dcc49 --- /dev/null +++ b/packages/PackageInstaller/res/layout/select_dialog_singlechoice_material.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> + +<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?attr/listPreferredItemHeightSmall" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textColor="?attr/textColorAlertDialogListItem" + android:gravity="center_vertical" + android:paddingStart="@dimen/select_dialog_padding_start_material" + android:paddingEnd="?attr/dialogPreferredPadding" + android:drawableStart="?android:attr/listChoiceIndicatorSingle" + android:drawablePadding="@dimen/select_dialog_drawable_padding_start_material" + android:ellipsize="marquee" /> diff --git a/packages/PackageInstaller/res/values-night/themes.xml b/packages/PackageInstaller/res/values-night/themes.xml index 483b0cf18c56..18320f7626e9 100644 --- a/packages/PackageInstaller/res/values-night/themes.xml +++ b/packages/PackageInstaller/res/values-night/themes.xml @@ -18,6 +18,8 @@ <resources> <style name="Theme.AlertDialogActivity" - parent="@android:style/Theme.DeviceDefault.Dialog.Alert" /> + parent="@android:style/Theme.DeviceDefault.Dialog.Alert"> + <item name="alertDialogStyle">@style/AlertDialog</item> + </style> </resources> diff --git a/packages/PackageInstaller/res/values-television/styles.xml b/packages/PackageInstaller/res/values-television/styles.xml new file mode 100644 index 000000000000..936fff009678 --- /dev/null +++ b/packages/PackageInstaller/res/values-television/styles.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <style name="AlertDialog.Leanback" parent="@style/AlertDialog"> + <item name="buttonPanelSideLayout">@layout/alert_dialog_leanback_button_panel_side</item> + <item name="layout">@layout/alert_dialog_leanback</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/PackageInstaller/res/values-television/themes.xml b/packages/PackageInstaller/res/values-television/themes.xml index 5ae4957b494d..1cc6933146a8 100644 --- a/packages/PackageInstaller/res/values-television/themes.xml +++ b/packages/PackageInstaller/res/values-television/themes.xml @@ -17,15 +17,20 @@ <resources> - <style name="Theme.AlertDialogActivity.NoAnimation"> + <style name="Theme.AlertDialogActivity.NoAnimation" + parent="@style/Theme.AlertDialogActivity.NoActionBar"> <item name="android:windowAnimationStyle">@null</item> </style> <style name="Theme.AlertDialogActivity" - parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert" /> + parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert"> + <item name="alertDialogStyle">@style/AlertDialog.Leanback</item> + </style> <style name="Theme.AlertDialogActivity.NoActionBar" - parent="@android:style/Theme.DeviceDefault.Light.NoActionBar"> + parent="@android:style/Theme.Material.Light.NoActionBar"> + <item name="android:windowActionBar">false</item> + <item name="android:windowNoTitle">true</item> </style> </resources> diff --git a/packages/PackageInstaller/res/values/attrs.xml b/packages/PackageInstaller/res/values/attrs.xml index e220f4c88edf..e3070e2e4b01 100644 --- a/packages/PackageInstaller/res/values/attrs.xml +++ b/packages/PackageInstaller/res/values/attrs.xml @@ -32,4 +32,49 @@ <attr name="circle_radius_pressed_percent" format="fraction" /> </declare-styleable> <!-- END: Ported from WearableSupport --> + <declare-styleable name="Theme"> + <attr name="alertDialogCenterButtons" format="boolean" /> + </declare-styleable> + <declare-styleable name="AlertDialog"> + <attr name="fullDark" format="reference|color" /> + <attr name="topDark" format="reference|color" /> + <attr name="centerDark" format="reference|color" /> + <attr name="bottomDark" format="reference|color" /> + <attr name="fullBright" format="reference|color" /> + <attr name="topBright" format="reference|color" /> + <attr name="centerBright" format="reference|color" /> + <attr name="bottomBright" format="reference|color" /> + <attr name="bottomMedium" format="reference|color" /> + <attr name="centerMedium" format="reference|color" /> + <attr name="layout" format="reference" /> + <attr name="buttonPanelSideLayout" format="reference" /> + <attr name="listLayout" format="reference" /> + <attr name="multiChoiceItemLayout" format="reference" /> + <attr name="singleChoiceItemLayout" format="reference" /> + <attr name="listItemLayout" format="reference" /> + <attr name="progressLayout" format="reference" /> + <attr name="horizontalProgressLayout" format="reference" /> + <!-- @hide Not ready for public use. --> + <attr name="showTitle" format="boolean" /> + <!-- Whether fullDark, etc. should use default values if null. --> + <attr name="needsDefaultBackgrounds" format="boolean" /> + <!-- Workaround until we replace AlertController with custom layout. --> + <attr name="controllerType"> + <!-- The default controller. --> + <enum name="normal" value="0" /> + <!-- Controller for micro specific layout. --> + <enum name="micro" value="1" /> + </attr> + <!-- Offset when scrolling to a selection. --> + <attr name="selectionScrollOffset" format="dimension" /> + </declare-styleable> + <declare-styleable name="ButtonBarLayout"> + <!-- Whether to automatically stack the buttons when there is not + enough space to lay them out side-by-side. --> + <attr name="allowStacking" format="boolean" /> + </declare-styleable> + <declare-styleable name="TextAppearance"> + <!-- Size of the text. Recommended dimension type for text is "sp" for scaled-pixels (example: 15sp). --> + <attr name="textSize" format="dimension" /> + </declare-styleable> </resources> diff --git a/packages/PackageInstaller/res/values/dimens.xml b/packages/PackageInstaller/res/values/dimens.xml index 112723f48217..bfea05e0f9ed 100644 --- a/packages/PackageInstaller/res/values/dimens.xml +++ b/packages/PackageInstaller/res/values/dimens.xml @@ -41,4 +41,16 @@ <dimen name="wear_permission_review_pref_padding">8dp</dimen> <dimen name="wear_permission_review_icon_size">24dp</dimen> + + <!-- Dialog title height --> + <dimen name="alert_dialog_title_height">64dip</dimen> + <!-- Dialog button bar height --> + <dimen name="alert_dialog_button_bar_height">48dip</dimen> + <!-- The amount to offset when scrolling to a selection in an AlertDialog --> + <dimen name="config_alertDialogSelectionScrollOffset">0dp</dimen> + <dimen name="dialog_padding_top_material">18dp</dimen> + <dimen name="dialog_title_divider_material">8dp</dimen> + <!-- Dialog padding minus control padding, used to fix alignment. --> + <dimen name="select_dialog_padding_start_material">20dp</dimen> + <dimen name="select_dialog_drawable_padding_start_material">20dp</dimen> </resources> diff --git a/packages/PackageInstaller/res/values/integers.xml b/packages/PackageInstaller/res/values/integers.xml new file mode 100644 index 000000000000..22ad3a348fb2 --- /dev/null +++ b/packages/PackageInstaller/res/values/integers.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <!-- The alert controller to use for alert dialogs. --> + <integer name="config_alertDialogController">0</integer> +</resources>
\ No newline at end of file diff --git a/packages/PackageInstaller/res/values/styles.xml b/packages/PackageInstaller/res/values/styles.xml new file mode 100644 index 000000000000..ca797e142f82 --- /dev/null +++ b/packages/PackageInstaller/res/values/styles.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <style name="AlertDialog"> + <item name="fullDark">@empty</item> + <item name="topDark">@empty</item> + <item name="centerDark">@empty</item> + <item name="bottomDark">@empty</item> + <item name="fullBright">@empty</item> + <item name="topBright">@empty</item> + <item name="centerBright">@empty</item> + <item name="bottomBright">@empty</item> + <item name="bottomMedium">@empty</item> + <item name="centerMedium">@empty</item> + <item name="layout">@layout/alert_dialog_material</item> + <item name="listLayout">@layout/select_dialog_material</item> + <item name="progressLayout">@layout/progress_dialog_material</item> + <item name="horizontalProgressLayout">@layout/alert_dialog_progress_material</item> + <item name="listItemLayout">@layout/select_dialog_item_material</item> + <item name="multiChoiceItemLayout">@layout/select_dialog_multichoice_material</item> + <item name="singleChoiceItemLayout">@layout/select_dialog_singlechoice_material</item> + <item name="controllerType">@integer/config_alertDialogController</item> + <item name="selectionScrollOffset">@dimen/config_alertDialogSelectionScrollOffset</item> + <item name="needsDefaultBackgrounds">false</item> + </style> + <style name="TextAppearance"> + <item name="textSize">16sp</item> + </style> +</resources>
\ No newline at end of file diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml index eecf9a1950ac..9a062295608d 100644 --- a/packages/PackageInstaller/res/values/themes.xml +++ b/packages/PackageInstaller/res/values/themes.xml @@ -23,7 +23,9 @@ </style> <style name="Theme.AlertDialogActivity" - parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert" /> + parent="@android:style/Theme.DeviceDefault.Light.Dialog.Alert"> + <item name="alertDialogStyle">@style/AlertDialog</item> + </style> <style name="Theme.AlertDialogActivity.NoActionBar"> <item name="android:windowActionBar">false</item> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/AlertActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/AlertActivity.java new file mode 100644 index 000000000000..79474006fa0e --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/AlertActivity.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2007 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; + +import android.app.Activity; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * An activity that follows the visual style of an AlertDialog. + * + * @see #mAlert + * @see #setupAlert() + */ +public abstract class AlertActivity extends Activity implements DialogInterface { + + public AlertActivity() { + } + + /** + * The model for the alert. + * + */ + protected AlertController mAlert; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mAlert = new AlertController(this, this, getWindow()); + } + + public void cancel() { + finish(); + } + + public void dismiss() { + // This is called after the click, since we finish when handling the + // click, don't do that again here. + if (!isFinishing()) { + finish(); + } + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + return dispatchPopulateAccessibilityEvent(this, event); + } + + public static boolean dispatchPopulateAccessibilityEvent(Activity act, + AccessibilityEvent event) { + event.setClassName(Dialog.class.getName()); + event.setPackageName(act.getPackageName()); + + ViewGroup.LayoutParams params = act.getWindow().getAttributes(); + boolean isFullScreen = (params.width == ViewGroup.LayoutParams.MATCH_PARENT) && + (params.height == ViewGroup.LayoutParams.MATCH_PARENT); + event.setFullScreen(isFullScreen); + + return false; + } + + /** + * Sets up the alert, including applying the parameters to the alert model, + * and installing the alert's content. + * + * @see #mAlert + */ + protected void setupAlert() { + mAlert.installContent(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (mAlert.onKeyDown(keyCode, event)) return true; + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (mAlert.onKeyUp(keyCode, event)) return true; + return super.onKeyUp(keyCode, event); + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/AlertController.java b/packages/PackageInstaller/src/com/android/packageinstaller/AlertController.java new file mode 100644 index 000000000000..33f38a680712 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/AlertController.java @@ -0,0 +1,685 @@ +/* + * Copyright (C) 2008 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; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Message; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewParent; +import android.view.ViewStub; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import com.android.packageinstaller.R; + +import java.lang.ref.WeakReference; + +public class AlertController { + private final Context mContext; + private final DialogInterface mDialogInterface; + protected final Window mWindow; + + private CharSequence mTitle; + protected CharSequence mMessage; + protected ListView mListView; + private View mView; + + private int mViewLayoutResId; + + private int mViewSpacingLeft; + private int mViewSpacingTop; + private int mViewSpacingRight; + private int mViewSpacingBottom; + private boolean mViewSpacingSpecified = false; + + private Button mButtonPositive; + private CharSequence mButtonPositiveText; + private Message mButtonPositiveMessage; + + private Button mButtonNegative; + private CharSequence mButtonNegativeText; + private Message mButtonNegativeMessage; + + private Button mButtonNeutral; + private CharSequence mButtonNeutralText; + private Message mButtonNeutralMessage; + + protected ScrollView mScrollView; + + private int mIconId = 0; + private Drawable mIcon; + + private ImageView mIconView; + private TextView mTitleView; + protected TextView mMessageView; + + private ListAdapter mAdapter; + + private int mAlertDialogLayout; + private int mButtonPanelSideLayout; + private int mListLayout; + private int mMultiChoiceItemLayout; + private int mSingleChoiceItemLayout; + private int mListItemLayout; + + private boolean mShowTitle; + + private Handler mHandler; + + private final View.OnClickListener mButtonHandler = new View.OnClickListener() { + @Override + public void onClick(View v) { + final Message m; + if (v == mButtonPositive && mButtonPositiveMessage != null) { + m = Message.obtain(mButtonPositiveMessage); + } else if (v == mButtonNegative && mButtonNegativeMessage != null) { + m = Message.obtain(mButtonNegativeMessage); + } else if (v == mButtonNeutral && mButtonNeutralMessage != null) { + m = Message.obtain(mButtonNeutralMessage); + } else { + m = null; + } + + if (m != null) { + m.sendToTarget(); + } + + // Post a message so we dismiss after the above handlers are executed + mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialogInterface) + .sendToTarget(); + } + }; + + private static final class ButtonHandler extends Handler { + // Button clicks have Message.what as the BUTTON{1,2,3} constant + private static final int MSG_DISMISS_DIALOG = 1; + + private WeakReference<DialogInterface> mDialog; + + public ButtonHandler(DialogInterface dialog) { + mDialog = new WeakReference<>(dialog); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + + case DialogInterface.BUTTON_POSITIVE: + case DialogInterface.BUTTON_NEGATIVE: + case DialogInterface.BUTTON_NEUTRAL: + ((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what); + break; + + case MSG_DISMISS_DIALOG: + ((DialogInterface) msg.obj).dismiss(); + } + } + } + + public AlertController(Context context, DialogInterface di, Window window) { + mContext = context; + mDialogInterface = di; + mWindow = window; + mHandler = new ButtonHandler(di); + + final TypedArray a = context.obtainStyledAttributes(null, + R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); + + mAlertDialogLayout = a.getResourceId( + R.styleable.AlertDialog_layout, R.layout.alert_dialog_material); + mButtonPanelSideLayout = a.getResourceId( + R.styleable.AlertDialog_buttonPanelSideLayout, 0); + mListLayout = a.getResourceId( + R.styleable.AlertDialog_listLayout, R.layout.select_dialog_material); + + mMultiChoiceItemLayout = a.getResourceId( + R.styleable.AlertDialog_multiChoiceItemLayout, + R.layout.select_dialog_multichoice_material); + mSingleChoiceItemLayout = a.getResourceId( + R.styleable.AlertDialog_singleChoiceItemLayout, + R.layout.select_dialog_singlechoice_material); + mListItemLayout = a.getResourceId( + R.styleable.AlertDialog_listItemLayout, + R.layout.select_dialog_item_material); + mShowTitle = a.getBoolean(R.styleable.AlertDialog_showTitle, true); + + a.recycle(); + + /* We use a custom title so never request a window title */ + window.requestFeature(Window.FEATURE_NO_TITLE); + } + + static boolean canTextInput(View v) { + if (v.onCheckIsTextEditor()) { + return true; + } + + if (!(v instanceof ViewGroup)) { + return false; + } + + ViewGroup vg = (ViewGroup)v; + int i = vg.getChildCount(); + while (i > 0) { + i--; + v = vg.getChildAt(i); + if (canTextInput(v)) { + return true; + } + } + + return false; + } + + public void installContent() { + mWindow.setContentView(mAlertDialogLayout); + setupView(); + } + + public void setTitle(CharSequence title) { + mTitle = title; + if (mTitleView != null) { + mTitleView.setText(title); + } + mWindow.setTitle(title); + } + + /** + * Set the view resource to display in the dialog. + */ + public void setView(int layoutResId) { + mView = null; + mViewLayoutResId = layoutResId; + mViewSpacingSpecified = false; + } + + /** + * Sets a click listener or a message to be sent when the button is clicked. + * You only need to pass one of {@code listener} or {@code msg}. + * + * @param whichButton Which button, can be one of + * {@link DialogInterface#BUTTON_POSITIVE}, + * {@link DialogInterface#BUTTON_NEGATIVE}, or + * {@link DialogInterface#BUTTON_NEUTRAL} + * @param text The text to display in positive button. + * @param listener The {@link DialogInterface.OnClickListener} to use. + * @param msg The {@link Message} to be sent when clicked. + */ + public void setButton(int whichButton, CharSequence text, + DialogInterface.OnClickListener listener, Message msg) { + + if (msg == null && listener != null) { + msg = mHandler.obtainMessage(whichButton, listener); + } + + switch (whichButton) { + + case DialogInterface.BUTTON_POSITIVE: + mButtonPositiveText = text; + mButtonPositiveMessage = msg; + break; + + case DialogInterface.BUTTON_NEGATIVE: + mButtonNegativeText = text; + mButtonNegativeMessage = msg; + break; + + case DialogInterface.BUTTON_NEUTRAL: + mButtonNeutralText = text; + mButtonNeutralMessage = msg; + break; + + default: + throw new IllegalArgumentException("Button does not exist"); + } + } + + /** + * Specifies the icon to display next to the alert title. + * + * @param resId the resource identifier of the drawable to use as the icon, + * or 0 for no icon + */ + public void setIcon(int resId) { + mIcon = null; + mIconId = resId; + + if (mIconView != null) { + if (resId != 0) { + mIconView.setVisibility(View.VISIBLE); + mIconView.setImageResource(mIconId); + } else { + mIconView.setVisibility(View.GONE); + } + } + } + + /** + * Specifies the icon to display next to the alert title. + * + * @param icon the drawable to use as the icon or null for no icon + */ + public void setIcon(Drawable icon) { + mIcon = icon; + mIconId = 0; + + if (mIconView != null) { + if (icon != null) { + mIconView.setVisibility(View.VISIBLE); + mIconView.setImageDrawable(icon); + } else { + mIconView.setVisibility(View.GONE); + } + } + } + + public Button getButton(int whichButton) { + switch (whichButton) { + case DialogInterface.BUTTON_POSITIVE: + return mButtonPositive; + case DialogInterface.BUTTON_NEGATIVE: + return mButtonNegative; + case DialogInterface.BUTTON_NEUTRAL: + return mButtonNeutral; + default: + return null; + } + } + + @SuppressWarnings({"UnusedDeclaration"}) + public boolean onKeyDown(int keyCode, KeyEvent event) { + return mScrollView != null && mScrollView.executeKeyEvent(event); + } + + @SuppressWarnings({"UnusedDeclaration"}) + public boolean onKeyUp(int keyCode, KeyEvent event) { + return mScrollView != null && mScrollView.executeKeyEvent(event); + } + + /** + * Resolves whether a custom or default panel should be used. Removes the + * default panel if a custom panel should be used. If the resolved panel is + * a view stub, inflates before returning. + * + * @param customPanel the custom panel + * @param defaultPanel the default panel + * @return the panel to use + */ + @Nullable + private ViewGroup resolvePanel(@Nullable View customPanel, @Nullable View defaultPanel) { + if (customPanel == null) { + // Inflate the default panel, if needed. + if (defaultPanel instanceof ViewStub) { + defaultPanel = ((ViewStub) defaultPanel).inflate(); + } + + return (ViewGroup) defaultPanel; + } + + // Remove the default panel entirely. + if (defaultPanel != null) { + final ViewParent parent = defaultPanel.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(defaultPanel); + } + } + + // Inflate the custom panel, if needed. + if (customPanel instanceof ViewStub) { + customPanel = ((ViewStub) customPanel).inflate(); + } + + return (ViewGroup) customPanel; + } + + private void setupView() { + final View parentPanel = mWindow.findViewById(R.id.parentPanel); + final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel); + final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel); + final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel); + + // Install custom content before setting up the title or buttons so + // that we can handle panel overrides. + final ViewGroup customPanel = parentPanel.findViewById(R.id.customPanel); + setupCustomContent(customPanel); + + final View customTopPanel = customPanel.findViewById(R.id.topPanel); + final View customContentPanel = customPanel.findViewById(R.id.contentPanel); + final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel); + + // Resolve the correct panels and remove the defaults, if needed. + final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel); + final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel); + final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel); + + setupContent(contentPanel); + setupButtons(buttonPanel); + setupTitle(topPanel); + + final boolean hasCustomPanel = customPanel != null + && customPanel.getVisibility() != View.GONE; + final boolean hasTopPanel = topPanel != null + && topPanel.getVisibility() != View.GONE; + final boolean hasButtonPanel = buttonPanel != null + && buttonPanel.getVisibility() != View.GONE; + + if (!parentPanel.isInTouchMode()) { + final View content = hasCustomPanel ? customPanel : contentPanel; + if (!requestFocusForContent(content)) { + requestFocusForDefaultButton(); + } + } + + if (hasTopPanel) { + // Only clip scrolling content to padding if we have a title. + if (mScrollView != null) { + mScrollView.setClipToPadding(true); + } + + // Only show the divider if we have a title. + View divider = null; + if (mMessage != null || hasCustomPanel) { + divider = topPanel.findViewById(R.id.titleDividerNoCustom); + } + + if (divider != null) { + divider.setVisibility(View.VISIBLE); + } + } else { + if (contentPanel != null) { + final View spacer = contentPanel.findViewById(R.id.textSpacerNoTitle); + if (spacer != null) { + spacer.setVisibility(View.VISIBLE); + } + } + } + + // Update scroll indicators as needed. + if (!hasCustomPanel) { + final View content = mScrollView; + if (content != null) { + final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0) + | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0); + content.setScrollIndicators(indicators, + View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); + } + } + + final TypedArray a = mContext.obtainStyledAttributes( + null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); + setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, + hasTopPanel, hasCustomPanel, hasButtonPanel); + a.recycle(); + } + + private boolean requestFocusForContent(View content) { + return content != null && content.requestFocus(); + } + + private void requestFocusForDefaultButton() { + if (mButtonPositive.getVisibility() == View.VISIBLE) { + mButtonPositive.requestFocus(); + } else if (mButtonNegative.getVisibility() == View.VISIBLE) { + mButtonNegative.requestFocus(); + } else if (mButtonNeutral.getVisibility() == View.VISIBLE) { + mButtonNeutral.requestFocus(); + } + } + + private void setupCustomContent(ViewGroup customPanel) { + final View customView; + if (mView != null) { + customView = mView; + } else if (mViewLayoutResId != 0) { + final LayoutInflater inflater = LayoutInflater.from(mContext); + customView = inflater.inflate(mViewLayoutResId, customPanel, false); + } else { + customView = null; + } + + final boolean hasCustomView = customView != null; + if (!hasCustomView || !canTextInput(customView)) { + mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + } + + if (hasCustomView) { + final FrameLayout custom = mWindow.findViewById(R.id.custom); + custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + if (mViewSpacingSpecified) { + custom.setPadding( + mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom); + } + } else { + customPanel.setVisibility(View.GONE); + } + } + + private void setupTitle(ViewGroup topPanel) { + mIconView = mWindow.findViewById(R.id.icon); + + final boolean hasTextTitle = !TextUtils.isEmpty(mTitle); + if (hasTextTitle && mShowTitle) { + // Display the title if a title is supplied, else hide it. + mTitleView = mWindow.findViewById(R.id.alertTitle); + mTitleView.setText(mTitle); + + // Do this last so that if the user has supplied any icons we + // use them instead of the default ones. If the user has + // specified 0 then make it disappear. + if (mIconId != 0) { + mIconView.setImageResource(mIconId); + } else if (mIcon != null) { + mIconView.setImageDrawable(mIcon); + } else { + // Apply the padding from the icon to ensure the title is + // aligned correctly. + mTitleView.setPadding(mIconView.getPaddingLeft(), + mIconView.getPaddingTop(), + mIconView.getPaddingRight(), + mIconView.getPaddingBottom()); + mIconView.setVisibility(View.GONE); + } + } else { + // Hide the title template + final View titleTemplate = mWindow.findViewById(R.id.title_template); + titleTemplate.setVisibility(View.GONE); + mIconView.setVisibility(View.GONE); + topPanel.setVisibility(View.GONE); + } + } + + private void setupContent(ViewGroup contentPanel) { + mScrollView = contentPanel.findViewById(R.id.scrollView); + mScrollView.setFocusable(false); + + // Special case for users that only want to display a String + mMessageView = contentPanel.findViewById(R.id.message); + if (mMessageView == null) { + return; + } + + mMessageView.setVisibility(View.GONE); + mScrollView.removeView(mMessageView); + + contentPanel.setVisibility(View.GONE); + } + + private void setupButtons(ViewGroup buttonPanel) { + int BIT_BUTTON_POSITIVE = 1; + int BIT_BUTTON_NEGATIVE = 2; + int BIT_BUTTON_NEUTRAL = 4; + int whichButtons = 0; + mButtonPositive = buttonPanel.findViewById(R.id.button1); + mButtonPositive.setOnClickListener(mButtonHandler); + + if (TextUtils.isEmpty(mButtonPositiveText)) { + mButtonPositive.setVisibility(View.GONE); + } else { + mButtonPositive.setText(mButtonPositiveText); + mButtonPositive.setVisibility(View.VISIBLE); + whichButtons = whichButtons | BIT_BUTTON_POSITIVE; + } + + mButtonNegative = buttonPanel.findViewById(R.id.button2); + mButtonNegative.setOnClickListener(mButtonHandler); + + if (TextUtils.isEmpty(mButtonNegativeText)) { + mButtonNegative.setVisibility(View.GONE); + } else { + mButtonNegative.setText(mButtonNegativeText); + mButtonNegative.setVisibility(View.VISIBLE); + + whichButtons = whichButtons | BIT_BUTTON_NEGATIVE; + } + + mButtonNeutral = buttonPanel.findViewById(R.id.button3); + mButtonNeutral.setOnClickListener(mButtonHandler); + + if (TextUtils.isEmpty(mButtonNeutralText)) { + mButtonNeutral.setVisibility(View.GONE); + } else { + mButtonNeutral.setText(mButtonNeutralText); + mButtonNeutral.setVisibility(View.VISIBLE); + + whichButtons = whichButtons | BIT_BUTTON_NEUTRAL; + } + + final boolean hasButtons = whichButtons != 0; + if (!hasButtons) { + buttonPanel.setVisibility(View.GONE); + } + } + + private void setBackground(TypedArray a, View topPanel, View contentPanel, View customPanel, + View buttonPanel, boolean hasTitle, boolean hasCustomView, boolean hasButtons) { + int fullDark = 0; + int topDark = 0; + int centerDark = 0; + int bottomDark = 0; + int fullBright = 0; + int topBright = 0; + int centerBright = 0; + int bottomBright = 0; + int bottomMedium = 0; + + topBright = a.getResourceId(R.styleable.AlertDialog_topBright, topBright); + topDark = a.getResourceId(R.styleable.AlertDialog_topDark, topDark); + centerBright = a.getResourceId(R.styleable.AlertDialog_centerBright, centerBright); + centerDark = a.getResourceId(R.styleable.AlertDialog_centerDark, centerDark); + + /* We now set the background of all of the sections of the alert. + * First collect together each section that is being displayed along + * with whether it is on a light or dark background, then run through + * them setting their backgrounds. This is complicated because we need + * to correctly use the full, top, middle, and bottom graphics depending + * on how many views they are and where they appear. + */ + + final View[] views = new View[4]; + final boolean[] light = new boolean[4]; + View lastView = null; + boolean lastLight = false; + + int pos = 0; + if (hasTitle) { + views[pos] = topPanel; + light[pos] = false; + pos++; + } + + /* The contentPanel displays either a custom text message or + * a ListView. If it's text we should use the dark background + * for ListView we should use the light background. PIA does not use + * a list view. Hence, we set it to use dark background. If neither + * are there the contentPanel will be hidden so set it as null. + */ + views[pos] = contentPanel.getVisibility() == View.GONE ? null : contentPanel; + light[pos] = false; + pos++; + + if (hasCustomView) { + views[pos] = customPanel; + light[pos] = false; + pos++; + } + + if (hasButtons) { + views[pos] = buttonPanel; + light[pos] = true; + } + + boolean setView = false; + for (pos = 0; pos < views.length; pos++) { + final View v = views[pos]; + if (v == null) { + continue; + } + + if (lastView != null) { + if (!setView) { + lastView.setBackgroundResource(lastLight ? topBright : topDark); + } else { + lastView.setBackgroundResource(lastLight ? centerBright : centerDark); + } + setView = true; + } + + lastView = v; + lastLight = light[pos]; + } + + if (lastView != null) { + if (setView) { + bottomBright = a.getResourceId(R.styleable.AlertDialog_bottomBright, bottomBright); + bottomMedium = a.getResourceId(R.styleable.AlertDialog_bottomMedium, bottomMedium); + bottomDark = a.getResourceId(R.styleable.AlertDialog_bottomDark, bottomDark); + + // ListViews will use the Bright background, but buttons use the + // Medium background. + lastView.setBackgroundResource( + lastLight ? (hasButtons ? bottomMedium : bottomBright) : bottomDark); + } else { + fullBright = a.getResourceId(R.styleable.AlertDialog_fullBright, fullBright); + fullDark = a.getResourceId(R.styleable.AlertDialog_fullDark, fullDark); + + lastView.setBackgroundResource(lastLight ? fullBright : fullDark); + } + } + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/AlertDialogLayout.java b/packages/PackageInstaller/src/com/android/packageinstaller/AlertDialogLayout.java new file mode 100644 index 000000000000..e22171edcf18 --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/AlertDialogLayout.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2016 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; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.AttrRes; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; + +import com.android.packageinstaller.R; + +/** + * Special implementation of linear layout that's capable of laying out alert + * dialog components. + * <p> + * A dialog consists of up to three panels. All panels are optional, and a + * dialog may contain only a single panel. The panels are laid out according + * to the following guidelines: + * <ul> + * <li>topPanel: exactly wrap_content</li> + * <li>contentPanel OR customPanel: at most fill_parent, first priority for + * extra space</li> + * <li>buttonPanel: at least minHeight, at most wrap_content, second + * priority for extra space</li> + * </ul> + */ +public class AlertDialogLayout extends LinearLayout { + + public AlertDialogLayout(@Nullable Context context) { + super(context); + } + + public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AlertDialogLayout(@Nullable Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (!tryOnMeasure(widthMeasureSpec, heightMeasureSpec)) { + // Failed to perform custom measurement, let superclass handle it. + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + private boolean tryOnMeasure(int widthMeasureSpec, int heightMeasureSpec) { + View topPanel = null; + View buttonPanel = null; + View middlePanel = null; + + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + final int id = child.getId(); + switch (id) { + case R.id.topPanel: + topPanel = child; + break; + case R.id.buttonPanel: + buttonPanel = child; + break; + case R.id.contentPanel: + case R.id.customPanel: + if (middlePanel != null) { + // Both the content and custom are visible. Abort! + return false; + } + middlePanel = child; + break; + default: + // Unknown top-level child. Abort! + return false; + } + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int childState = 0; + int usedHeight = getPaddingTop() + getPaddingBottom(); + + if (topPanel != null) { + topPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); + + usedHeight += topPanel.getMeasuredHeight(); + childState = combineMeasuredStates(childState, topPanel.getMeasuredState()); + } + + int buttonHeight = 0; + int buttonWantsHeight = 0; + if (buttonPanel != null) { + buttonPanel.measure(widthMeasureSpec, MeasureSpec.UNSPECIFIED); + buttonHeight = resolveMinimumHeight(buttonPanel); + buttonWantsHeight = buttonPanel.getMeasuredHeight() - buttonHeight; + + usedHeight += buttonHeight; + childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState()); + } + + int middleHeight = 0; + if (middlePanel != null) { + final int childHeightSpec; + if (heightMode == MeasureSpec.UNSPECIFIED) { + childHeightSpec = MeasureSpec.UNSPECIFIED; + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec( + Math.max(0, heightSize - usedHeight), heightMode); + } + + middlePanel.measure(widthMeasureSpec, childHeightSpec); + middleHeight = middlePanel.getMeasuredHeight(); + + usedHeight += middleHeight; + childState = combineMeasuredStates(childState, middlePanel.getMeasuredState()); + } + + int remainingHeight = heightSize - usedHeight; + + // Time for the "real" button measure pass. If we have remaining space, + // make the button pane bigger up to its target height. Otherwise, + // just remeasure the button at whatever height it needs. + if (buttonPanel != null) { + usedHeight -= buttonHeight; + + final int heightToGive = Math.min(remainingHeight, buttonWantsHeight); + if (heightToGive > 0) { + remainingHeight -= heightToGive; + buttonHeight += heightToGive; + } + + final int childHeightSpec = MeasureSpec.makeMeasureSpec( + buttonHeight, MeasureSpec.EXACTLY); + buttonPanel.measure(widthMeasureSpec, childHeightSpec); + + usedHeight += buttonPanel.getMeasuredHeight(); + childState = combineMeasuredStates(childState, buttonPanel.getMeasuredState()); + } + + // If we still have remaining space, make the middle pane bigger up + // to the maximum height. + if (middlePanel != null && remainingHeight > 0) { + usedHeight -= middleHeight; + + final int heightToGive = remainingHeight; + remainingHeight -= heightToGive; + middleHeight += heightToGive; + + // Pass the same height mode as we're using for the dialog itself. + // If it's EXACTLY, then the middle pane MUST use the entire + // height. + final int childHeightSpec = MeasureSpec.makeMeasureSpec( + middleHeight, heightMode); + middlePanel.measure(widthMeasureSpec, childHeightSpec); + + usedHeight += middlePanel.getMeasuredHeight(); + childState = combineMeasuredStates(childState, middlePanel.getMeasuredState()); + } + + // Compute desired width as maximum child width. + int maxWidth = 0; + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != View.GONE) { + maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); + } + } + + maxWidth += getPaddingLeft() + getPaddingRight(); + + final int widthSizeAndState = resolveSizeAndState(maxWidth, widthMeasureSpec, childState); + final int heightSizeAndState = resolveSizeAndState(usedHeight, heightMeasureSpec, 0); + setMeasuredDimension(widthSizeAndState, heightSizeAndState); + + // If the children weren't already measured EXACTLY, we need to run + // another measure pass to for MATCH_PARENT widths. + if (widthMode != MeasureSpec.EXACTLY) { + forceUniformWidth(count, heightMeasureSpec); + } + + return true; + } + + /** + * Remeasures child views to exactly match the layout's measured width. + * + * @param count the number of child views + * @param heightMeasureSpec the original height measure spec + */ + private void forceUniformWidth(int count, int heightMeasureSpec) { + // Pretend that the linear layout has an exact size. + final int uniformMeasureSpec = MeasureSpec.makeMeasureSpec( + getMeasuredWidth(), MeasureSpec.EXACTLY); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.width == LayoutParams.MATCH_PARENT) { + // Temporarily force children to reuse their old measured + // height. + final int oldHeight = lp.height; + lp.height = child.getMeasuredHeight(); + + // Remeasure with new dimensions. + measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0); + lp.height = oldHeight; + } + } + } + } + + /** + * Attempts to resolve the minimum height of a view. + * <p> + * If the view doesn't have a minimum height set and only contains a single + * child, attempts to resolve the minimum height of the child view. + * + * @param v the view whose minimum height to resolve + * @return the minimum height + */ + private int resolveMinimumHeight(View v) { + final int minHeight = v.getMinimumHeight(); + if (minHeight > 0) { + return minHeight; + } + + if (v instanceof ViewGroup) { + final ViewGroup vg = (ViewGroup) v; + if (vg.getChildCount() == 1) { + return resolveMinimumHeight(vg.getChildAt(0)); + } + } + + return 0; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int paddingLeft = getPaddingLeft(); + final int paddingRight = getPaddingRight(); + final int paddingTop = getPaddingTop(); + + // Where right end of child should go + final int width = right - left; + final int childRight = width - paddingRight; + + // Space available for child + final int childSpace = width - paddingLeft - paddingRight; + + final int totalLength = getMeasuredHeight(); + final int count = getChildCount(); + final int gravity = getGravity(); + final int majorGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + final int minorGravity = gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK; + + int childTop; + switch (majorGravity) { + case Gravity.BOTTOM: + // totalLength contains the padding already + childTop = paddingTop + bottom - top - totalLength; + break; + + // totalLength contains the padding already + case Gravity.CENTER_VERTICAL: + childTop = paddingTop + (bottom - top - totalLength) / 2; + break; + + case Gravity.TOP: + default: + childTop = paddingTop; + break; + } + + final Drawable dividerDrawable = getDividerDrawable(); + final int dividerHeight = dividerDrawable == null ? + 0 : dividerDrawable.getIntrinsicHeight(); + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child != null && child.getVisibility() != GONE) { + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + final LayoutParams lp = + (LayoutParams) child.getLayoutParams(); + + int layoutGravity = lp.gravity; + if (layoutGravity < 0) { + layoutGravity = minorGravity; + } + final int layoutDirection = getLayoutDirection(); + final int absoluteGravity = Gravity.getAbsoluteGravity( + layoutGravity, layoutDirection); + + final int childLeft; + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + childLeft = paddingLeft + ((childSpace - childWidth) / 2) + + lp.leftMargin - lp.rightMargin; + break; + + case Gravity.RIGHT: + childLeft = childRight - childWidth - lp.rightMargin; + break; + + case Gravity.LEFT: + default: + childLeft = paddingLeft + lp.leftMargin; + break; + } + + childTop += lp.topMargin; + setChildFrame(child, childLeft, childTop, childWidth, childHeight); + childTop += childHeight + lp.bottomMargin; + } + } + } + + private void setChildFrame(View child, int left, int top, int width, int height) { + child.layout(left, top, left + width, top + height); + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/ButtonBarLayout.java b/packages/PackageInstaller/src/com/android/packageinstaller/ButtonBarLayout.java new file mode 100644 index 000000000000..8d478c4efd3d --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/ButtonBarLayout.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2015 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; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; + +import com.android.packageinstaller.R; + +/** + * An extension of LinearLayout that automatically switches to vertical + * orientation when it can't fit its child views horizontally. + */ +public class ButtonBarLayout extends LinearLayout { + /** Amount of the second button to "peek" above the fold when stacked. */ + private static final int PEEK_BUTTON_DP = 16; + + /** Whether the current configuration allows stacking. */ + private boolean mAllowStacking; + + private int mLastWidthSize = -1; + + private int mMinimumHeight = 0; + + public ButtonBarLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout); + mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, true); + ta.recycle(); + } + + public void setAllowStacking(boolean allowStacking) { + if (mAllowStacking != allowStacking) { + mAllowStacking = allowStacking; + if (!mAllowStacking && getOrientation() == LinearLayout.VERTICAL) { + setStacked(false); + } + requestLayout(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + + if (mAllowStacking) { + if (widthSize > mLastWidthSize && isStacked()) { + // We're being measured wider this time, try un-stacking. + setStacked(false); + } + + mLastWidthSize = widthSize; + } + + boolean needsRemeasure = false; + + // If we're not stacked, make sure the measure spec is AT_MOST rather + // than EXACTLY. This ensures that we'll still get TOO_SMALL so that we + // know to stack the buttons. + final int initialWidthMeasureSpec; + if (!isStacked() && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) { + initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST); + + // We'll need to remeasure again to fill excess space. + needsRemeasure = true; + } else { + initialWidthMeasureSpec = widthMeasureSpec; + } + + super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec); + + if (mAllowStacking && !isStacked()) { + final int measuredWidth = getMeasuredWidthAndState(); + final int measuredWidthState = measuredWidth & MEASURED_STATE_MASK; + if (measuredWidthState == MEASURED_STATE_TOO_SMALL) { + setStacked(true); + + // Measure again in the new orientation. + needsRemeasure = true; + } + } + + if (needsRemeasure) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + // Compute minimum height such that, when stacked, some portion of the + // second button is visible. + int minHeight = 0; + final int firstVisible = getNextVisibleChildIndex(0); + if (firstVisible >= 0) { + final View firstButton = getChildAt(firstVisible); + final LayoutParams firstParams = (LayoutParams) firstButton.getLayoutParams(); + minHeight += getPaddingTop() + firstButton.getMeasuredHeight() + + firstParams.topMargin + firstParams.bottomMargin; + if (isStacked()) { + final int secondVisible = getNextVisibleChildIndex(firstVisible + 1); + if (secondVisible >= 0) { + minHeight += getChildAt(secondVisible).getPaddingTop() + + PEEK_BUTTON_DP * getResources().getDisplayMetrics().density; + } + } else { + minHeight += getPaddingBottom(); + } + } + + if (getMinimumHeight() != minHeight) { + setMinimumHeight(minHeight); + } + } + + private int getNextVisibleChildIndex(int index) { + for (int i = index, count = getChildCount(); i < count; i++) { + if (getChildAt(i).getVisibility() == View.VISIBLE) { + return i; + } + } + return -1; + } + + @Override + public int getMinimumHeight() { + return Math.max(mMinimumHeight, super.getMinimumHeight()); + } + + private void setStacked(boolean stacked) { + setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); + setGravity(stacked ? Gravity.END : Gravity.BOTTOM); + + final View spacer = findViewById(R.id.spacer); + if (spacer != null) { + spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE); + } + + // Reverse the child order. This is specific to the Material button + // bar's layout XML and will probably not generalize. + final int childCount = getChildCount(); + for (int i = childCount - 2; i >= 0; i--) { + bringChildToFront(getChildAt(i)); + } + } + + private boolean isStacked() { + return getOrientation() == LinearLayout.VERTICAL; + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java index 33e5231acee5..4c5875b1f2d9 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/DeleteStagedFileOnResult.java @@ -16,11 +16,12 @@ package com.android.packageinstaller; -import android.annotation.Nullable; import android.app.Activity; import android.content.Intent; import android.os.Bundle; +import androidx.annotation.Nullable; + import java.io.File; /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/DialogTitle.java b/packages/PackageInstaller/src/com/android/packageinstaller/DialogTitle.java new file mode 100644 index 000000000000..068834c5759f --- /dev/null +++ b/packages/PackageInstaller/src/com/android/packageinstaller/DialogTitle.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2008 Google Inc. + * + * 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; + +import android.content.Context; +import android.content.res.TypedArray; +import android.text.Layout; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.TextView; + +import com.android.packageinstaller.R; +/** + * Used by dialogs to change the font size and number of lines to try to fit + * the text to the available space. + */ +public class DialogTitle extends TextView { + + public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public DialogTitle(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public DialogTitle(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialogTitle(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final Layout layout = getLayout(); + if (layout != null) { + final int lineCount = layout.getLineCount(); + if (lineCount > 0) { + final int ellipsisCount = layout.getEllipsisCount(lineCount - 1); + if (ellipsisCount > 0) { + setSingleLine(false); + setMaxLines(2); + + final TypedArray a = getContext().obtainStyledAttributes(null, + R.styleable.TextAppearance, android.R.attr.textAppearanceMedium, + android.R.style.TextAppearance_Medium); + final int textSize = a.getDimensionPixelSize( + R.styleable.TextAppearance_textSize, 0); + if (textSize != 0) { + // textSize is already expressed in pixels + setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + } + a.recycle(); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + } + } +} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java b/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java index 3a94fdcd252d..8639f47bba08 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/EventResultPersister.java @@ -16,8 +16,6 @@ package com.android.packageinstaller; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInstaller; @@ -27,6 +25,9 @@ import android.util.Log; import android.util.SparseArray; import android.util.Xml; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; @@ -40,17 +41,18 @@ import java.nio.charset.StandardCharsets; /** * Persists results of events and calls back observers when a matching result arrives. */ -class EventResultPersister { +public class EventResultPersister { private static final String LOG_TAG = EventResultPersister.class.getSimpleName(); /** Id passed to {@link #addObserver(int, EventResultObserver)} to generate new id */ - static final int GENERATE_NEW_ID = Integer.MIN_VALUE; + 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)} */ - static final String EXTRA_ID = "EventResultPersister.EXTRA_ID"; + public static final String EXTRA_ID = "EventResultPersister.EXTRA_ID"; + public static final String EXTRA_SERVICE_ID = "EventResultPersister.EXTRA_SERVICE_ID"; /** Persisted state of this object */ private final AtomicFile mResultsFile; @@ -89,8 +91,8 @@ class EventResultPersister { } /** Call back when a result is received. Observer is removed when onResult it called. */ - interface EventResultObserver { - void onResult(int status, int legacyStatus, @Nullable String message); + public interface EventResultObserver { + void onResult(int status, int legacyStatus, @Nullable String message, int serviceId); } /** @@ -153,12 +155,14 @@ class EventResultPersister { 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)); + mResults.put(id, new EventResult(status, legacyStatus, statusMessage, + serviceId)); } else { throw new Exception("unexpected tag"); } @@ -190,6 +194,7 @@ class EventResultPersister { 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) { @@ -204,9 +209,9 @@ class EventResultPersister { } if (observerToCall != null) { - observerToCall.onResult(status, legacyStatus, statusMessage); + observerToCall.onResult(status, legacyStatus, statusMessage, serviceId); } else { - mResults.put(id, new EventResult(status, legacyStatus, statusMessage)); + mResults.put(id, new EventResult(status, legacyStatus, statusMessage, serviceId)); writeState(); } } @@ -258,6 +263,8 @@ class EventResultPersister { serializer.attribute(null, "statusMessage", results.valueAt(i).message); } + serializer.attribute(null, "serviceId", + Integer.toString(results.valueAt(i).serviceId)); serializer.endTag(null, "result"); } @@ -311,7 +318,8 @@ class EventResultPersister { if (resultIndex >= 0) { EventResult result = mResults.valueAt(resultIndex); - observer.onResult(result.status, result.legacyStatus, result.message); + observer.onResult(result.status, result.legacyStatus, result.message, + result.serviceId); mResults.removeAt(resultIndex); writeState(); } else { @@ -341,13 +349,15 @@ class EventResultPersister { 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) { + private EventResult(int status, int legacyStatus, @Nullable String message, int serviceId) { this.status = status; this.legacyStatus = legacyStatus; this.message = message; + this.serviceId = serviceId; } } - class OutOfIdsException extends Exception {} + public class OutOfIdsException extends Exception {} } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java index c70d7dbcf518..be8eabbd7100 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallEventReceiver.java @@ -16,11 +16,12 @@ package com.android.packageinstaller; -import android.annotation.NonNull; 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}. */ diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java index 54105bb8432d..3505cfb9d38a 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallFailed.java @@ -16,7 +16,6 @@ package com.android.packageinstaller; -import android.annotation.Nullable; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -32,7 +31,7 @@ import android.os.Bundle; import android.util.Log; import android.view.View; -import com.android.internal.app.AlertActivity; +import androidx.annotation.Nullable; import java.io.File; @@ -79,6 +78,8 @@ public class InstallFailed extends AlertActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setFinishOnTouchOutside(true); + int statusCode = getIntent().getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java index 3aa8dbf495aa..93387e2768ac 100755 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java @@ -16,28 +16,22 @@ package com.android.packageinstaller; -import static android.content.pm.PackageInstaller.SessionParams.UID_UNKNOWN; - -import android.annotation.Nullable; import android.app.PendingIntent; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; +import android.content.pm.PackageInstaller.InstallInfo; import android.content.pm.PackageManager; -import android.content.pm.parsing.ApkLiteParseUtils; -import android.content.pm.parsing.PackageLite; -import android.content.pm.parsing.result.ParseResult; -import android.content.pm.parsing.result.ParseTypeImpl; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.os.Process; import android.util.Log; import android.view.View; import android.widget.Button; -import com.android.internal.app.AlertActivity; -import com.android.internal.content.InstallLocationUtils; +import androidx.annotation.Nullable; import java.io.File; import java.io.FileInputStream; @@ -139,34 +133,30 @@ public class InstallInstalling extends AlertActivity { params.setOriginatingUri(getIntent() .getParcelableExtra(Intent.EXTRA_ORIGINATING_URI)); params.setOriginatingUid(getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, - UID_UNKNOWN)); + Process.INVALID_UID)); params.setInstallerPackageName(getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME)); params.setInstallReason(PackageManager.INSTALL_REASON_USER); File file = new File(mPackageURI.getPath()); try { - final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); - final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite( - input.reset(), file, /* flags */ 0); - if (result.isError()) { - Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults."); - Log.e(LOG_TAG, - "Cannot calculate installed size " + file + ". Try only apk size."); + final InstallInfo result = getPackageManager().getPackageInstaller() + .getInstallInfo(file, 0); + params.setAppPackageName(result.getPackageName()); + params.setInstallLocation(result.getInstallLocation()); + try { + params.setSize(result.calculateInstalledSize(params)); + } catch (IOException e) { + e.printStackTrace(); params.setSize(file.length()); - } else { - final PackageLite pkg = result.getResult(); - params.setAppPackageName(pkg.getPackageName()); - params.setInstallLocation(pkg.getInstallLocation()); - params.setSize(InstallLocationUtils.calculateInstalledSize(pkg, - params.abiOverride)); } - } catch (IOException e) { + } catch (PackageInstaller.PackageParsingException e) { + + Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults.", e); Log.e(LOG_TAG, "Cannot calculate installed size " + file + ". Try only apk size."); params.setSize(file.length()); } - try { mInstallId = InstallEventReceiver .addObserver(this, EventResultPersister.GENERATE_NEW_ID, @@ -281,8 +271,11 @@ public class InstallInstalling extends AlertActivity { * @param statusCode The installation result. * @param legacyStatus The installation as used internally in the package manager. * @param statusMessage The detailed installation result. + * @param serviceId Id for PowerManager.WakeLock service. Used only by Wear devices + * during an uninstall. */ - private void launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage) { + private void launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage, + int serviceId /* ignore */) { if (statusCode == PackageInstaller.STATUS_SUCCESS) { launchSuccess(); } else { @@ -292,7 +285,7 @@ public class InstallInstalling extends AlertActivity { /** * Send the package to the package installer and then register a event result observer that - * will call {@link #launchFinishBasedOnResult(int, int, String)} + * will call {@link #launchFinishBasedOnResult(int, int, String, int)} */ private final class InstallingAsyncTask extends AsyncTask<Void, Void, PackageInstaller.Session> { @@ -318,6 +311,7 @@ public class InstallInstalling extends AlertActivity { try (InputStream in = new FileInputStream(file)) { long sizeBytes = file.length(); + long totalRead = 0; try (OutputStream out = session .openWrite("PackageInstaller", 0, sizeBytes)) { byte[] buffer = new byte[1024 * 1024]; @@ -336,8 +330,9 @@ public class InstallInstalling extends AlertActivity { out.write(buffer, 0, numRead); if (sizeBytes > 0) { - float fraction = ((float) numRead / (float) sizeBytes); - session.addProgress(fraction); + totalRead += numRead; + float fraction = ((float) totalRead / (float) sizeBytes); + session.setStagingProgress(fraction); } } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java index b6b88375a0a2..68de2f67bd94 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java @@ -16,7 +16,6 @@ package com.android.packageinstaller; -import android.annotation.Nullable; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; @@ -31,7 +30,7 @@ import android.os.Bundle; import android.util.Log; import android.view.View; -import com.android.internal.app.AlertActivity; +import androidx.annotation.Nullable; import java.io.File; import java.io.FileOutputStream; @@ -58,6 +57,7 @@ public class InstallStaging extends AlertActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setFinishOnTouchOutside(true); mAlert.setIcon(R.drawable.ic_file_download); mAlert.setTitle(getString(R.string.app_name_unknown)); mAlert.setView(R.layout.install_content_view); @@ -123,7 +123,8 @@ public class InstallStaging extends AlertActivity { * Show an error message and set result as error. */ private void showError() { - (new ErrorDialog()).showAllowingStateLoss(getFragmentManager(), "error"); + getFragmentManager().beginTransaction() + .add(new ErrorDialog(), "error").commitAllowingStateLoss(); Intent result = new Intent(); result.putExtra(Intent.EXTRA_INSTALL_RESULT, diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index 88c10365219b..7338e645440c 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -19,26 +19,25 @@ package com.android.packageinstaller; import static com.android.packageinstaller.PackageUtil.getMaxTargetSdkVersionForUid; import android.Manifest; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.Activity; -import android.app.ActivityManager; import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ProviderInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.RemoteException; +import android.os.Process; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.util.Arrays; /** @@ -80,12 +79,11 @@ public class InstallStart extends Activity { final ApplicationInfo sourceInfo = getSourceInfo(callingPackage); final int originatingUid = getOriginatingUid(sourceInfo); boolean isTrustedSource = false; - if (sourceInfo != null - && (sourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) { + if (sourceInfo != null && sourceInfo.isPrivilegedApp()) { isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false); } - if (!isTrustedSource && originatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) { + if (!isTrustedSource && originatingUid != Process.INVALID_UID) { final int targetSdkVersion = getMaxTargetSdkVersionForUid(this, originatingUid); if (targetSdkVersion < 0) { Log.w(LOG_TAG, "Cannot get target sdk version for uid " + originatingUid); @@ -99,6 +97,10 @@ public class InstallStart extends Activity { } } + if (sessionId != -1 && !isCallerSessionOwner(originatingUid, sessionId)) { + mAbortInstall = true; + } + final String installerPackageNameFromIntent = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME); if (installerPackageNameFromIntent != null) { @@ -208,30 +210,27 @@ public class InstallStart extends Activity { } /** - * Get the originating uid if possible, or - * {@link android.content.pm.PackageInstaller.SessionParams#UID_UNKNOWN} if not available + * Get the originating uid if possible, or {@link Process#INVALID_UID} if not available * * @param sourceInfo The source of this installation - * @return The UID of the installation source or UID_UNKNOWN + * @return The UID of the installation source or INVALID_UID */ private int getOriginatingUid(@Nullable ApplicationInfo sourceInfo) { // The originating uid from the intent. We only trust/use this if it comes from either // the document manager app or the downloads provider final int uidFromIntent = getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, - PackageInstaller.SessionParams.UID_UNKNOWN); + Process.INVALID_UID); final int callingUid; if (sourceInfo != null) { callingUid = sourceInfo.uid; } else { - try { - callingUid = ActivityManager.getService() - .getLaunchedFromUid(getActivityToken()); - } catch (RemoteException ex) { + callingUid = getLaunchedFromUid(); + if (callingUid == Process.INVALID_UID) { // Cannot reach ActivityManager. Aborting install. Log.e(LOG_TAG, "Could not determine the launching uid."); mAbortInstall = true; - return PackageInstaller.SessionParams.UID_UNKNOWN; + return Process.INVALID_UID; } } if (checkPermission(Manifest.permission.MANAGE_DOCUMENTS, -1, callingUid) @@ -253,7 +252,8 @@ public class InstallStart extends Activity { return false; } final ApplicationInfo appInfo = downloadProviderPackage.applicationInfo; - return (appInfo.isSystemApp() && uid == appInfo.uid); + return ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 + && uid == appInfo.uid); } @NonNull @@ -268,8 +268,14 @@ public class InstallStart extends Activity { try { return mPackageManager.canPackageQuery(callingPackage, targetPackage); - } catch (NameNotFoundException e) { + } catch (PackageManager.NameNotFoundException e) { return false; } } + + private boolean isCallerSessionOwner(int originatingUid, int sessionId) { + PackageInstaller packageInstaller = getPackageManager().getPackageInstaller(); + int installerUid = packageInstaller.getSessionInfo(sessionId).getInstallerUid(); + return (originatingUid == Process.ROOT_UID) || (originatingUid == installerUid); + } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java index 38c06dd48b85..73c03a57cade 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallSuccess.java @@ -16,7 +16,6 @@ package com.android.packageinstaller; -import android.annotation.Nullable; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.DialogInterface; @@ -30,7 +29,7 @@ import android.util.Log; import android.view.View; import android.widget.Button; -import com.android.internal.app.AlertActivity; +import androidx.annotation.Nullable; import java.io.File; import java.util.List; @@ -54,6 +53,8 @@ public class InstallSuccess extends AlertActivity { protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setFinishOnTouchOutside(true); + if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { // Return result if requested Intent result = new Intent(); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java index eea12eca1859..228a9f5788d5 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledNotificationUtils.java @@ -16,7 +16,6 @@ package com.android.packageinstaller; -import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -24,7 +23,6 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -32,8 +30,11 @@ import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; + /** * A util class that handle and post new app installed notifications. */ @@ -107,8 +108,8 @@ class PackageInstalledNotificationUtils { @NonNull String packageName) { CharSequence label = appInfo.loadSafeLabel(context.getPackageManager(), DEFAULT_MAX_LABEL_SIZE_PX, - PackageItemInfo.SAFE_LABEL_FLAG_TRIM - | PackageItemInfo.SAFE_LABEL_FLAG_FIRST_LINE).toString(); + TextUtils.SAFE_STRING_FLAG_TRIM + | TextUtils.SAFE_STRING_FLAG_FIRST_LINE).toString(); if (label != null) { return label.toString(); } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java index 74c7b5809337..2278f7c36574 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstalledReceiver.java @@ -20,7 +20,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.provider.Settings; import android.util.Log; /** @@ -33,8 +32,7 @@ public class PackageInstalledReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - if (Settings.Global.getInt(context.getContentResolver(), - Settings.Global.SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED, 0) == 0) { + if (!context.getPackageManager().shouldShowNewAppInstalledNotification()) { return; } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index fa936706cf22..313815839f53 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -21,11 +21,8 @@ import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; import android.Manifest; -import android.annotation.NonNull; -import android.annotation.StringRes; import android.app.Activity; import android.app.AlertDialog; -import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.Dialog; import android.app.DialogFragment; @@ -36,24 +33,26 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageManager; 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.NameNotFoundException; -import android.content.pm.UserInfo; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.Process; +import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; import android.view.View; import android.widget.Button; -import com.android.internal.app.AlertActivity; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; import java.io.File; import java.util.ArrayList; @@ -86,13 +85,12 @@ public class PackageInstallerActivity extends AlertActivity { private Uri mPackageURI; private Uri mOriginatingURI; private Uri mReferrerURI; - private int mOriginatingUid = PackageInstaller.SessionParams.UID_UNKNOWN; + private int mOriginatingUid = Process.INVALID_UID; private String mOriginatingPackage; // The package name corresponding to #mOriginatingUid private int mActivityResultCode = Activity.RESULT_CANCELED; private final boolean mLocalLOGV = false; PackageManager mPm; - IPackageManager mIpm; AppOpsManager mAppOpsManager; UserManager mUserManager; PackageInstaller mInstaller; @@ -166,7 +164,8 @@ public class PackageInstallerActivity extends AlertActivity { DialogFragment newDialog = createDialog(id); if (newDialog != null) { - newDialog.showAllowingStateLoss(getFragmentManager(), "dialog"); + getFragmentManager().beginTransaction() + .add(newDialog, "dialog").commitAllowingStateLoss(); } } @@ -211,9 +210,9 @@ public class PackageInstallerActivity extends AlertActivity { // Log the fact that the app is requesting an install, and is now allowed to do it // (before this point we could only log that it's requesting an install, but isn't // allowed to do it yet). - int appOpCode = - AppOpsManager.permissionToOpCode(Manifest.permission.REQUEST_INSTALL_PACKAGES); - mAppOpsManager.noteOpNoThrow(appOpCode, mOriginatingUid, mOriginatingPackage, + String appOpStr = + AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES); + mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid, mOriginatingPackage, mCallingAttributionTag, "Successfully started package installation activity"); @@ -250,12 +249,9 @@ public class PackageInstallerActivity extends AlertActivity { private boolean isInstallRequestFromUnknownSource(Intent intent) { if (mCallingPackage != null && intent.getBooleanExtra( Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) { - if (mSourceInfo != null) { - if ((mSourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) - != 0) { - // Privileged apps can bypass unknown sources check if they want. - return false; - } + if (mSourceInfo != null && mSourceInfo.isPrivilegedApp()) { + // Privileged apps can bypass unknown sources check if they want. + return false; } } return true; @@ -305,7 +301,7 @@ public class PackageInstallerActivity extends AlertActivity { @Override protected void onCreate(Bundle icicle) { - if (mLocalLOGV) Log.i(TAG, "creating for user " + getUserId()); + if (mLocalLOGV) Log.i(TAG, "creating for user " + UserHandle.myUserId()); getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); super.onCreate(null); @@ -316,7 +312,6 @@ public class PackageInstallerActivity extends AlertActivity { setFinishOnTouchOutside(true); mPm = getPackageManager(); - mIpm = AppGlobals.getPackageManager(); mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); mInstaller = mPm.getPackageInstaller(); mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); @@ -328,8 +323,8 @@ public class PackageInstallerActivity extends AlertActivity { mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG); mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO); mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, - PackageInstaller.SessionParams.UID_UNKNOWN); - mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) + Process.INVALID_UID); + mOriginatingPackage = (mOriginatingUid != Process.INVALID_UID) ? getPackageNameForUid(mOriginatingUid) : null; final Object packageSource; @@ -337,21 +332,23 @@ public class PackageInstallerActivity extends AlertActivity { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1 /* defaultValue */); final SessionInfo info = mInstaller.getSessionInfo(sessionId); - if (info == null || !info.sealed || info.resolvedBaseCodePath == null) { + final String resolvedBaseCodePath = intent.getStringExtra( + PackageInstaller.EXTRA_RESOLVED_BASE_PATH); + if (info == null || !info.isSealed() || resolvedBaseCodePath == null) { Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring"); finish(); return; } mSessionId = sessionId; - packageSource = Uri.fromFile(new File(info.resolvedBaseCodePath)); + packageSource = Uri.fromFile(new File(resolvedBaseCodePath)); mOriginatingURI = null; mReferrerURI = null; } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1 /* defaultValue */); final SessionInfo info = mInstaller.getSessionInfo(sessionId); - if (info == null || !info.isPreapprovalRequested) { + if (info == null || !info.getIsPreApprovalRequested()) { Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring"); finish(); return; @@ -547,15 +544,15 @@ public class PackageInstallerActivity extends AlertActivity { return; } // Shouldn't use static constant directly, see b/65534401. - final int appOpCode = - AppOpsManager.permissionToOpCode(Manifest.permission.REQUEST_INSTALL_PACKAGES); - final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpCode, mOriginatingUid, + final String appOpStr = + AppOpsManager.permissionToOp(Manifest.permission.REQUEST_INSTALL_PACKAGES); + final int appOpMode = mAppOpsManager.noteOpNoThrow(appOpStr, mOriginatingUid, mOriginatingPackage, mCallingAttributionTag, "Started package installation activity"); if (mLocalLOGV) Log.i(TAG, "handleUnknownSources(): appMode=" + appOpMode); switch (appOpMode) { case AppOpsManager.MODE_DEFAULT: - mAppOpsManager.setMode(appOpCode, mOriginatingUid, + mAppOpsManager.setMode(appOpStr, mOriginatingUid, mOriginatingPackage, AppOpsManager.MODE_ERRORED); // fall through case AppOpsManager.MODE_ERRORED: @@ -588,8 +585,8 @@ public class PackageInstallerActivity extends AlertActivity { switch (scheme) { case SCHEME_PACKAGE: { - for (UserInfo info : mUserManager.getUsers()) { - PackageManager pmForUser = createContextAsUser(info.getUserHandle(), 0) + for (UserHandle handle : mUserManager.getUserHandles(true)) { + PackageManager pmForUser = createContextAsUser(handle, 0) .getPackageManager(); try { if (pmForUser.canPackageQuery(mCallingPackage, packageName)) { @@ -645,9 +642,9 @@ public class PackageInstallerActivity extends AlertActivity { * @return {@code true} iff the installer could be set up */ private boolean processSessionInfo(@NonNull SessionInfo info) { - mPkgInfo = generateStubPackageInfo(info.appPackageName); - mAppSnippet = new PackageUtil.AppSnippet(info.appLabel, - info.appIcon != null ? new BitmapDrawable(getResources(), info.appIcon) + mPkgInfo = generateStubPackageInfo(info.getAppPackageName()); + mAppSnippet = new PackageUtil.AppSnippet(info.getAppLabel(), + info.getAppIcon() != null ? new BitmapDrawable(getResources(), info.getAppIcon()) : getPackageManager().getDefaultActivityIcon()); return true; } @@ -693,7 +690,7 @@ public class PackageInstallerActivity extends AlertActivity { if (mReferrerURI != null) { newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI); } - if (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) { + if (mOriginatingUid != Process.INVALID_UID) { newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid); } if (installerPackageName != null) { @@ -829,7 +826,7 @@ public class PackageInstallerActivity extends AlertActivity { if (isDestroyed()) { return; } - getMainThreadHandler().postDelayed(() -> { + new Handler(Looper.getMainLooper()).postDelayed(() -> { if (!isDestroyed()) { startActivity(getIntent().addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT)); } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java index f5570dfff7ca..698159f24bb7 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java @@ -17,14 +17,11 @@ package com.android.packageinstaller; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.Activity; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.res.AssetManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.UserHandle; @@ -33,6 +30,9 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import java.io.File; /** @@ -131,16 +131,15 @@ public class PackageUtil { public static AppSnippet getAppSnippet( Activity pContext, ApplicationInfo appInfo, File sourceFile) { final String archiveFilePath = sourceFile.getAbsolutePath(); - Resources pRes = pContext.getResources(); - AssetManager assmgr = new AssetManager(); - assmgr.addAssetPath(archiveFilePath); - Resources res = new Resources(assmgr, pRes.getDisplayMetrics(), pRes.getConfiguration()); + PackageManager pm = pContext.getPackageManager(); + appInfo.publicSourceDir = archiveFilePath; + 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 = res.getText(appInfo.labelRes); + label = appInfo.loadLabel(pm); } catch (Resources.NotFoundException e) { } } @@ -154,7 +153,7 @@ public class PackageUtil { try { if (appInfo.icon != 0) { try { - icon = res.getDrawable(appInfo.icon); + icon = appInfo.loadIcon(pm); } catch (Resources.NotFoundException e) { } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java b/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java index f77318cf6e23..afb2ea473388 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/TemporaryFileManager.java @@ -16,13 +16,14 @@ package com.android.packageinstaller; -import android.annotation.NonNull; 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; diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java index c3e9c23cda26..86b0321fdb13 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallEventReceiver.java @@ -16,11 +16,12 @@ package com.android.packageinstaller; -import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import androidx.annotation.NonNull; + /** * Receives uninstall events and persists them using a {@link EventResultPersister}. */ @@ -58,7 +59,7 @@ public class UninstallEventReceiver extends BroadcastReceiver { * * @return The id for this event */ - static int addObserver(@NonNull Context context, int id, + public static int addObserver(@NonNull Context context, int id, @NonNull EventResultPersister.EventResultObserver observer) throws EventResultPersister.OutOfIdsException { return getReceiver(context).addObserver(id, observer); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java index 973ab8956304..b9552fc4e143 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallFinish.java @@ -16,29 +16,27 @@ package com.android.packageinstaller; -import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.admin.IDevicePolicyManager; +import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageManager; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; -import android.content.pm.UserInfo; import android.graphics.drawable.Icon; -import android.os.RemoteException; -import android.os.ServiceManager; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Log; import android.widget.Toast; +import androidx.annotation.NonNull; + import java.util.List; /** @@ -94,28 +92,24 @@ public class UninstallFinish extends BroadcastReceiver { switch (legacyStatus) { case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER: { - IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface( - ServiceManager.getService(Context.DEVICE_POLICY_SERVICE)); // Find out if the package is an active admin for some non-current user. - int myUserId = UserHandle.myUserId(); - UserInfo otherBlockingUser = null; - for (UserInfo user : userManager.getUsers()) { + UserHandle myUserHandle = Process.myUserHandle(); + UserHandle otherBlockingUserHandle = null; + for (UserHandle otherUserHandle : userManager.getUserHandles(true)) { // We only catch the case when the user in question is neither the // current user nor its profile. - if (isProfileOfOrSame(userManager, myUserId, user.id)) { + if (isProfileOfOrSame(userManager, myUserHandle, otherUserHandle)) { continue; } - - try { - if (dpm.packageHasActiveAdmins(appInfo.packageName, user.id)) { - otherBlockingUser = user; - break; - } - } catch (RemoteException e) { - Log.e(LOG_TAG, "Failed to talk to package manager", e); + DevicePolicyManager dpm = + context.createContextAsUser(otherUserHandle, 0) + .getSystemService(DevicePolicyManager.class); + if (dpm.packageHasActiveAdmins(appInfo.packageName)) { + otherBlockingUserHandle = otherUserHandle; + break; } } - if (otherBlockingUser == null) { + if (otherBlockingUserHandle == null) { Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName + " is a device admin"); @@ -124,46 +118,41 @@ public class UninstallFinish extends BroadcastReceiver { R.string.uninstall_failed_device_policy_manager)); } else { Log.d(LOG_TAG, "Uninstall failed because " + appInfo.packageName - + " is a device admin of user " + otherBlockingUser); + + " is a device admin of user " + otherBlockingUserHandle); + String userName = + context.createContextAsUser(otherBlockingUserHandle, 0) + .getSystemService(UserManager.class).getUserName(); setBigText(uninstallFailedNotification, String.format(context.getString( R.string.uninstall_failed_device_policy_manager_of_user), - otherBlockingUser.name)); + userName)); } break; } case PackageManager.DELETE_FAILED_OWNER_BLOCKED: { - IPackageManager packageManager = IPackageManager.Stub.asInterface( - ServiceManager.getService("package")); - - List<UserInfo> users = userManager.getUsers(); - int blockingUserId = UserHandle.USER_NULL; - for (int i = 0; i < users.size(); ++i) { - final UserInfo user = users.get(i); - try { - if (packageManager.getBlockUninstallForUser(appInfo.packageName, - user.id)) { - blockingUserId = user.id; - break; - } - } catch (RemoteException e) { - // Shouldn't happen. - Log.e(LOG_TAG, "Failed to talk to package manager", e); + PackageManager packageManager = context.getPackageManager(); + List<UserHandle> userHandles = userManager.getUserHandles(true); + UserHandle otherBlockingUserHandle = null; + for (int i = 0; i < userHandles.size(); ++i) { + final UserHandle handle = userHandles.get(i); + if (packageManager.canUserUninstall(appInfo.packageName, handle)) { + otherBlockingUserHandle = handle; + break; } } - int myUserId = UserHandle.myUserId(); - if (isProfileOfOrSame(userManager, myUserId, blockingUserId)) { + UserHandle myUserHandle = Process.myUserHandle(); + if (isProfileOfOrSame(userManager, myUserHandle, otherBlockingUserHandle)) { addDeviceManagerButton(context, uninstallFailedNotification); } else { addManageUsersButton(context, uninstallFailedNotification); } - if (blockingUserId == UserHandle.USER_NULL) { + if (otherBlockingUserHandle == null) { Log.d(LOG_TAG, "Uninstall failed for " + appInfo.packageName + " with code " + returnCode + " no blocking user"); - } else if (blockingUserId == UserHandle.USER_SYSTEM) { + } else if (otherBlockingUserHandle == UserHandle.SYSTEM) { setBigText(uninstallFailedNotification, context.getString(R.string.uninstall_blocked_device_owner)); } else { @@ -200,18 +189,18 @@ public class UninstallFinish extends BroadcastReceiver { * Is a profile part of a user? * * @param userManager The user manager - * @param userId The id of the user - * @param profileId The id of the profile + * @param userHandle The handle of the user + * @param profileHandle The handle of the profile * * @return If the profile is part of the user or the profile parent of the user */ - private boolean isProfileOfOrSame(@NonNull UserManager userManager, int userId, int profileId) { - if (userId == profileId) { + private boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle, + UserHandle profileHandle) { + if (userHandle.equals(profileHandle)) { return true; } - - UserInfo parentUser = userManager.getProfileParent(profileId); - return parentUser != null && parentUser.id == userId; + return userManager.getProfileParent(profileHandle) != null + && userManager.getProfileParent(profileHandle).equals(userHandle); } /** diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java index 1485352c0f52..b60aba8be6f7 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java @@ -16,9 +16,7 @@ package com.android.packageinstaller; -import android.annotation.Nullable; import android.app.Activity; -import android.app.ActivityThread; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; @@ -27,19 +25,18 @@ import android.app.FragmentTransaction; import android.app.PendingIntent; import android.content.Intent; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageDeleteObserver2; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.os.Bundle; -import android.os.IBinder; import android.os.Process; -import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; import android.widget.Toast; +import androidx.annotation.Nullable; + /** * Start an uninstallation, show a dialog while uninstalling and return result to the caller. */ @@ -57,7 +54,7 @@ public class UninstallUninstalling extends Activity implements private int mUninstallId; private ApplicationInfo mAppInfo; - private IBinder mCallback; + private PackageManager.UninstallCompleteCallback mCallback; private boolean mReturnResult; private String mLabel; @@ -68,7 +65,8 @@ public class UninstallUninstalling extends Activity implements setFinishOnTouchOutside(false); mAppInfo = getIntent().getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); - mCallback = getIntent().getIBinderExtra(PackageInstaller.EXTRA_CALLBACK); + mCallback = getIntent().getParcelableExtra(PackageInstaller.EXTRA_CALLBACK, + PackageManager.UninstallCompleteCallback.class); mReturnResult = getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false); mLabel = getIntent().getStringExtra(EXTRA_APP_LABEL); @@ -119,15 +117,10 @@ public class UninstallUninstalling extends Activity implements int flags = allUsers ? PackageManager.DELETE_ALL_USERS : 0; flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0; - try { - ActivityThread.getPackageManager().getPackageInstaller().uninstall( - new VersionedPackage(mAppInfo.packageName, - PackageManager.VERSION_CODE_HIGHEST), - getPackageName(), flags, pendingIntent.getIntentSender(), - user.getIdentifier()); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } + getPackageManager().getPackageInstaller().uninstall( + new VersionedPackage(mAppInfo.packageName, + PackageManager.VERSION_CODE_HIGHEST), + flags, pendingIntent.getIntentSender()); } else { mUninstallId = savedInstanceState.getInt(UNINSTALL_ID); UninstallEventReceiver.addObserver(this, mUninstallId, this); @@ -135,7 +128,7 @@ public class UninstallUninstalling extends Activity implements } catch (EventResultPersister.OutOfIdsException | IllegalArgumentException e) { Log.e(LOG_TAG, "Fails to start uninstall", e); onResult(PackageInstaller.STATUS_FAILURE, PackageManager.DELETE_FAILED_INTERNAL_ERROR, - null); + null, 0); } } @@ -152,15 +145,10 @@ public class UninstallUninstalling extends Activity implements } @Override - public void onResult(int status, int legacyStatus, @Nullable String message) { + public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) { if (mCallback != null) { // The caller will be informed about the result via a callback - final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub - .asInterface(mCallback); - try { - observer.onPackageDeleted(mAppInfo.packageName, legacyStatus, message); - } catch (RemoteException ignored) { - } + mCallback.onUninstallComplete(mAppInfo.packageName, legacyStatus, message); } else if (mReturnResult) { // The caller will be informed about the result and might decide to display it Intent result = new Intent(); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java index 0198168f9fda..04496b91c978 100755 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallerActivity.java @@ -22,12 +22,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static com.android.packageinstaller.PackageUtil.getMaxTargetSdkVersionForUid; import android.Manifest; -import android.annotation.NonNull; -import android.annotation.StringRes; import android.app.Activity; -import android.app.ActivityManager; -import android.app.ActivityThread; -import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.DialogFragment; import android.app.Fragment; @@ -37,12 +32,9 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ComponentName; -import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageDeleteObserver2; -import android.content.pm.IPackageManager; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; @@ -50,13 +42,14 @@ import android.content.res.Configuration; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; -import android.os.ServiceManager; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + import com.android.packageinstaller.handheld.ErrorDialogFragment; import com.android.packageinstaller.handheld.UninstallAlertDialogFragment; import com.android.packageinstaller.television.ErrorFragment; @@ -80,7 +73,7 @@ public class UninstallerActivity extends Activity { public ActivityInfo activityInfo; public boolean allUsers; public UserHandle user; - public IBinder callback; + public PackageManager.UninstallCompleteCallback callback; } private String mPackageName; @@ -94,46 +87,42 @@ public class UninstallerActivity extends Activity { // be stale, if e.g. the app was uninstalled while the activity was destroyed. super.onCreate(null); - try { - int callingUid = ActivityManager.getService().getLaunchedFromUid(getActivityToken()); - - String callingPackage = getPackageNameForUid(callingUid); - if (callingPackage == null) { - Log.e(TAG, "Package not found for originating uid " + callingUid); - setResult(Activity.RESULT_FIRST_USER); - finish(); - return; - } else { - AppOpsManager appOpsManager = (AppOpsManager) getSystemService( - Context.APP_OPS_SERVICE); - if (appOpsManager.noteOpNoThrow( - AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage) - != MODE_ALLOWED) { - Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps"); - setResult(Activity.RESULT_FIRST_USER); - finish(); - return; - } - } + int callingUid = getLaunchedFromUid(); + if (callingUid == Process.INVALID_UID) { + // Cannot reach Package/ActivityManager. Aborting uninstall. + Log.e(TAG, "Could not determine the launching uid."); - if (getMaxTargetSdkVersionForUid(this, callingUid) - >= Build.VERSION_CODES.P && AppGlobals.getPackageManager().checkUidPermission( - Manifest.permission.REQUEST_DELETE_PACKAGES, callingUid) - != PackageManager.PERMISSION_GRANTED - && AppGlobals.getPackageManager().checkUidPermission( - Manifest.permission.DELETE_PACKAGES, callingUid) - != PackageManager.PERMISSION_GRANTED) { - Log.e(TAG, "Uid " + callingUid + " does not have " - + Manifest.permission.REQUEST_DELETE_PACKAGES + " or " - + Manifest.permission.DELETE_PACKAGES); + setResult(Activity.RESULT_FIRST_USER); + finish(); + return; + } + String callingPackage = getPackageNameForUid(callingUid); + if (callingPackage == null) { + Log.e(TAG, "Package not found for originating uid " + callingUid); + setResult(Activity.RESULT_FIRST_USER); + finish(); + return; + } else { + AppOpsManager appOpsManager = getSystemService(AppOpsManager.class); + if (appOpsManager.noteOpNoThrow( + AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage) + != MODE_ALLOWED) { + Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps"); setResult(Activity.RESULT_FIRST_USER); finish(); return; } - } catch (RemoteException ex) { - // Cannot reach Package/ActivityManager. Aborting uninstall. - Log.e(TAG, "Could not determine the launching uid."); + } + + if (getMaxTargetSdkVersionForUid(this, callingUid) >= Build.VERSION_CODES.P + && getBaseContext().checkPermission(Manifest.permission.REQUEST_DELETE_PACKAGES, + 0 /* random value for pid */, callingUid) != PackageManager.PERMISSION_GRANTED + && getBaseContext().checkPermission(Manifest.permission.DELETE_PACKAGES, + 0 /* random value for pid */, callingUid) != PackageManager.PERMISSION_GRANTED) { + Log.e(TAG, "Uid " + callingUid + " does not have " + + Manifest.permission.REQUEST_DELETE_PACKAGES + " or " + + Manifest.permission.DELETE_PACKAGES); setResult(Activity.RESULT_FIRST_USER); finish(); @@ -157,37 +146,37 @@ public class UninstallerActivity extends Activity { return; } - final IPackageManager pm = IPackageManager.Stub.asInterface( - ServiceManager.getService("package")); + PackageManager pm = getPackageManager(); + UserManager userManager = getBaseContext().getSystemService(UserManager.class); mDialogInfo = new DialogInfo(); mDialogInfo.allUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false); - if (mDialogInfo.allUsers && !UserManager.get(this).isAdminUser()) { + if (mDialogInfo.allUsers && !userManager.isAdminUser()) { Log.e(TAG, "Only admin user can request uninstall for all users"); showUserIsNotAllowed(); return; } mDialogInfo.user = intent.getParcelableExtra(Intent.EXTRA_USER); if (mDialogInfo.user == null) { - mDialogInfo.user = android.os.Process.myUserHandle(); + mDialogInfo.user = Process.myUserHandle(); } else { - UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); List<UserHandle> profiles = userManager.getUserProfiles(); if (!profiles.contains(mDialogInfo.user)) { - Log.e(TAG, "User " + android.os.Process.myUserHandle() + " can't request uninstall " + Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall " + "for user " + mDialogInfo.user); showUserIsNotAllowed(); return; } } - mDialogInfo.callback = intent.getIBinderExtra(PackageInstaller.EXTRA_CALLBACK); + mDialogInfo.callback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK, + PackageManager.UninstallCompleteCallback.class); try { mDialogInfo.appInfo = pm.getApplicationInfo(mPackageName, - PackageManager.MATCH_ANY_USER, mDialogInfo.user.getIdentifier()); - } catch (RemoteException e) { + PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER)); + } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Unable to get packageName. Package manager is dead?"); } @@ -202,9 +191,9 @@ public class UninstallerActivity extends Activity { if (className != null) { try { mDialogInfo.activityInfo = pm.getActivityInfo( - new ComponentName(mPackageName, className), 0, - mDialogInfo.user.getIdentifier()); - } catch (RemoteException e) { + new ComponentName(mPackageName, className), + PackageManager.ComponentInfoFlags.of(0)); + } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Unable to get className. Package manager is dead?"); // Continue as the ActivityInfo isn't critical. } @@ -315,7 +304,6 @@ public class UninstallerActivity extends Activity { newIntent.putExtra(UninstallUninstalling.EXTRA_APP_LABEL, label); newIntent.putExtra(UninstallUninstalling.EXTRA_KEEP_DATA, keepData); newIntent.putExtra(PackageInstaller.EXTRA_CALLBACK, mDialogInfo.callback); - if (returnResult) { newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); } @@ -366,11 +354,10 @@ public class UninstallerActivity extends Activity { int flags = mDialogInfo.allUsers ? PackageManager.DELETE_ALL_USERS : 0; flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0; - ActivityThread.getPackageManager().getPackageInstaller().uninstall( + getPackageManager().getPackageInstaller().uninstall( new VersionedPackage(mDialogInfo.appInfo.packageName, PackageManager.VERSION_CODE_HIGHEST), - getPackageName(), flags, pendingIntent.getIntentSender(), - mDialogInfo.user.getIdentifier()); + flags, pendingIntent.getIntentSender()); } catch (Exception e) { notificationManager.cancel(uninstallId); @@ -382,13 +369,8 @@ public class UninstallerActivity extends Activity { public void dispatchAborted() { if (mDialogInfo != null && mDialogInfo.callback != null) { - final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub.asInterface( - mDialogInfo.callback); - try { - observer.onPackageDeleted(mPackageName, - PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user"); - } catch (RemoteException ignored) { - } + mDialogInfo.callback.onUninstallComplete(mPackageName, + PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user"); } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java index a1bc9928ae69..21f4be0004f4 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java @@ -16,10 +16,10 @@ package com.android.packageinstaller.handheld; +import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; +import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.text.format.Formatter.formatFileSize; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; @@ -29,7 +29,6 @@ import android.content.DialogInterface; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.UserInfo; import android.os.Bundle; import android.os.Process; import android.os.UserHandle; @@ -41,6 +40,9 @@ import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.packageinstaller.R; import com.android.packageinstaller.UninstallerActivity; @@ -91,11 +93,11 @@ public class UninstallAlertDialogFragment extends DialogFragment implements long appDataSize = 0; if (user == null) { - List<UserInfo> users = userManager.getUsers(); + List<UserHandle> userHandles = userManager.getUserHandles(true); - int numUsers = users.size(); + int numUsers = userHandles.size(); for (int i = 0; i < numUsers; i++) { - appDataSize += getAppDataSizeForUser(pkg, UserHandle.of(users.get(i).id)); + appDataSize += getAppDataSizeForUser(pkg, userHandles.get(i)); } } else { appDataSize = getAppDataSizeForUser(pkg, user); @@ -128,7 +130,7 @@ public class UninstallAlertDialogFragment extends DialogFragment implements final boolean isUpdate = ((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); final UserHandle myUserHandle = Process.myUserHandle(); - UserManager userManager = UserManager.get(getActivity()); + UserManager userManager = getContext().getSystemService(UserManager.class); if (isUpdate) { if (isSingleUser(userManager)) { messageBuilder.append(getString(R.string.uninstall_update_text)); @@ -139,20 +141,25 @@ public class UninstallAlertDialogFragment extends DialogFragment implements if (dialogInfo.allUsers && !isSingleUser(userManager)) { messageBuilder.append(getString(R.string.uninstall_application_text_all_users)); } else if (!dialogInfo.user.equals(myUserHandle)) { - UserInfo userInfo = userManager.getUserInfo(dialogInfo.user.getIdentifier()); - if (userInfo.isManagedProfile() - && userInfo.profileGroupId == myUserHandle.getIdentifier()) { + int userId = dialogInfo.user.getIdentifier(); + UserManager customUserManager = getContext() + .createContextAsUser(UserHandle.of(userId), 0) + .getSystemService(UserManager.class); + String userName = customUserManager.getUserName(); + + if (customUserManager.isUserOfType(USER_TYPE_PROFILE_MANAGED) + && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { messageBuilder.append( getString(R.string.uninstall_application_text_current_user_work_profile, - userInfo.name)); - } else if (userInfo.isCloneProfile() - && userInfo.profileGroupId == myUserHandle.getIdentifier()) { + userName)); + } else if (customUserManager.isUserOfType(USER_TYPE_PROFILE_CLONE) + && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { isClonedApp = true; messageBuilder.append(getString( R.string.uninstall_application_text_current_user_clone_profile)); } else { messageBuilder.append( - getString(R.string.uninstall_application_text_user, userInfo.name)); + getString(R.string.uninstall_application_text_user, userName)); } } else if (isCloneProfile(myUserHandle)) { isClonedApp = true; @@ -238,8 +245,8 @@ public class UninstallAlertDialogFragment extends DialogFragment implements // Check if another instance of given package exists in clone user profile. if (cloneUser != null) { try { - if (getContext().getPackageManager() - .getPackageUidAsUser(packageName, cloneUser.getIdentifier()) > 0) { + if (getContext().getPackageManager().getPackageUidAsUser(packageName, + PackageManager.PackageInfoFlags.of(0), cloneUser.getIdentifier()) > 0) { return true; } } catch (PackageManager.NameNotFoundException e) { @@ -273,7 +280,6 @@ public class UninstallAlertDialogFragment extends DialogFragment implements */ private boolean isSingleUser(UserManager userManager) { final int userCount = userManager.getUserCount(); - return userCount == 1 - || (UserManager.isSplitSystemUser() && userCount == 2); + return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2); } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java index 2d241ca9038e..5c5720a61186 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAlertFragment.java @@ -16,10 +16,11 @@ package com.android.packageinstaller.television; +import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; + import android.app.Activity; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; -import android.content.pm.UserInfo; import android.os.Bundle; import android.os.Process; import android.os.UserHandle; @@ -63,7 +64,7 @@ public class UninstallAlertFragment extends GuidedStepFragment { final boolean isUpdate = ((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); final UserHandle myUserHandle = Process.myUserHandle(); - UserManager userManager = UserManager.get(getActivity()); + UserManager userManager = getContext().getSystemService(UserManager.class); if (isUpdate) { if (isSingleUser(userManager)) { messageBuilder.append(getString(R.string.uninstall_update_text)); @@ -74,15 +75,21 @@ public class UninstallAlertFragment extends GuidedStepFragment { if (dialogInfo.allUsers && !isSingleUser(userManager)) { messageBuilder.append(getString(R.string.uninstall_application_text_all_users)); } else if (!dialogInfo.user.equals(myUserHandle)) { - UserInfo userInfo = userManager.getUserInfo(dialogInfo.user.getIdentifier()); - if (userInfo.isManagedProfile() - && userInfo.profileGroupId == myUserHandle.getIdentifier()) { + int userId = dialogInfo.user.getIdentifier(); + UserManager customUserManager = getContext() + .createContextAsUser(UserHandle.of(userId), 0) + .getSystemService(UserManager.class); + String userName = customUserManager.getUserName(); + + if (customUserManager.isUserOfType(USER_TYPE_PROFILE_MANAGED) + && customUserManager.isSameProfileGroup(dialogInfo.user, myUserHandle)) { + messageBuilder.append( getString(R.string.uninstall_application_text_current_user_work_profile, - userInfo.name)); + userName)); } else { messageBuilder.append( - getString(R.string.uninstall_application_text_user, userInfo.name)); + getString(R.string.uninstall_application_text_user, userName)); } } else { messageBuilder.append(getString(R.string.uninstall_application_text)); @@ -126,7 +133,6 @@ public class UninstallAlertFragment extends GuidedStepFragment { */ private boolean isSingleUser(UserManager userManager) { final int userCount = userManager.getUserCount(); - return userCount == 1 - || (UserManager.isSplitSystemUser() && userCount == 2); + return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2); } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java index a4f217c4a5e3..0c59d44b9c8f 100755 --- a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgress.java @@ -17,24 +17,20 @@ package com.android.packageinstaller.television; import android.app.Activity; -import android.app.admin.IDevicePolicyManager; +import android.app.PendingIntent; +import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageDeleteObserver; -import android.content.pm.IPackageDeleteObserver2; -import android.content.pm.IPackageManager; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; -import android.content.pm.UserInfo; +import android.content.pm.VersionedPackage; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; import android.os.Handler; -import android.os.IBinder; import android.os.Message; -import android.os.RemoteException; -import android.os.ServiceManager; +import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -42,8 +38,12 @@ import android.util.TypedValue; import android.view.KeyEvent; import android.widget.Toast; +import androidx.annotation.Nullable; + +import com.android.packageinstaller.EventResultPersister; import com.android.packageinstaller.PackageUtil; import com.android.packageinstaller.R; +import com.android.packageinstaller.UninstallEventReceiver; import java.lang.ref.WeakReference; import java.util.List; @@ -55,14 +55,17 @@ import java.util.List; * by an intent with the intent's class name explicitly set to UninstallAppProgress and expects * the application object of the application to uninstall. */ -public class UninstallAppProgress extends Activity { +public class UninstallAppProgress extends Activity implements + EventResultPersister.EventResultObserver { private static final String TAG = "UninstallAppProgress"; private static final String FRAGMENT_TAG = "progress_fragment"; + private static final String BROADCAST_ACTION = + "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT"; private ApplicationInfo mAppInfo; private boolean mAllUsers; - private IBinder mCallback; + private PackageManager.UninstallCompleteCallback mCallback; private volatile int mResultCode = -1; @@ -116,13 +119,7 @@ public class UninstallAppProgress extends Activity { final String packageName = (String) msg.obj; if (mCallback != null) { - final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub - .asInterface(mCallback); - try { - observer.onPackageDeleted(mAppInfo.packageName, mResultCode, - packageName); - } catch (RemoteException ignored) { - } + mCallback.onUninstallComplete(mAppInfo.packageName, mResultCode, packageName); finish(); return; } @@ -139,37 +136,34 @@ public class UninstallAppProgress extends Activity { // Update the status text final String statusText; + Context ctx = getBaseContext(); switch (msg.arg1) { case PackageManager.DELETE_SUCCEEDED: statusText = getString(R.string.uninstall_done); // Show a Toast and finish the activity - Context ctx = getBaseContext(); Toast.makeText(ctx, statusText, Toast.LENGTH_LONG).show(); setResultAndFinish(); return; case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER: { UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); - IDevicePolicyManager dpm = IDevicePolicyManager.Stub.asInterface( - ServiceManager.getService(Context.DEVICE_POLICY_SERVICE)); // Find out if the package is an active admin for some non-current user. - int myUserId = UserHandle.myUserId(); - UserInfo otherBlockingUser = null; - for (UserInfo user : userManager.getUsers()) { + UserHandle myUserHandle = Process.myUserHandle(); + UserHandle otherBlockingUserHandle = null; + for (UserHandle otherUserHandle : userManager.getUserHandles(true)) { // We only catch the case when the user in question is neither the // current user nor its profile. - if (isProfileOfOrSame(userManager, myUserId, user.id)) continue; - - try { - if (dpm.packageHasActiveAdmins(packageName, user.id)) { - otherBlockingUser = user; - break; - } - } catch (RemoteException e) { - Log.e(TAG, "Failed to talk to package manager", e); + if (isProfileOfOrSame(userManager, myUserHandle, otherUserHandle)) { + continue; + } + DevicePolicyManager dpm = ctx.createContextAsUser(otherUserHandle, 0) + .getSystemService(DevicePolicyManager.class); + if (dpm.packageHasActiveAdmins(packageName)) { + otherBlockingUserHandle = otherUserHandle; + break; } } - if (otherBlockingUser == null) { + if (otherBlockingUserHandle == null) { Log.d(TAG, "Uninstall failed because " + packageName + " is a device admin"); getProgressFragment().setDeviceManagerButtonVisible(true); @@ -177,45 +171,40 @@ public class UninstallAppProgress extends Activity { R.string.uninstall_failed_device_policy_manager); } else { Log.d(TAG, "Uninstall failed because " + packageName - + " is a device admin of user " + otherBlockingUser); + + " is a device admin of user " + otherBlockingUserHandle); getProgressFragment().setDeviceManagerButtonVisible(false); + String userName = ctx.createContextAsUser(otherBlockingUserHandle, 0) + .getSystemService(UserManager.class).getUserName(); statusText = String.format( getString(R.string.uninstall_failed_device_policy_manager_of_user), - otherBlockingUser.name); + userName); } break; } case PackageManager.DELETE_FAILED_OWNER_BLOCKED: { UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); - IPackageManager packageManager = IPackageManager.Stub.asInterface( - ServiceManager.getService("package")); - List<UserInfo> users = userManager.getUsers(); - int blockingUserId = UserHandle.USER_NULL; - for (int i = 0; i < users.size(); ++i) { - final UserInfo user = users.get(i); - try { - if (packageManager.getBlockUninstallForUser(packageName, - user.id)) { - blockingUserId = user.id; - break; - } - } catch (RemoteException e) { - // Shouldn't happen. - Log.e(TAG, "Failed to talk to package manager", e); + PackageManager packageManager = ctx.getPackageManager(); + List<UserHandle> userHandles = userManager.getUserHandles(true); + UserHandle otherBlockingUserHandle = null; + for (int i = 0; i < userHandles.size(); ++i) { + final UserHandle handle = userHandles.get(i); + if (packageManager.canUserUninstall(packageName, handle)) { + otherBlockingUserHandle = handle; + break; } } - int myUserId = UserHandle.myUserId(); - if (isProfileOfOrSame(userManager, myUserId, blockingUserId)) { + UserHandle myUserHandle = Process.myUserHandle(); + if (isProfileOfOrSame(userManager, myUserHandle, otherBlockingUserHandle)) { getProgressFragment().setDeviceManagerButtonVisible(true); } else { getProgressFragment().setDeviceManagerButtonVisible(false); getProgressFragment().setUsersButtonVisible(true); } // TODO: b/25442806 - if (blockingUserId == UserHandle.USER_SYSTEM) { + if (otherBlockingUserHandle == UserHandle.SYSTEM) { statusText = getString(R.string.uninstall_blocked_device_owner); - } else if (blockingUserId == UserHandle.USER_NULL) { + } else if (otherBlockingUserHandle == null) { Log.d(TAG, "Uninstall failed for " + packageName + " with code " + msg.arg1 + " no blocking user"); statusText = getString(R.string.uninstall_failed); @@ -239,12 +228,13 @@ public class UninstallAppProgress extends Activity { } } - private boolean isProfileOfOrSame(UserManager userManager, int userId, int profileId) { - if (userId == profileId) { + private boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle, + UserHandle profileHandle) { + if (userHandle.equals(profileHandle)) { return true; } - UserInfo parentUser = userManager.getProfileParent(profileId); - return parentUser != null && parentUser.id == userId; + return userManager.getProfileParent(profileHandle) != null + && userManager.getProfileParent(profileHandle).equals(userHandle); } @Override @@ -253,7 +243,8 @@ public class UninstallAppProgress extends Activity { Intent intent = getIntent(); mAppInfo = intent.getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); - mCallback = intent.getIBinderExtra(PackageInstaller.EXTRA_CALLBACK); + mCallback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK, + PackageManager.UninstallCompleteCallback.class); // This currently does not support going through a onDestroy->onCreate cycle. Hence if that // happened, just fail the operation for mysterious reasons. @@ -261,12 +252,7 @@ public class UninstallAppProgress extends Activity { mResultCode = PackageManager.DELETE_FAILED_INTERNAL_ERROR; if (mCallback != null) { - final IPackageDeleteObserver2 observer = IPackageDeleteObserver2.Stub - .asInterface(mCallback); - try { - observer.onPackageDeleted(mAppInfo.packageName, mResultCode, null); - } catch (RemoteException ignored) { - } + mCallback.onUninstallComplete(mAppInfo.packageName, mResultCode, null); finish(); } else { setResultAndFinish(); @@ -278,10 +264,9 @@ public class UninstallAppProgress extends Activity { mAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, false); UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); if (user == null) { - user = android.os.Process.myUserHandle(); + user = Process.myUserHandle(); } - PackageDeleteObserver observer = new PackageDeleteObserver(); // Make window transparent until initView is called. In many cases we can avoid showing the // UI at all as the app is uninstalled very quickly. If we show the UI and instantly remove @@ -291,11 +276,29 @@ public class UninstallAppProgress extends Activity { getWindow().setNavigationBarColor(Color.TRANSPARENT); try { - getPackageManager().deletePackageAsUser(mAppInfo.packageName, observer, - mAllUsers ? PackageManager.DELETE_ALL_USERS : 0, user.getIdentifier()); + int uninstallId = UninstallEventReceiver.addObserver(this, + EventResultPersister.GENERATE_NEW_ID, this); + + Intent broadcastIntent = new Intent(BROADCAST_ACTION); + broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId); + broadcastIntent.setPackage(getPackageName()); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId, + broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT + | PendingIntent.FLAG_MUTABLE); + + createContextAsUser(user, 0).getPackageManager().getPackageInstaller().uninstall( + new VersionedPackage(mAppInfo.packageName, PackageManager.VERSION_CODE_HIGHEST), + mAllUsers ? PackageManager.DELETE_ALL_USERS : 0, + pendingIntent.getIntentSender()); } catch (IllegalArgumentException e) { // Couldn't find the package, no need to call uninstall. Log.w(TAG, "Could not find package, not deleting " + mAppInfo.packageName, e); + } catch (EventResultPersister.OutOfIdsException e) { + Log.e(TAG, "Fails to start uninstall", e); + onResult(PackageInstaller.STATUS_FAILURE, PackageManager.DELETE_FAILED_INTERNAL_ERROR, + null, 0); } mHandler.sendMessageDelayed(mHandler.obtainMessage(UNINSTALL_IS_SLOW), @@ -306,13 +309,12 @@ public class UninstallAppProgress extends Activity { return mAppInfo; } - private class PackageDeleteObserver extends IPackageDeleteObserver.Stub { - public void packageDeleted(String packageName, int returnCode) { - Message msg = mHandler.obtainMessage(UNINSTALL_COMPLETE); - msg.arg1 = returnCode; - msg.obj = packageName; - mHandler.sendMessage(msg); - } + @Override + public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) { + Message msg = mHandler.obtainMessage(UNINSTALL_COMPLETE); + msg.arg1 = legacyStatus; + msg.obj = mAppInfo.packageName; + mHandler.sendMessage(msg); } public void setResultAndFinish() { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java index af6d9c58c733..c2d95b238dc9 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/television/UninstallAppProgressFragment.java @@ -16,7 +16,6 @@ package com.android.packageinstaller.television; -import android.annotation.Nullable; import android.app.Fragment; import android.content.Intent; import android.os.Bundle; @@ -28,6 +27,8 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.Nullable; + import com.android.packageinstaller.PackageUtil; import com.android.packageinstaller.R; diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java index 063d78986ae5..8dd691d14092 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/PackageInstallerImpl.java @@ -265,7 +265,7 @@ public class PackageInstallerImpl { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(action); mContext.registerReceiver(broadcastReceiver, intentFilter, - Context.RECEIVER_EXPORTED_UNAUDITED); + Context.RECEIVER_EXPORTED); // Create a matching PendingIntent and use it to generate the IntentSender Intent broadcastIntent = new Intent(action); diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java index 06b1c1635644..959257f8f6c0 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/wear/WearPackageInstallerService.java @@ -19,14 +19,16 @@ package com.android.packageinstaller.wear; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.FeatureInfo; -import android.content.pm.IPackageDeleteObserver; import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; import android.database.Cursor; import android.net.Uri; import android.os.Build; @@ -43,13 +45,18 @@ import android.util.ArrayMap; import android.util.Log; import android.util.Pair; +import androidx.annotation.Nullable; + import com.android.packageinstaller.DeviceUtils; +import com.android.packageinstaller.EventResultPersister; import com.android.packageinstaller.PackageUtil; import com.android.packageinstaller.R; +import com.android.packageinstaller.UninstallEventReceiver; import java.io.File; import java.io.FileNotFoundException; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -80,16 +87,30 @@ import java.util.Set; * adb shell am startservice -a com.android.packageinstaller.wear.RETRY_GMS \ * com.android.packageinstaller/com.android.packageinstaller.wear.WearPackageInstallerService */ -public class WearPackageInstallerService extends Service { +public class WearPackageInstallerService extends Service + implements EventResultPersister.EventResultObserver { private static final String TAG = "WearPkgInstallerService"; private static final String WEAR_APPS_CHANNEL = "wear_app_install_uninstall"; + private static final String BROADCAST_ACTION = + "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT"; private final int START_INSTALL = 1; private final int START_UNINSTALL = 2; private int mInstallNotificationId = 1; private final Map<String, Integer> mNotifIdMap = new ArrayMap<>(); + private final Map<Integer, UninstallParams> mServiceIdToParams = new HashMap<>(); + + private class UninstallParams { + public String mPackageName; + public PowerManager.WakeLock mLock; + + UninstallParams(String packageName, PowerManager.WakeLock lock) { + mPackageName = packageName; + mLock = lock; + } + } private final class ServiceHandler extends Handler { public ServiceHandler(Looper looper) { @@ -211,7 +232,6 @@ public class WearPackageInstallerService extends Service { } final PackageManager pm = getPackageManager(); File tempFile = null; - int installFlags = 0; PowerManager.WakeLock lock = getLock(this.getApplicationContext()); boolean messageSent = false; try { @@ -220,17 +240,14 @@ public class WearPackageInstallerService extends Service { existingPkgInfo = pm.getPackageInfo(packageName, PackageManager.MATCH_ANY_USER | PackageManager.GET_PERMISSIONS); if (existingPkgInfo != null) { - installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Replacing package:" + packageName); + } } } catch (PackageManager.NameNotFoundException e) { // Ignore this exception. We could not find the package, will treat as a new // installation. } - if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Replacing package:" + packageName); - } - } // TODO(28021618): This was left as a temp file due to the fact that this code is being // deprecated and that we need the bare minimum to continue working moving forward // If this code is used as reference, this permission logic might want to be @@ -366,21 +383,60 @@ public class WearPackageInstallerService extends Service { final String packageName = WearPackageArgs.getPackageName(argsBundle); PowerManager.WakeLock lock = getLock(this.getApplicationContext()); + + UninstallParams params = new UninstallParams(packageName, lock); + mServiceIdToParams.put(startId, params); + final PackageManager pm = getPackageManager(); try { PackageInfo pkgInfo = pm.getPackageInfo(packageName, 0); getLabelAndUpdateNotification(packageName, getString(R.string.uninstalling_app, pkgInfo.applicationInfo.loadLabel(pm))); + int uninstallId = UninstallEventReceiver.addObserver(this, + EventResultPersister.GENERATE_NEW_ID, this); + + Intent broadcastIntent = new Intent(BROADCAST_ACTION); + broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); + broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, uninstallId); + broadcastIntent.putExtra(EventResultPersister.EXTRA_SERVICE_ID, startId); + broadcastIntent.setPackage(getPackageName()); + + PendingIntent pendingIntent = PendingIntent.getBroadcast(this, uninstallId, + broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT + | PendingIntent.FLAG_MUTABLE); + // Found package, send uninstall request. - pm.deletePackage(packageName, new PackageDeleteObserver(lock, startId), - PackageManager.DELETE_ALL_USERS); + pm.getPackageInstaller().uninstall( + new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), + PackageManager.DELETE_ALL_USERS, + pendingIntent.getIntentSender()); Log.i(TAG, "Sent delete request for " + packageName); } catch (IllegalArgumentException | PackageManager.NameNotFoundException e) { // Couldn't find the package, no need to call uninstall. Log.w(TAG, "Could not find package, not deleting " + packageName, e); finishService(lock, startId); + } catch (EventResultPersister.OutOfIdsException e) { + Log.e(TAG, "Fails to start uninstall", e); + finishService(lock, startId); + } + } + + @Override + public void onResult(int status, int legacyStatus, @Nullable String message, int serviceId) { + if (mServiceIdToParams.containsKey(serviceId)) { + UninstallParams params = mServiceIdToParams.get(serviceId); + try { + if (status == PackageInstaller.STATUS_SUCCESS) { + Log.i(TAG, "Package " + params.mPackageName + " was uninstalled."); + } else { + Log.e(TAG, "Package uninstall failed " + params.mPackageName + + ", returnCode " + legacyStatus); + } + } finally { + finishService(params.mLock, serviceId); + } } } @@ -537,29 +593,6 @@ public class WearPackageInstallerService extends Service { } } - private class PackageDeleteObserver extends IPackageDeleteObserver.Stub { - private PowerManager.WakeLock mWakeLock; - private int mStartId; - - private PackageDeleteObserver(PowerManager.WakeLock wakeLock, int startId) { - mWakeLock = wakeLock; - mStartId = startId; - } - - public void packageDeleted(String packageName, int returnCode) { - try { - if (returnCode >= 0) { - Log.i(TAG, "Package " + packageName + " was uninstalled."); - } else { - Log.e(TAG, "Package uninstall failed " + packageName + ", returnCode " + - returnCode); - } - } finally { - finishService(mWakeLock, mStartId); - } - } - } - private synchronized Pair<Integer, Notification> buildNotification(final String packageName, final String title) { int notifId; diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt index c08169e88852..e8788048776e 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt @@ -41,6 +41,7 @@ class AppOpsController( context: Context, private val app: ApplicationInfo, private val op: Int, + private val setModeByUid: Boolean = false, ) : IAppOpsController { private val appOpsManager = context.appOpsManager @@ -49,7 +50,11 @@ class AppOpsController( override fun setAllowed(allowed: Boolean) { val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED - appOpsManager.setMode(op, app.uid, app.packageName, mode) + if (setModeByUid) { + appOpsManager.setUidMode(op, app.uid, mode) + } else { + appOpsManager.setMode(op, app.uid, app.packageName, mode) + } _mode.postValue(mode) } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt index a3578321a098..ee21b81fe92a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt @@ -49,6 +49,13 @@ abstract class AppOpPermissionListModel( abstract val appOp: Int abstract val permission: String + /** + * Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed. + * + * Security related app-ops should be set with setUidMode() instead of setMode(). + */ + open val setModeByUid = false + /** These not changeable packages will also be hidden from app list. */ private val notChangeablePackages = setOf("android", "com.android.systemui", context.packageName) @@ -61,7 +68,7 @@ abstract class AppOpPermissionListModel( AppOpPermissionRecord( app = app, hasRequestPermission = app.packageName in packageNames, - appOpsController = AppOpsController(context = context, app = app, op = appOp), + appOpsController = createAppOpsController(app), ) } } @@ -69,7 +76,14 @@ abstract class AppOpPermissionListModel( override fun transformItem(app: ApplicationInfo) = AppOpPermissionRecord( app = app, hasRequestPermission = with(packageManagers) { app.hasRequestPermission(permission) }, - appOpsController = AppOpsController(context = context, app = app, op = appOp), + appOpsController = createAppOpsController(app), + ) + + private fun createAppOpsController(app: ApplicationInfo) = AppOpsController( + context = context, + app = app, + op = appOp, + setModeByUid = setModeByUid, ) override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) = diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt new file mode 100644 index 000000000000..668bfdfd7e32 --- /dev/null +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spaprivileged.model.app + +import android.app.AppOpsManager +import android.app.AppOpsManager.MODE_ALLOWED +import android.app.AppOpsManager.MODE_ERRORED +import android.content.Context +import android.content.pm.ApplicationInfo +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settingslib.spaprivileged.framework.common.appOpsManager +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Spy +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidJUnit4::class) +class AppOpsControllerTest { + @get:Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @Spy + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var appOpsManager: AppOpsManager + + @Before + fun setUp() { + whenever(context.appOpsManager).thenReturn(appOpsManager) + } + + @Test + fun setAllowed_setToTrue() { + val controller = AppOpsController(context = context, app = APP, op = OP) + + controller.setAllowed(true) + + verify(appOpsManager).setMode(OP, APP.uid, APP.packageName, MODE_ALLOWED) + } + + @Test + fun setAllowed_setToFalse() { + val controller = AppOpsController(context = context, app = APP, op = OP) + + controller.setAllowed(false) + + verify(appOpsManager).setMode(OP, APP.uid, APP.packageName, MODE_ERRORED) + } + + @Test + fun setAllowed_setToTrueByUid() { + val controller = + AppOpsController(context = context, app = APP, op = OP, setModeByUid = true) + + controller.setAllowed(true) + + verify(appOpsManager).setUidMode(OP, APP.uid, MODE_ALLOWED) + } + + @Test + fun setAllowed_setToFalseByUid() { + val controller = + AppOpsController(context = context, app = APP, op = OP, setModeByUid = true) + + controller.setAllowed(false) + + verify(appOpsManager).setUidMode(OP, APP.uid, MODE_ERRORED) + } + + @Test + fun getMode() { + whenever( + appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName) + ).thenReturn(MODE_ALLOWED) + val controller = AppOpsController(context = context, app = APP, op = OP) + + val mode = controller.getMode() + + assertThat(mode).isEqualTo(MODE_ALLOWED) + } + + private companion object { + const val OP = 1 + val APP = ApplicationInfo().apply { + packageName = "package.name" + uid = 123 + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt index cd9c048297a0..966b86927e55 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.MutableLiveData import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull +import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.model.app.IAppOpsController import com.android.settingslib.spaprivileged.model.app.IPackageManagers import com.android.settingslib.spaprivileged.test.R @@ -37,6 +38,8 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Spy import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import org.mockito.Mockito.`when` as whenever @@ -50,17 +53,20 @@ class AppOpPermissionAppListTest { @get:Rule val composeTestRule = createComposeRule() + @Spy private val context: Context = ApplicationProvider.getApplicationContext() @Mock private lateinit var packageManagers: IPackageManagers + @Mock + private lateinit var appOpsManager: AppOpsManager + private lateinit var listModel: TestAppOpPermissionAppListModel @Before - fun setUp() = runTest { - whenever(packageManagers.getAppOpPermissionPackages(USER_ID, PERMISSION)) - .thenReturn(emptySet()) + fun setUp() { + whenever(context.appOpsManager).thenReturn(appOpsManager) listModel = TestAppOpPermissionAppListModel() } @@ -221,6 +227,16 @@ class AppOpPermissionAppListTest { assertThat(appOpsController.setAllowedCalledWith).isTrue() } + @Test + fun setAllowed_setModeByUid() { + listModel.setModeByUid = true + val record = listModel.transformItem(APP) + + listModel.setAllowed(record = record, newAllowed = true) + + verify(appOpsManager).setUidMode(listModel.appOp, APP.uid, AppOpsManager.MODE_ALLOWED) + } + private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? { lateinit var isAllowedState: State<Boolean?> composeTestRule.setContent { @@ -236,6 +252,7 @@ class AppOpPermissionAppListTest { override val footerResId = R.string.test_app_op_permission_footer override val appOp = AppOpsManager.OP_MANAGE_MEDIA override val permission = PERMISSION + override var setModeByUid = false } private companion object { diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java index 3e710e4c7e35..28353ab7dff4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatterySaverUtils.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.os.Bundle; import android.os.PowerManager; import android.os.UserHandle; +import android.provider.Settings; import android.provider.Settings.Global; import android.provider.Settings.Secure; import android.util.KeyValueListParser; @@ -55,6 +56,10 @@ public class BatterySaverUtils { public static final String EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL = "extra_power_save_mode_trigger_level"; + /** Battery saver schedule keys. */ + public static final String KEY_NO_SCHEDULE = "key_battery_saver_no_schedule"; + public static final String KEY_PERCENTAGE = "key_battery_saver_percentage"; + private BatterySaverUtils() { } @@ -108,7 +113,6 @@ public class BatterySaverUtils { * - If it's 4th time through 8th time, show the schedule suggestion notification. * * @param enable true to enable battery saver. - * * @return true if the request succeeded. */ public static synchronized boolean setPowerSaveMode(Context context, @@ -154,10 +158,10 @@ public class BatterySaverUtils { * Shows the battery saver confirmation warning if it hasn't been acknowledged by the user in * the past before. Various extras can be provided that will change the behavior of this * notification as well as the ui for it. - * @param context A valid context - * @param extras Any extras to include in the intent to trigger this confirmation that will - * help the system disambiguate what to show/do * + * @param context A valid context + * @param extras Any extras to include in the intent to trigger this confirmation that will + * help the system disambiguate what to show/do * @return True if it showed the notification because it has not been previously acknowledged. * @see #EXTRA_CONFIRM_TEXT_ONLY * @see #EXTRA_POWER_SAVE_MODE_TRIGGER @@ -221,6 +225,7 @@ public class BatterySaverUtils { /** * Reverts battery saver schedule mode to none if routine mode is selected. + * * @param context a valid context */ public static void revertScheduleToNoneIfNeeded(Context context) { @@ -233,4 +238,50 @@ public class BatterySaverUtils { PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); } } + + /** + * Gets battery saver schedule mode. + * + * @param context a valid context + * @return battery saver schedule key + */ + public static String getBatterySaverScheduleKey(Context context) { + final ContentResolver resolver = context.getContentResolver(); + final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) { + final int threshold = + Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); + return threshold <= 0 ? KEY_NO_SCHEDULE : KEY_PERCENTAGE; + } + revertScheduleToNoneIfNeeded(context); + return KEY_NO_SCHEDULE; + } + + /** + * Sets battery saver schedule mode. + * + * @param context a valid context + * @param scheduleKey {@link #KEY_NO_SCHEDULE} and {@link #KEY_PERCENTAGE} + * @param triggerLevel for automatic battery saver trigger level + */ + public static void setBatterySaverScheduleMode(Context context, String scheduleKey, + int triggerLevel) { + final ContentResolver resolver = context.getContentResolver(); + switch (scheduleKey) { + case KEY_NO_SCHEDULE: + Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + Settings.Global.putInt(resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); + break; + case KEY_PERCENTAGE: + Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + Settings.Global.putInt(resolver, + Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, triggerLevel); + break; + default: + throw new IllegalStateException("Not a valid schedule key"); + } + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java index 2bb3c2af8304..a15fe9f90206 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/fuelgauge/BatterySaverUtilsTest.java @@ -16,6 +16,9 @@ package com.android.settingslib.fuelgauge; +import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_NO_SCHEDULE; +import static com.android.settingslib.fuelgauge.BatterySaverUtils.KEY_PERCENTAGE; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; @@ -186,4 +189,46 @@ public class BatterySaverUtilsTest { assertThat(Secure.getInt(mMockResolver, Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, -1)) .isEqualTo(1); } + + @Test + public void testGetBatterySaverScheduleKey_returnExpectedKey() { + Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0); + Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + + assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo( + KEY_NO_SCHEDULE); + + Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20); + Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + + assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo( + KEY_PERCENTAGE); + + Global.putInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 20); + Global.putInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, + PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC); + + assertThat(BatterySaverUtils.getBatterySaverScheduleKey(mMockContext)).isEqualTo( + KEY_NO_SCHEDULE); + } + + @Test + public void testSetBatterySaverScheduleMode_setSchedule() { + BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_NO_SCHEDULE, -1); + + assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1)) + .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1)) + .isEqualTo(0); + + BatterySaverUtils.setBatterySaverScheduleMode(mMockContext, KEY_PERCENTAGE, 20); + + assertThat(Global.getInt(mMockResolver, Global.AUTOMATIC_POWER_SAVE_MODE, -1)) + .isEqualTo(PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE); + assertThat(Global.getInt(mMockResolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, -1)) + .isEqualTo(20); + + } } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index e1000e0d5424..220c16ae526b 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -31,6 +31,52 @@ license { ], } +// Opt-in configuration for code depending on Jetpack Compose. +soong_config_module_type { + name: "systemui_compose_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: ["SYSTEMUI_USE_COMPOSE"], + properties: [ + "srcs", + "static_libs", + ], +} + +systemui_compose_java_defaults { + name: "SystemUI_compose_defaults", + soong_config_variables: { + SYSTEMUI_USE_COMPOSE: { + // Because files in compose/features/ depend on SystemUI + // code, we compile those files when compiling SystemUI-core. + // We also compile the ComposeFacade in + // compose/facade/enabled/. + srcs: [ + "compose/features/src/**/*.kt", + "compose/facade/enabled/src/**/*.kt", + ], + + // The dependencies needed by SystemUIComposeFeatures, + // except for SystemUI-core. + // Copied from compose/features/Android.bp. + static_libs: [ + "SystemUIComposeCore", + + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + "androidx.activity_activity-compose", + ], + + // By default, Compose is disabled and we compile the ComposeFacade + // in compose/facade/disabled/. + conditions_default: { + srcs: ["compose/facade/disabled/src/**/*.kt"], + static_libs: [], + }, + }, + }, +} + java_library { name: "SystemUI-proto", @@ -68,6 +114,9 @@ filegroup { android_library { name: "SystemUI-core", + defaults: [ + "SystemUI_compose_defaults", + ], srcs: [ "src/**/*.kt", "src/**/*.java", @@ -228,6 +277,9 @@ android_library { android_library { name: "SystemUI-tests", + defaults: [ + "SystemUI_compose_defaults", + ], manifest: "tests/AndroidManifest-base.xml", additional_manifests: ["tests/AndroidManifest.xml"], srcs: [ diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt index 163cab468fc0..fd9355d6a77d 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewRootSync.kt @@ -28,7 +28,7 @@ object ViewRootSync { return } - val syncGroup = SurfaceSyncGroup() + val syncGroup = SurfaceSyncGroup("SysUIAnimation") syncGroup.addSyncCompleteCallback(view.context.mainExecutor) { then() } syncGroup.addToSync(view.rootSurfaceControl) syncGroup.addToSync(otherView.rootSurfaceControl) diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt new file mode 100644 index 000000000000..3f2f96b293e0 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/runtime/MovableContent.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.compose.runtime + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.InternalComposeApi +import androidx.compose.runtime.MovableContent +import androidx.compose.runtime.currentComposer + +/** + * An overload of [androidx.compose.runtime.movableContentOf] with 5 parameters. + * + * @see androidx.compose.runtime.movableContentOf + */ +@OptIn(InternalComposeApi::class) +fun <P1, P2, P3, P4, P5> movableContentOf( + content: @Composable (P1, P2, P3, P4, P5) -> Unit +): @Composable (P1, P2, P3, P4, P5) -> Unit { + val movableContent = + MovableContent<Pair<Triple<P1, P2, P3>, Pair<P4, P5>>> { + content( + it.first.first, + it.first.second, + it.first.third, + it.second.first, + it.second.second, + ) + } + return { p1, p2, p3, p4, p5 -> + currentComposer.insertMovableContent(movableContent, Triple(p1, p2, p3) to (p4 to p5)) + } +} 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 new file mode 100644 index 000000000000..6e728ce7248f --- /dev/null +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.compose + +import androidx.activity.ComponentActivity +import com.android.systemui.people.ui.viewmodel.PeopleViewModel + +/** The Compose facade, when Compose is *not* available. */ +object ComposeFacade : BaseComposeFacade { + override fun isComposeAvailable(): Boolean = false + + override fun setPeopleSpaceActivityContent( + activity: ComponentActivity, + viewModel: PeopleViewModel, + onResult: (PeopleViewModel.Result) -> Unit, + ) { + throwComposeUnavailableError() + } + + private fun throwComposeUnavailableError() { + error( + "Compose is not available. Make sure to check isComposeAvailable() before calling any" + + " other function on ComposeFacade." + ) + } +} 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 new file mode 100644 index 000000000000..16294d9c1977 --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.compose + +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import com.android.systemui.compose.theme.SystemUITheme +import com.android.systemui.people.ui.compose.PeopleScreen +import com.android.systemui.people.ui.viewmodel.PeopleViewModel + +/** The Compose facade, when Compose is available. */ +object ComposeFacade : BaseComposeFacade { + override fun isComposeAvailable(): Boolean = true + + override fun setPeopleSpaceActivityContent( + activity: ComponentActivity, + viewModel: PeopleViewModel, + onResult: (PeopleViewModel.Result) -> Unit, + ) { + activity.setContent { SystemUITheme { PeopleScreen(viewModel, onResult) } } + } +} diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp index 325ede613de8..453333053118 100644 --- a/packages/SystemUI/compose/features/Android.bp +++ b/packages/SystemUI/compose/features/Android.bp @@ -35,6 +35,7 @@ android_library { "androidx.compose.runtime_runtime", "androidx.compose.material3_material3", + "androidx.activity_activity-compose", ], kotlincflags: ["-Xjvm-default=all"], diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt index 2aac46e90342..4a56b027e07c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt @@ -139,11 +139,20 @@ private fun PeopleScreenWithConversations( bottom = PeopleSpacePadding, start = 8.dp, end = 8.dp, - ) + ), ) { - ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked) - item { Spacer(Modifier.height(35.dp)) } - ConversationList(R.string.recent_conversations, recentTiles, onTileClicked) + val hasPriorityConversations = priorityTiles.isNotEmpty() + if (hasPriorityConversations) { + ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked) + } + + if (recentTiles.isNotEmpty()) { + if (hasPriorityConversations) { + item { Spacer(Modifier.height(35.dp)) } + } + + ConversationList(R.string.recent_conversations, recentTiles, onTileClicked) + } } } } diff --git a/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml b/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml index 708bc1ac7e8a..8aae276789d6 100644 --- a/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml +++ b/packages/SystemUI/res/drawable/media_ttt_chip_background_receiver.xml @@ -19,8 +19,8 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:shape="oval"> <size - android:height="@dimen/media_ttt_chip_size_receiver" - android:width="@dimen/media_ttt_chip_size_receiver" + android:height="@dimen/media_ttt_icon_size_receiver" + android:width="@dimen/media_ttt_icon_size_receiver" /> - <solid android:color="?androidprv:attr/colorSurface" /> + <solid android:color="?androidprv:attr/colorAccentPrimary" /> </shape> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ea1095d75340..3b17bce6c2f1 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1077,11 +1077,12 @@ <dimen name="media_ttt_last_item_start_margin">12dp</dimen> <!-- Media tap-to-transfer chip for receiver device --> - <dimen name="media_ttt_chip_size_receiver">100dp</dimen> - <dimen name="media_ttt_icon_size_receiver">95dp</dimen> + <dimen name="media_ttt_icon_size_receiver">112dp</dimen> <!-- Add some padding for the generic icon so it doesn't go all the way to the border. --> - <dimen name="media_ttt_generic_icon_padding">12dp</dimen> - <dimen name="media_ttt_receiver_vert_translation">20dp</dimen> + <!-- The generic icon should be 40dp, and the full icon is 112dp, so the padding should be + (112 - 40) / 2 = 36dp --> + <dimen name="media_ttt_generic_icon_padding">36dp</dimen> + <dimen name="media_ttt_receiver_vert_translation">40dp</dimen> <!-- Window magnification --> <dimen name="magnification_border_drag_size">35dp</dimen> @@ -1289,6 +1290,9 @@ <!-- DREAMING -> LOCKSCREEN transition: Amount to shift lockscreen content on entering --> <dimen name="dreaming_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen> + <!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering --> + <dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen> + <!-- The amount of vertical offset for the keyguard during the full shade transition. --> <dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index ce3084c6046c..2eb58b9c98ec 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2870,7 +2870,7 @@ Error message shown when a button should be pressed and held to activate it, usually shown when the user attempted to tap the button or held it for too short a time. [CHAR LIMIT=32]. --> - <string name="keyguard_affordance_press_too_short">Press and hold to activate</string> + <string name="keyguard_affordance_press_too_short">Touch & hold to open</string> <!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] --> <string name="rear_display_bottom_sheet_cancel">Cancel</string> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java index b057fe422fc4..cc3d7a8931b0 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java @@ -57,7 +57,7 @@ public class PluginActionManager<T extends Plugin> { private static final boolean DEBUG = false; - private static final String TAG = "PluginInstanceManager"; + private static final String TAG = "PluginActionManager"; public static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN"; private final Context mContext; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java index 82d70116bbff..e08a604338d2 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java @@ -43,6 +43,10 @@ public final class InteractionJankMonitorWrapper { InteractionJankMonitor.CUJ_LAUNCHER_QUICK_SWITCH; public static final int CUJ_OPEN_ALL_APPS = InteractionJankMonitor.CUJ_LAUNCHER_OPEN_ALL_APPS; + public static final int CUJ_CLOSE_ALL_APPS_SWIPE = + InteractionJankMonitor.CUJ_LAUNCHER_CLOSE_ALL_APPS_SWIPE; + public static final int CUJ_CLOSE_ALL_APPS_TO_HOME = + InteractionJankMonitor.CUJ_LAUNCHER_CLOSE_ALL_APPS_TO_HOME; public static final int CUJ_ALL_APPS_SCROLL = InteractionJankMonitor.CUJ_LAUNCHER_ALL_APPS_SCROLL; public static final int CUJ_APP_LAUNCH_FROM_WIDGET = @@ -65,7 +69,10 @@ public final class InteractionJankMonitorWrapper { CUJ_APP_LAUNCH_FROM_WIDGET, CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION, CUJ_RECENTS_SCROLLING, - CUJ_APP_SWIPE_TO_RECENTS + CUJ_APP_SWIPE_TO_RECENTS, + CUJ_OPEN_ALL_APPS, + CUJ_CLOSE_ALL_APPS_SWIPE, + CUJ_CLOSE_ALL_APPS_TO_HOME }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt index f974e27a99ec..edd150c293f6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt @@ -19,6 +19,8 @@ package com.android.keyguard import android.content.Context import android.view.ViewGroup import com.android.systemui.R +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState.KEYGUARD import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START @@ -36,27 +38,29 @@ class KeyguardUnfoldTransition @Inject constructor( private val context: Context, - unfoldProgressProvider: NaturalRotationUnfoldProgressProvider + statusBarStateController: StatusBarStateController, + unfoldProgressProvider: NaturalRotationUnfoldProgressProvider, ) { /** Certain views only need to move if they are not currently centered */ var statusViewCentered = false - private val filterSplitShadeOnly = { !statusViewCentered } - private val filterNever = { true } + private val filterKeyguardAndSplitShadeOnly: () -> Boolean = { + statusBarStateController.getState() == KEYGUARD && !statusViewCentered } + private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD } private val translateAnimator by lazy { UnfoldConstantTranslateAnimator( viewsIdToTranslate = setOf( - ViewIdToTranslate(R.id.keyguard_status_area, START, filterNever), + ViewIdToTranslate(R.id.keyguard_status_area, START, filterKeyguard), ViewIdToTranslate( - R.id.lockscreen_clock_view_large, START, filterSplitShadeOnly), - ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterNever), + R.id.lockscreen_clock_view_large, START, filterKeyguardAndSplitShadeOnly), + ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterKeyguard), ViewIdToTranslate( - R.id.notification_stack_scroller, END, filterSplitShadeOnly), - ViewIdToTranslate(R.id.start_button, START, filterNever), - ViewIdToTranslate(R.id.end_button, END, filterNever)), + R.id.notification_stack_scroller, END, filterKeyguardAndSplitShadeOnly), + ViewIdToTranslate(R.id.start_button, START, filterKeyguard), + ViewIdToTranslate(R.id.end_button, END, filterKeyguard)), progressProvider = unfoldProgressProvider) } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index e3c58ce1e061..271fc7bc836f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -875,7 +875,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab Assert.isMainThread(); if (mWakeOnFingerprintAcquiredStart && acquireInfo == FINGERPRINT_ACQUIRED_START) { mPowerManager.wakeUp( - SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, + SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, "com.android.systemui.keyguard:FINGERPRINT_ACQUIRED_START"); } for (int i = 0; i < mCallbacks.size(); i++) { diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java index 82e570438dab..805a20a6d965 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardListener.java @@ -16,15 +16,21 @@ package com.android.systemui.clipboardoverlay; +import static android.content.ClipDescription.CLASSIFICATION_COMPLETE; + import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED; import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED; +import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN; + +import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.os.SystemProperties; import android.provider.DeviceConfig; +import android.provider.Settings; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; @@ -56,6 +62,7 @@ public class ClipboardListener implements private final DeviceConfigProxy mDeviceConfig; private final Provider<ClipboardOverlayController> mOverlayProvider; private final ClipboardOverlayControllerLegacyFactory mOverlayFactory; + private final ClipboardToast mClipboardToast; private final ClipboardManager mClipboardManager; private final UiEventLogger mUiEventLogger; private final FeatureFlags mFeatureFlags; @@ -66,6 +73,7 @@ public class ClipboardListener implements public ClipboardListener(Context context, DeviceConfigProxy deviceConfigProxy, Provider<ClipboardOverlayController> clipboardOverlayControllerProvider, ClipboardOverlayControllerLegacyFactory overlayFactory, + ClipboardToast clipboardToast, ClipboardManager clipboardManager, UiEventLogger uiEventLogger, FeatureFlags featureFlags) { @@ -73,6 +81,7 @@ public class ClipboardListener implements mDeviceConfig = deviceConfigProxy; mOverlayProvider = clipboardOverlayControllerProvider; mOverlayFactory = overlayFactory; + mClipboardToast = clipboardToast; mClipboardManager = clipboardManager; mUiEventLogger = uiEventLogger; mFeatureFlags = featureFlags; @@ -102,6 +111,15 @@ public class ClipboardListener implements return; } + if (!isUserSetupComplete()) { + // just show a toast, user should not access intents from this state + if (shouldShowToast(clipData)) { + mUiEventLogger.log(CLIPBOARD_TOAST_SHOWN, 0, clipSource); + mClipboardToast.showCopiedToast(); + } + return; + } + boolean enabled = mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR); if (mClipboardOverlay == null || enabled != mUsingNewOverlay) { mUsingNewOverlay = enabled; @@ -136,10 +154,26 @@ public class ClipboardListener implements return clipData.getDescription().getExtras().getBoolean(EXTRA_SUPPRESS_OVERLAY, false); } + boolean shouldShowToast(ClipData clipData) { + if (clipData == null) { + return false; + } else if (clipData.getDescription().getClassificationStatus() == CLASSIFICATION_COMPLETE) { + // only show for classification complete if we aren't already showing a toast, to ignore + // the duplicate ClipData with classification + return !mClipboardToast.isShowing(); + } + return true; + } + private static boolean isEmulator() { return SystemProperties.getBoolean("ro.boot.qemu", false); } + private boolean isUserSetupComplete() { + return Settings.Secure.getInt(mContext.getContentResolver(), + SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + } + interface ClipboardOverlay { void setClipData(ClipData clipData, String clipSource); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java index 9917507ec3bf..4b5f8765c4d0 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayEvent.java @@ -43,7 +43,9 @@ public enum ClipboardOverlayEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "clipboard overlay tapped outside") CLIPBOARD_OVERLAY_TAP_OUTSIDE(1077), @UiEvent(doc = "clipboard overlay dismissed, miscellaneous reason") - CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078); + CLIPBOARD_OVERLAY_DISMISSED_OTHER(1078), + @UiEvent(doc = "clipboard toast shown") + CLIPBOARD_TOAST_SHOWN(1270); private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java new file mode 100644 index 000000000000..0ed7d2711c62 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardToast.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.clipboardoverlay; + +import android.content.Context; +import android.widget.Toast; + +import com.android.systemui.R; + +import javax.inject.Inject; + +/** + * Utility class for showing a simple clipboard toast on copy. + */ +class ClipboardToast extends Toast.Callback { + private final Context mContext; + private Toast mCopiedToast; + + @Inject + ClipboardToast(Context context) { + mContext = context; + } + + void showCopiedToast() { + if (mCopiedToast != null) { + mCopiedToast.cancel(); + } + mCopiedToast = Toast.makeText(mContext, + R.string.clipboard_overlay_text_copied, Toast.LENGTH_SHORT); + mCopiedToast.show(); + } + + boolean isShowing() { + return mCopiedToast != null; + } + + @Override // Toast.Callback + public void onToastHidden() { + super.onToastHidden(); + mCopiedToast = null; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt new file mode 100644 index 000000000000..e5ec727f0437 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.compose + +import androidx.activity.ComponentActivity +import com.android.systemui.people.ui.viewmodel.PeopleViewModel + +/** + * A facade to interact with Compose, when it is available. + * + * You should access this facade by calling the static methods on + * [com.android.systemui.compose.ComposeFacade] directly. + */ +interface BaseComposeFacade { + /** + * Whether Compose is currently available. This function should be checked before calling any + * other functions on this facade. + * + * This value will never change at runtime. + */ + fun isComposeAvailable(): Boolean + + /** Bind the content of [activity] to [viewModel]. */ + fun setPeopleSpaceActivityContent( + activity: ComponentActivity, + viewModel: PeopleViewModel, + onResult: (PeopleViewModel.Result) -> Unit, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 96707f458199..59f68f7c72ba 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -44,6 +44,7 @@ import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.statusbar.notification.fsi.FsiChromeRepo import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.notification.fsi.FsiChromeViewModelFactory +import com.android.systemui.statusbar.notification.fsi.FsiChromeViewBinder import com.android.systemui.statusbar.phone.KeyguardLiftController import com.android.systemui.stylus.StylusUsiPowerStartable import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -94,6 +95,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(FsiChromeViewModelFactory::class) abstract fun bindFSIChromeWindowViewModel(sysui: FsiChromeViewModelFactory): CoreStartable + /** Inject into FsiChromeWindowBinder. */ + @Binds + @IntoMap + @ClassKey(FsiChromeViewBinder::class) + abstract fun bindFsiChromeWindowBinder(sysui: FsiChromeViewBinder): CoreStartable + /** Inject into GarbageMonitor.Service. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt index 16f415070b45..c746efdbbd30 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt @@ -20,7 +20,7 @@ import android.content.Context import android.database.ContentObserver import android.os.Handler import android.os.Looper -import android.provider.Settings +import com.android.systemui.util.settings.GlobalSettings /** * Class to track the availability of [DemoMode]. Use this class to track the availability and @@ -29,7 +29,10 @@ import android.provider.Settings * This class works by wrapping a content observer for the relevant keys related to DemoMode * availability and current on/off state, and triggering callbacks. */ -abstract class DemoModeAvailabilityTracker(val context: Context) { +abstract class DemoModeAvailabilityTracker( + val context: Context, + val globalSettings: GlobalSettings, +) { var isInDemoMode = false var isDemoModeAvailable = false @@ -41,9 +44,9 @@ abstract class DemoModeAvailabilityTracker(val context: Context) { fun startTracking() { val resolver = context.contentResolver resolver.registerContentObserver( - Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver) + globalSettings.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver) resolver.registerContentObserver( - Settings.Global.getUriFor(DEMO_MODE_ON), false, onObserver) + globalSettings.getUriFor(DEMO_MODE_ON), false, onObserver) } fun stopTracking() { @@ -57,12 +60,11 @@ abstract class DemoModeAvailabilityTracker(val context: Context) { abstract fun onDemoModeFinished() private fun checkIsDemoModeAllowed(): Boolean { - return Settings.Global - .getInt(context.contentResolver, DEMO_MODE_ALLOWED, 0) != 0 + return globalSettings.getInt(DEMO_MODE_ALLOWED, 0) != 0 } private fun checkIsDemoModeOn(): Boolean { - return Settings.Global.getInt(context.contentResolver, DEMO_MODE_ON, 0) != 0 + return globalSettings.getInt(DEMO_MODE_ON, 0) != 0 } private val allowedObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { diff --git a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt index 000bbe6afc50..84f83f1ae956 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt +++ b/packages/SystemUI/src/com/android/systemui/demomode/DemoModeController.kt @@ -24,22 +24,28 @@ import android.os.Bundle import android.os.UserHandle import android.util.Log import com.android.systemui.Dumpable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.demomode.DemoMode.ACTION_DEMO import com.android.systemui.dump.DumpManager import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.util.Assert import com.android.systemui.util.settings.GlobalSettings import java.io.PrintWriter +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow /** * Handles system broadcasts for [DemoMode] * * Injected via [DemoModeModule] */ -class DemoModeController constructor( +class DemoModeController +constructor( private val context: Context, private val dumpManager: DumpManager, - private val globalSettings: GlobalSettings + private val globalSettings: GlobalSettings, + private val broadcastDispatcher: BroadcastDispatcher, ) : CallbackController<DemoMode>, Dumpable { // Var updated when the availability tracker changes, or when we enter/exit demo mode in-process @@ -58,9 +64,7 @@ class DemoModeController constructor( requestFinishDemoMode() val m = mutableMapOf<String, MutableList<DemoMode>>() - DemoMode.COMMANDS.map { command -> - m.put(command, mutableListOf()) - } + DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) } receiverMap = m } @@ -71,7 +75,7 @@ class DemoModeController constructor( initialized = true - dumpManager.registerDumpable(TAG, this) + dumpManager.registerNormalDumpable(TAG, this) // Due to DemoModeFragment running in systemui:tuner process, we have to observe for // content changes to know if the setting turned on or off @@ -81,8 +85,13 @@ class DemoModeController constructor( val demoFilter = IntentFilter() demoFilter.addAction(ACTION_DEMO) - context.registerReceiverAsUser(broadcastReceiver, UserHandle.ALL, demoFilter, - android.Manifest.permission.DUMP, null, Context.RECEIVER_EXPORTED) + + broadcastDispatcher.registerReceiver( + receiver = broadcastReceiver, + filter = demoFilter, + user = UserHandle.ALL, + permission = android.Manifest.permission.DUMP, + ) } override fun addCallback(listener: DemoMode) { @@ -91,16 +100,15 @@ class DemoModeController constructor( commands.forEach { command -> if (!receiverMap.containsKey(command)) { - throw IllegalStateException("Command ($command) not recognized. " + - "See DemoMode.java for valid commands") + throw IllegalStateException( + "Command ($command) not recognized. " + "See DemoMode.java for valid commands" + ) } receiverMap[command]!!.add(listener) } - synchronized(this) { - receivers.add(listener) - } + synchronized(this) { receivers.add(listener) } if (isInDemoMode) { listener.onDemoModeStarted() @@ -109,14 +117,46 @@ class DemoModeController constructor( override fun removeCallback(listener: DemoMode) { synchronized(this) { - listener.demoCommands().forEach { command -> - receiverMap[command]!!.remove(listener) - } + listener.demoCommands().forEach { command -> receiverMap[command]!!.remove(listener) } receivers.remove(listener) } } + /** + * Create a [Flow] for the stream of demo mode arguments that come in for the given [command] + * + * This is equivalent of creating a listener manually and adding an event handler for the given + * command, like so: + * + * ``` + * class Demoable { + * private val demoHandler = object : DemoMode { + * override fun demoCommands() = listOf(<command>) + * + * override fun dispatchDemoCommand(command: String, args: Bundle) { + * handleDemoCommand(args) + * } + * } + * } + * ``` + * + * @param command The top-level demo mode command you want a stream for + */ + fun demoFlowForCommand(command: String): Flow<Bundle> = conflatedCallbackFlow { + val callback = + object : DemoMode { + override fun demoCommands(): List<String> = listOf(command) + + override fun dispatchDemoCommand(command: String, args: Bundle) { + trySend(args) + } + } + + addCallback(callback) + awaitClose { removeCallback(callback) } + } + private fun setIsDemoModeAllowed(enabled: Boolean) { // Turn off demo mode if it was on if (isInDemoMode && !enabled) { @@ -129,13 +169,9 @@ class DemoModeController constructor( Assert.isMainThread() val copy: List<DemoModeCommandReceiver> - synchronized(this) { - copy = receivers.toList() - } + synchronized(this) { copy = receivers.toList() } - copy.forEach { r -> - r.onDemoModeStarted() - } + copy.forEach { r -> r.onDemoModeStarted() } } private fun exitDemoMode() { @@ -143,18 +179,13 @@ class DemoModeController constructor( Assert.isMainThread() val copy: List<DemoModeCommandReceiver> - synchronized(this) { - copy = receivers.toList() - } + synchronized(this) { copy = receivers.toList() } - copy.forEach { r -> - r.onDemoModeFinished() - } + copy.forEach { r -> r.onDemoModeFinished() } } fun dispatchDemoCommand(command: String, args: Bundle) { Assert.isMainThread() - if (DEBUG) { Log.d(TAG, "dispatchDemoCommand: $command, args=$args") } @@ -172,9 +203,7 @@ class DemoModeController constructor( } // See? demo mode is easy now, you just notify the listeners when their command is called - receiverMap[command]!!.forEach { receiver -> - receiver.dispatchDemoCommand(command, args) - } + receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) } } override fun dump(pw: PrintWriter, args: Array<out String>) { @@ -183,65 +212,64 @@ class DemoModeController constructor( pw.println(" isDemoModeAllowed=$isAvailable") pw.print(" receivers=[") val copy: List<DemoModeCommandReceiver> - synchronized(this) { - copy = receivers.toList() - } - copy.forEach { recv -> - pw.print(" ${recv.javaClass.simpleName}") - } + synchronized(this) { copy = receivers.toList() } + copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") } pw.println(" ]") pw.println(" receiverMap= [") receiverMap.keys.forEach { command -> pw.print(" $command : [") - val recvs = receiverMap[command]!!.map { receiver -> - receiver.javaClass.simpleName - }.joinToString(",") + val recvs = + receiverMap[command]!! + .map { receiver -> receiver.javaClass.simpleName } + .joinToString(",") pw.println("$recvs ]") } } - private val tracker = object : DemoModeAvailabilityTracker(context) { - override fun onDemoModeAvailabilityChanged() { - setIsDemoModeAllowed(isDemoModeAvailable) - } - - override fun onDemoModeStarted() { - if (this@DemoModeController.isInDemoMode != isInDemoMode) { - enterDemoMode() + private val tracker = + object : DemoModeAvailabilityTracker(context, globalSettings) { + override fun onDemoModeAvailabilityChanged() { + setIsDemoModeAllowed(isDemoModeAvailable) } - } - override fun onDemoModeFinished() { - if (this@DemoModeController.isInDemoMode != isInDemoMode) { - exitDemoMode() + override fun onDemoModeStarted() { + if (this@DemoModeController.isInDemoMode != isInDemoMode) { + enterDemoMode() + } } - } - } - private val broadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (DEBUG) { - Log.v(TAG, "onReceive: $intent") - } - - val action = intent.action - if (!ACTION_DEMO.equals(action)) { - return - } - - val bundle = intent.extras ?: return - val command = bundle.getString("command", "").trim().toLowerCase() - if (command.length == 0) { - return + override fun onDemoModeFinished() { + if (this@DemoModeController.isInDemoMode != isInDemoMode) { + exitDemoMode() + } } + } - try { - dispatchDemoCommand(command, bundle) - } catch (t: Throwable) { - Log.w(TAG, "Error running demo command, intent=$intent $t") + private val broadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (DEBUG) { + Log.v(TAG, "onReceive: $intent") + } + + val action = intent.action + if (!ACTION_DEMO.equals(action)) { + return + } + + val bundle = intent.extras ?: return + val command = bundle.getString("command", "").trim().lowercase() + if (command.isEmpty()) { + return + } + + try { + dispatchDemoCommand(command, bundle) + } catch (t: Throwable) { + Log.w(TAG, "Error running demo command, intent=$intent $t") + } } } - } fun requestSetDemoModeAllowed(allowed: Boolean) { setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0) @@ -258,10 +286,12 @@ class DemoModeController constructor( private fun setGlobal(key: String, value: Int) { globalSettings.putInt(key, value) } + + companion object { + const val DEMO_MODE_ALLOWED = "sysui_demo_allowed" + const val DEMO_MODE_ON = "sysui_tuner_demo_on" + } } private const val TAG = "DemoModeController" -private const val DEMO_MODE_ALLOWED = "sysui_demo_allowed" -private const val DEMO_MODE_ON = "sysui_tuner_demo_on" - private const val DEBUG = false diff --git a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java index de9affb5c748..b84fa5af4d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java +++ b/packages/SystemUI/src/com/android/systemui/demomode/dagger/DemoModeModule.java @@ -18,6 +18,7 @@ package com.android.systemui.demomode.dagger; import android.content.Context; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; @@ -37,8 +38,14 @@ public abstract class DemoModeModule { static DemoModeController provideDemoModeController( Context context, DumpManager dumpManager, - GlobalSettings globalSettings) { - DemoModeController dmc = new DemoModeController(context, dumpManager, globalSettings); + GlobalSettings globalSettings, + BroadcastDispatcher broadcastDispatcher + ) { + DemoModeController dmc = new DemoModeController( + context, + dumpManager, + globalSettings, + broadcastDispatcher); dmc.initialize(); return /*run*/dmc; } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java index 5d21349fce11..5b90ef2bb806 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java @@ -16,7 +16,14 @@ package com.android.systemui.doze; +import static android.os.PowerManager.WAKE_REASON_BIOMETRIC; +import static android.os.PowerManager.WAKE_REASON_GESTURE; +import static android.os.PowerManager.WAKE_REASON_LIFT; +import static android.os.PowerManager.WAKE_REASON_PLUGGED_IN; +import static android.os.PowerManager.WAKE_REASON_TAP; + import android.annotation.IntDef; +import android.os.PowerManager; import android.util.TimeUtils; import androidx.annotation.NonNull; @@ -511,6 +518,25 @@ public class DozeLog implements Dumpable { } } + /** + * Converts {@link Reason} to {@link PowerManager.WakeReason}. + */ + public static @PowerManager.WakeReason int getPowerManagerWakeReason(@Reason int wakeReason) { + switch (wakeReason) { + case REASON_SENSOR_DOUBLE_TAP: + case REASON_SENSOR_TAP: + return WAKE_REASON_TAP; + case REASON_SENSOR_PICKUP: + return WAKE_REASON_LIFT; + case REASON_SENSOR_UDFPS_LONG_PRESS: + return WAKE_REASON_BIOMETRIC; + case PULSE_REASON_DOCKING: + return WAKE_REASON_PLUGGED_IN; + default: + return WAKE_REASON_GESTURE; + } + } + @Retention(RetentionPolicy.SOURCE) @IntDef({PULSE_REASON_NONE, PULSE_REASON_INTENT, PULSE_REASON_NOTIFICATION, PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP, diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java index f8bd1e712b1b..ba38ab0583d4 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeService.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeService.java @@ -116,8 +116,8 @@ public class DozeService extends DreamService @Override public void requestWakeUp(@DozeLog.Reason int reason) { - PowerManager pm = getSystemService(PowerManager.class); - pm.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, + final PowerManager pm = getSystemService(PowerManager.class); + pm.wakeUp(SystemClock.uptimeMillis(), DozeLog.getPowerManagerWakeReason(reason), "com.android.systemui:NODOZE " + DozeLog.reasonToString(reason)); } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index abe9355d3375..c882f8adceb6 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -101,13 +101,11 @@ constructor( transitionViewModel.dreamOverlayTranslationY(it.translationYPx) } .collect { px -> - setElementsTranslationYAtPosition( - px, - ComplicationLayoutParams.POSITION_TOP - ) - setElementsTranslationYAtPosition( - px, - ComplicationLayoutParams.POSITION_BOTTOM + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsTranslationYAtPosition(px, position) + }, + POSITION_TOP or POSITION_BOTTOM ) } } @@ -115,15 +113,15 @@ constructor( /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */ launch { transitionViewModel.dreamOverlayAlpha.collect { alpha -> - setElementsAlphaAtPosition( - alpha = alpha, - position = ComplicationLayoutParams.POSITION_TOP, - fadingOut = true, - ) - setElementsAlphaAtPosition( - alpha = alpha, - position = ComplicationLayoutParams.POSITION_BOTTOM, - fadingOut = true, + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsAlphaAtPosition( + alpha = alpha, + position = position, + fadingOut = true, + ) + }, + POSITION_TOP or POSITION_BOTTOM ) } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt index ab4632b08fa1..d5ff8f21abb2 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt @@ -20,26 +20,39 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.policy.CallbackController import javax.inject.Inject -/** Dream-related callback information */ +/** Dream overlay-related callback information */ @SysUISingleton -class DreamCallbackController @Inject constructor() : - CallbackController<DreamCallbackController.DreamCallback> { +class DreamOverlayCallbackController @Inject constructor() : + CallbackController<DreamOverlayCallbackController.Callback> { - private val callbacks = mutableSetOf<DreamCallbackController.DreamCallback>() + private val callbacks = mutableSetOf<DreamOverlayCallbackController.Callback>() - override fun addCallback(callback: DreamCallbackController.DreamCallback) { + var isDreaming = false + private set + + override fun addCallback(callback: DreamOverlayCallbackController.Callback) { callbacks.add(callback) } - override fun removeCallback(callback: DreamCallbackController.DreamCallback) { + override fun removeCallback(callback: DreamOverlayCallbackController.Callback) { callbacks.remove(callback) } fun onWakeUp() { + isDreaming = false callbacks.forEach { it.onWakeUp() } } - interface DreamCallback { + fun onStartDream() { + isDreaming = true + callbacks.forEach { it.onStartDream() } + } + + interface Callback { + /** Dream overlay has ended */ fun onWakeUp() + + /** Dream overlay has started */ + fun onStartDream() } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 1763dd9ac990..fdc115bd84ab 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -70,7 +70,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ // A controller for the dream overlay container view (which contains both the status bar and the // content area). private DreamOverlayContainerViewController mDreamOverlayContainerViewController; - private final DreamCallbackController mDreamCallbackController; + private final DreamOverlayCallbackController mDreamOverlayCallbackController; private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Nullable private final ComponentName mLowLightDreamComponent; @@ -151,7 +151,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ TouchInsetManager touchInsetManager, @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) ComponentName lowLightDreamComponent, - DreamCallbackController dreamCallbackController) { + DreamOverlayCallbackController dreamOverlayCallbackController) { mContext = context; mExecutor = executor; mWindowManager = windowManager; @@ -160,7 +160,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback); mStateController = stateController; mUiEventLogger = uiEventLogger; - mDreamCallbackController = dreamCallbackController; + mDreamOverlayCallbackController = dreamOverlayCallbackController; final ViewModelStore viewModelStore = new ViewModelStore(); final Complication.Host host = @@ -229,6 +229,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent)); mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START); + mDreamOverlayCallbackController.onStartDream(); mStarted = true; }); } @@ -245,7 +246,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ public void onWakeUp(@NonNull Runnable onCompletedCallback) { mExecutor.execute(() -> { if (mDreamOverlayContainerViewController != null) { - mDreamCallbackController.onWakeUp(); + mDreamOverlayCallbackController.onWakeUp(); mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor); } }); @@ -255,6 +256,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ * Inserts {@link Window} to host the dream overlay into the dream's parent window. Must be * called from the main executing thread. The window attributes closely mirror those that are * set by the {@link android.service.dreams.DreamService} on the dream Window. + * * @param layoutParams The {@link android.view.WindowManager.LayoutParams} which allow inserting * into the dream window. */ @@ -301,7 +303,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private void resetCurrentDreamOverlayLocked() { if (mStarted && mWindow != null) { - mWindowManager.removeView(mWindow.getDecorView()); + try { + mWindowManager.removeView(mWindow.getDecorView()); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Error removing decor view when resetting overlay", e); + } } mStateController.setOverlayActive(false); diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 4268d3ecacc7..7b876d036e21 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -330,6 +330,8 @@ object Flags { // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") + val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot") + // 1100 - windowing @Keep @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 9a0fbbf60f60..a4fd087a24b1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -29,8 +29,7 @@ import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener -import com.android.systemui.dreams.DreamCallbackController -import com.android.systemui.dreams.DreamCallbackController.DreamCallback +import com.android.systemui.dreams.DreamOverlayCallbackController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource @@ -49,7 +48,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.merge /** Defines interface for classes that encapsulate application state for the keyguard. */ interface KeyguardRepository { @@ -81,6 +79,9 @@ interface KeyguardRepository { */ val isKeyguardShowing: Flow<Boolean> + /** Is an activity showing over the keyguard? */ + val isKeyguardOccluded: Flow<Boolean> + /** Observable for the signal that keyguard is about to go away. */ val isKeyguardGoingAway: Flow<Boolean> @@ -107,6 +108,9 @@ interface KeyguardRepository { */ val isDreaming: Flow<Boolean> + /** Observable for whether the device is dreaming with an overlay, see [DreamOverlayService] */ + val isDreamingWithOverlay: Flow<Boolean> + /** * Observable for the amount of doze we are currently in. * @@ -179,7 +183,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, private val authController: AuthController, - private val dreamCallbackController: DreamCallbackController, + private val dreamOverlayCallbackController: DreamOverlayCallbackController, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) override val animateBottomAreaDozingTransitions = @@ -191,28 +195,55 @@ constructor( private val _clockPosition = MutableStateFlow(Position(0, 0)) override val clockPosition = _clockPosition.asStateFlow() - override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow { - val callback = - object : KeyguardStateController.Callback { - override fun onKeyguardShowingChanged() { - trySendWithFailureLogging( - keyguardStateController.isShowing, - TAG, - "updated isKeyguardShowing" - ) - } + override val isKeyguardShowing: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onKeyguardShowingChanged() { + trySendWithFailureLogging( + keyguardStateController.isShowing, + TAG, + "updated isKeyguardShowing" + ) + } + } + + keyguardStateController.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + keyguardStateController.isShowing, + TAG, + "initial isKeyguardShowing" + ) + + awaitClose { keyguardStateController.removeCallback(callback) } } + .distinctUntilChanged() - keyguardStateController.addCallback(callback) - // Adding the callback does not send an initial update. - trySendWithFailureLogging( - keyguardStateController.isShowing, - TAG, - "initial isKeyguardShowing" - ) + override val isKeyguardOccluded: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardStateController.Callback { + override fun onKeyguardShowingChanged() { + trySendWithFailureLogging( + keyguardStateController.isOccluded, + TAG, + "updated isKeyguardOccluded" + ) + } + } - awaitClose { keyguardStateController.removeCallback(callback) } - } + keyguardStateController.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + keyguardStateController.isOccluded, + TAG, + "initial isKeyguardOccluded" + ) + + awaitClose { keyguardStateController.removeCallback(callback) } + } + .distinctUntilChanged() override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow { val callback = @@ -279,36 +310,45 @@ constructor( } .distinctUntilChanged() - override val isDreaming: Flow<Boolean> = - merge( - conflatedCallbackFlow { - val callback = - object : KeyguardUpdateMonitorCallback() { - override fun onDreamingStateChanged(isDreaming: Boolean) { - trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming") - } + override val isDreamingWithOverlay: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DreamOverlayCallbackController.Callback { + override fun onStartDream() { + trySendWithFailureLogging(true, TAG, "updated isDreamingWithOverlay") } - keyguardUpdateMonitor.registerCallback(callback) - trySendWithFailureLogging( - keyguardUpdateMonitor.isDreaming, - TAG, - "initial isDreaming", - ) + override fun onWakeUp() { + trySendWithFailureLogging(false, TAG, "updated isDreamingWithOverlay") + } + } + dreamOverlayCallbackController.addCallback(callback) + trySendWithFailureLogging( + dreamOverlayCallbackController.isDreaming, + TAG, + "initial isDreamingWithOverlay", + ) + + awaitClose { dreamOverlayCallbackController.removeCallback(callback) } + } + .distinctUntilChanged() - awaitClose { keyguardUpdateMonitor.removeCallback(callback) } - }, - conflatedCallbackFlow { - val callback = - object : DreamCallback { - override fun onWakeUp() { - trySendWithFailureLogging(false, TAG, "updated isDreaming") - } + override val isDreaming: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onDreamingStateChanged(isDreaming: Boolean) { + trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming") } - dreamCallbackController.addCallback(callback) + } + keyguardUpdateMonitor.registerCallback(callback) + trySendWithFailureLogging( + keyguardUpdateMonitor.isDreaming, + TAG, + "initial isDreaming", + ) - awaitClose { dreamCallbackController.removeCallback(callback) } - } - ) + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } .distinctUntilChanged() override val linearDozeAmount: Flow<Float> = conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index d72d7183b0f0..343c2dc172fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -131,6 +131,10 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio } override fun startTransition(info: TransitionInfo): UUID? { + if (lastStep.from == info.from && lastStep.to == info.to) { + Log.i(TAG, "Duplicate call to start the transition, rejecting: $info") + return null + } if (lastStep.transitionState != TransitionState.FINISHED) { Log.i(TAG, "Transition still active: $lastStep, canceling") } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index f3d2905121bb..c2d139c21074 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo @@ -30,33 +31,33 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @SysUISingleton -class AodLockscreenTransitionInteractor +class FromAodTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromAodTransitionInteractor::class.simpleName!!) { override fun start() { - listenForTransitionToAodFromLockscreen() - listenForTransitionToLockscreenFromDozeStates() + listenForAodToLockscreen() + listenForAodToGone() } - private fun listenForTransitionToAodFromLockscreen() { + private fun listenForAodToLockscreen() { scope.launch { keyguardInteractor - .dozeTransitionTo(DozeStateModel.DOZE_AOD) + .dozeTransitionTo(DozeStateModel.FINISH) .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) .collect { pair -> val (dozeToAod, lastStartedStep) = pair - if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + if (lastStartedStep.to == KeyguardState.AOD) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - KeyguardState.LOCKSCREEN, KeyguardState.AOD, + KeyguardState.LOCKSCREEN, getAnimator(), ) ) @@ -65,20 +66,20 @@ constructor( } } - private fun listenForTransitionToLockscreenFromDozeStates() { - val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING) + private fun listenForAodToGone() { scope.launch { - keyguardInteractor - .dozeTransitionTo(DozeStateModel.FINISH) - .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + keyguardInteractor.biometricUnlockState + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) .collect { pair -> - val (dozeToAod, lastStartedStep) = pair - if (canGoToLockscreen.contains(lastStartedStep.to)) { + val (biometricUnlockState, keyguardState) = pair + if ( + keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState) + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - lastStartedStep.to, - KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + KeyguardState.GONE, getAnimator(), ) ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt index 056c44dc72cf..0e9c44703205 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt @@ -23,16 +23,18 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton -class BouncerToGoneTransitionInteractor +class FromBouncerTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, @@ -40,15 +42,54 @@ constructor( private val shadeRepository: ShadeRepository, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor -) : TransitionInteractor(BouncerToGoneTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromBouncerTransitionInteractor::class.simpleName!!) { private var transitionId: UUID? = null override fun start() { - listenForKeyguardGoingAway() + listenForBouncerToGone() + listenForBouncerToLockscreenOrAod() } - private fun listenForKeyguardGoingAway() { + private fun listenForBouncerToLockscreenOrAod() { + scope.launch { + keyguardInteractor.isBouncerShowing + .sample( + combine( + keyguardInteractor.wakefulnessModel, + keyguardTransitionInteractor.startedKeyguardTransitionStep, + ::Pair + ), + ::toTriple + ) + .collect { triple -> + val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple + if ( + !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER + ) { + val to = + if ( + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP || + wakefulnessState.state == WakefulnessState.ASLEEP + ) { + KeyguardState.AOD + } else { + KeyguardState.LOCKSCREEN + } + keyguardTransitionRepository.startTransition( + TransitionInfo( + ownerName = name, + from = KeyguardState.BOUNCER, + to = to, + animator = getAnimator(), + ) + ) + } + } + } + } + + private fun listenForBouncerToGone() { scope.launch { keyguardInteractor.isKeyguardGoingAway .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index 95d96025cf4a..fd2d271e40f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -21,36 +21,46 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @SysUISingleton -class LockscreenGoneTransitionInteractor +class FromDozingTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, -) : TransitionInteractor(LockscreenGoneTransitionInteractor::class.simpleName!!) { + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, +) : TransitionInteractor(FromDozingTransitionInteractor::class.simpleName!!) { override fun start() { + listenForDozingToLockscreen() + } + + private fun listenForDozingToLockscreen() { scope.launch { - keyguardInteractor.isKeyguardGoingAway + keyguardInteractor.dozeTransitionModel .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) .collect { pair -> - val (isKeyguardGoingAway, lastStartedStep) = pair - if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { + val (dozeTransitionModel, lastStartedTransition) = pair + if ( + isDozeOff(dozeTransitionModel.to) && + lastStartedTransition.to == KeyguardState.DOZING + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, + KeyguardState.DOZING, KeyguardState.LOCKSCREEN, - KeyguardState.GONE, getAnimator(), ) ) @@ -59,14 +69,14 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 10L + private val DEFAULT_DURATION = 500.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 188930c36097..3b09ae7ba8ea 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -36,52 +36,48 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton -class DreamingTransitionInteractor +class FromDreamingTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(DreamingTransitionInteractor::class.simpleName!!) { - - private val canDreamFrom = - setOf(KeyguardState.LOCKSCREEN, KeyguardState.GONE, KeyguardState.DOZING) +) : TransitionInteractor(FromDreamingTransitionInteractor::class.simpleName!!) { override fun start() { - listenForEntryToDreaming() listenForDreamingToLockscreen() + listenForDreamingToOccluded() listenForDreamingToGone() listenForDreamingToDozing() } - private fun listenForEntryToDreaming() { + private fun listenForDreamingToLockscreen() { scope.launch { - keyguardInteractor.isDreaming + // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which + // otherwise would have gone through OCCLUDED first + keyguardInteractor.isDreamingWithOverlay .sample( combine( keyguardInteractor.dozeTransitionModel, - keyguardTransitionInteractor.finishedKeyguardState, + keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair ), ::toTriple ) .collect { triple -> - val (isDreaming, dozeTransitionModel, keyguardState) = triple - // Dozing/AOD and dreaming have overlapping events. If the state remains in - // FINISH, it means that doze mode is not running and DREAMING is ok to - // commence. + val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple if ( - isDozeOff(dozeTransitionModel.to) && - isDreaming && - canDreamFrom.contains(keyguardState) + !isDreaming && + isDozeOff(dozeTransitionModel.to) && + lastStartedTransition.to == KeyguardState.DREAMING ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - keyguardState, KeyguardState.DREAMING, - getAnimator(), + KeyguardState.LOCKSCREEN, + getAnimator(TO_LOCKSCREEN_DURATION), ) ) } @@ -89,30 +85,35 @@ constructor( } } - private fun listenForDreamingToLockscreen() { + private fun listenForDreamingToOccluded() { scope.launch { keyguardInteractor.isDreaming .sample( combine( - keyguardInteractor.dozeTransitionModel, + keyguardInteractor.isKeyguardOccluded, keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair, ), ::toTriple ) .collect { triple -> - val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple + val (isDreaming, isOccluded, lastStartedTransition) = triple if ( - isDozeOff(dozeTransitionModel.to) && + isOccluded && !isDreaming && - lastStartedTransition.to == KeyguardState.DREAMING + (lastStartedTransition.to == KeyguardState.DREAMING || + lastStartedTransition.to == KeyguardState.LOCKSCREEN) ) { + // At the moment, checking for LOCKSCREEN state above provides a corrective + // action. There's no great signal to determine when the dream is ending + // and a transition to OCCLUDED is beginning directly. For now, the solution + // is DREAMING->LOCKSCREEN->OCCLUDED keyguardTransitionRepository.startTransition( TransitionInfo( name, - KeyguardState.DREAMING, - KeyguardState.LOCKSCREEN, - getAnimator(TO_LOCKSCREEN_DURATION), + lastStartedTransition.to, + KeyguardState.OCCLUDED, + getAnimator(), ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index a50e75909dd8..553fafeb92c3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -30,19 +30,44 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @SysUISingleton -class GoneAodTransitionInteractor +class FromGoneTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(GoneAodTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) { override fun start() { + listenForGoneToAod() + listenForGoneToDreaming() + } + + private fun listenForGoneToDreaming() { + scope.launch { + keyguardInteractor.isAbleToDream + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) + .collect { pair -> + val (isAbleToDream, keyguardState) = pair + if (isAbleToDream && keyguardState == KeyguardState.GONE) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.GONE, + KeyguardState.DREAMING, + getAnimator(), + ) + ) + } + } + } + } + + private fun listenForGoneToAod() { scope.launch { keyguardInteractor.wakefulnessModel - .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) .collect { pair -> val (wakefulnessState, keyguardState) = pair if ( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 5cb7d709a1a2..326acc9eb762 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -21,11 +21,11 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID @@ -36,57 +36,54 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton -class LockscreenBouncerTransitionInteractor +class FromLockscreenTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val shadeRepository: ShadeRepository, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor -) : TransitionInteractor(LockscreenBouncerTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) { private var transitionId: UUID? = null override fun start() { - listenForDraggingUpToBouncer() - listenForBouncer() + listenForLockscreenToGone() + listenForLockscreenToOccluded() + listenForLockscreenToAod() + listenForLockscreenToBouncer() + listenForLockscreenToDreaming() + listenForLockscreenToBouncerDragging() } - private fun listenForBouncer() { + private fun listenForLockscreenToDreaming() { scope.launch { - keyguardInteractor.isBouncerShowing - .sample( - combine( - keyguardInteractor.wakefulnessModel, - keyguardTransitionInteractor.startedKeyguardTransitionStep, - ::Pair - ), - ::toTriple - ) - .collect { triple -> - val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple - if ( - !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER - ) { - val to = - if ( - wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP || - wakefulnessState.state == WakefulnessState.ASLEEP - ) { - KeyguardState.AOD - } else { - KeyguardState.LOCKSCREEN - } + keyguardInteractor.isAbleToDream + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isAbleToDream, lastStartedTransition) = pair + if (isAbleToDream && lastStartedTransition.to == KeyguardState.LOCKSCREEN) { keyguardTransitionRepository.startTransition( TransitionInfo( - ownerName = name, - from = KeyguardState.BOUNCER, - to = to, - animator = getAnimator(), + name, + KeyguardState.LOCKSCREEN, + KeyguardState.DREAMING, + getAnimator(), ) ) - } else if ( + } + } + } + } + + private fun listenForLockscreenToBouncer() { + scope.launch { + keyguardInteractor.isBouncerShowing + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isBouncerShowing, lastStartedTransitionStep) = pair + if ( isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN ) { keyguardTransitionRepository.startTransition( @@ -98,13 +95,12 @@ constructor( ) ) } - Unit } } } /* Starts transitions when manually dragging up the bouncer from the lockscreen. */ - private fun listenForDraggingUpToBouncer() { + private fun listenForLockscreenToBouncerDragging() { scope.launch { shadeRepository.shadeModel .sample( @@ -157,6 +153,76 @@ constructor( } } + private fun listenForLockscreenToGone() { + scope.launch { + keyguardInteractor.isKeyguardGoingAway + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isKeyguardGoingAway, lastStartedStep) = pair + if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.GONE, + getAnimator(), + ) + ) + } + } + } + } + + private fun listenForLockscreenToOccluded() { + scope.launch { + keyguardInteractor.isKeyguardOccluded + .sample( + combine( + keyguardTransitionInteractor.finishedKeyguardState, + keyguardInteractor.isDreaming, + ::Pair + ), + ::toTriple + ) + .collect { triple -> + val (isOccluded, keyguardState, isDreaming) = triple + // Occlusion signals come from the framework, and should interrupt any + // existing transition + if (isOccluded && !isDreaming) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + keyguardState, + KeyguardState.OCCLUDED, + getAnimator(), + ) + ) + } + } + } + } + + private fun listenForLockscreenToAod() { + scope.launch { + keyguardInteractor + .dozeTransitionTo(DozeStateModel.DOZE_AOD) + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (dozeToAod, lastStartedStep) = pair + if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.LOCKSCREEN, + KeyguardState.AOD, + getAnimator(), + ) + ) + } + } + } + } + private fun getAnimator(): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) @@ -165,6 +231,6 @@ constructor( } companion object { - private const val TRANSITION_DURATION_MS = 300L + private const val TRANSITION_DURATION_MS = 500L } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index dad166f2b5e0..88789019b10f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -21,39 +21,43 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch @SysUISingleton -class AodToGoneTransitionInteractor +class FromOccludedTransitionInteractor @Inject constructor( @Application private val scope: CoroutineScope, private val keyguardInteractor: KeyguardInteractor, private val keyguardTransitionRepository: KeyguardTransitionRepository, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, -) : TransitionInteractor(AodToGoneTransitionInteractor::class.simpleName!!) { +) : TransitionInteractor(FromOccludedTransitionInteractor::class.simpleName!!) { override fun start() { + listenForOccludedToLockscreen() + listenForOccludedToDreaming() + } + + private fun listenForOccludedToDreaming() { scope.launch { - keyguardInteractor.biometricUnlockState - .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) }) + keyguardInteractor.isAbleToDream + .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) .collect { pair -> - val (biometricUnlockState, keyguardState) = pair - if ( - keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState) - ) { + val (isAbleToDream, keyguardState) = pair + if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) { keyguardTransitionRepository.startTransition( TransitionInfo( name, - KeyguardState.AOD, - KeyguardState.GONE, + KeyguardState.OCCLUDED, + KeyguardState.DREAMING, getAnimator(), ) ) @@ -62,14 +66,37 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun listenForOccludedToLockscreen() { + scope.launch { + keyguardInteractor.isKeyguardOccluded + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isOccluded, lastStartedKeyguardState) = pair + // Occlusion signals come from the framework, and should interrupt any + // existing transition + if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.OCCLUDED, + KeyguardState.LOCKSCREEN, + getAnimator(TO_LOCKSCREEN_DURATION), + ) + ) + } + } + } + } + + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_LOCKSCREEN_DURATION = 933.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 6912e1d3558e..402c1793f0b2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -22,12 +22,16 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.merge /** * Encapsulates business-logic related to the keyguard but not to a more specific part within it. @@ -52,8 +56,27 @@ constructor( * but not vice-versa. */ val isDreaming: Flow<Boolean> = repository.isDreaming + /** Whether the system is dreaming with an overlay active */ + val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay + + /** + * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means + * that doze mode is not running and DREAMING is ok to commence. + */ + val isAbleToDream: Flow<Boolean> = + merge(isDreaming, isDreamingWithOverlay) + .sample( + dozeTransitionModel, + { isDreaming, dozeTransitionModel -> + isDreaming && isDozeOff(dozeTransitionModel.to) + } + ) + .distinctUntilChanged() + /** Whether the keyguard is showing or not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + /** Whether the keyguard is occluded (covered by an activity). */ + val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded /** Whether the keyguard is going away. */ val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway /** Whether the bouncer is showing or not. */ @@ -74,8 +97,8 @@ constructor( /** The approximate location on the screen of the face unlock sensor, if one is available. */ val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation - fun dozeTransitionTo(state: DozeStateModel): Flow<DozeTransitionModel> { - return dozeTransitionModel.filter { it.to == state } + fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> { + return dozeTransitionModel.filter { states.contains(it.to) } } fun isKeyguardShowing(): Boolean { return repository.isKeyguardShowing() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt index bb8b79a3aa6b..fbed446b7d9a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt @@ -37,13 +37,13 @@ constructor( // exhaustive val ret = when (it) { - is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it") - is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") - is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it") - is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it") - is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it") - is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it") - is DreamingTransitionInteractor -> Log.d(TAG, "Started $it") + is FromBouncerTransitionInteractor -> Log.d(TAG, "Started $it") + is FromAodTransitionInteractor -> Log.d(TAG, "Started $it") + is FromGoneTransitionInteractor -> Log.d(TAG, "Started $it") + is FromLockscreenTransitionInteractor -> Log.d(TAG, "Started $it") + is FromDreamingTransitionInteractor -> Log.d(TAG, "Started $it") + is FromOccludedTransitionInteractor -> Log.d(TAG, "Started $it") + is FromDozingTransitionInteractor -> Log.d(TAG, "Started $it") } it.start() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 3b9d6f59a8e7..04024be571c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -24,7 +24,9 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlin.time.Duration @@ -50,6 +52,10 @@ constructor( val dreamingToLockscreenTransition: Flow<TransitionStep> = repository.transition(DREAMING, LOCKSCREEN) + /** OCCLUDED->LOCKSCREEN transition information. */ + val occludedToLockscreenTransition: Flow<TransitionStep> = + repository.transition(OCCLUDED, LOCKSCREEN) + /** (any)->AOD transition information */ val anyStateToAodTransition: Flow<TransitionStep> = repository.transitions.filter { step -> step.to == KeyguardState.AOD } @@ -93,7 +99,14 @@ constructor( val start = (params.startTime / totalDuration).toFloat() val chunks = (totalDuration / params.duration).toFloat() return flow - .map { step -> (step.value - start) * chunks } + // When starting, emit a value of 0f to give animations a chance to set initial state + .map { step -> + if (step.transitionState == STARTED) { + 0f + } else { + (step.value - start) * chunks + } + } .filter { value -> value >= 0f && value <= 1f } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt index 6e25200bc2f6..a59c407182e2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -89,6 +89,7 @@ constructor( KeyguardState.BOUNCER -> true KeyguardState.LOCKSCREEN -> true KeyguardState.GONE -> true + KeyguardState.OCCLUDED -> true } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt index 5f63ae765854..81fa2336d40d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt @@ -32,25 +32,25 @@ abstract class StartKeyguardTransitionModule { @Binds @IntoSet - abstract fun lockscreenBouncer( - impl: LockscreenBouncerTransitionInteractor - ): TransitionInteractor + abstract fun fromBouncer(impl: FromBouncerTransitionInteractor): TransitionInteractor @Binds @IntoSet - abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor + abstract fun fromLockscreen(impl: FromLockscreenTransitionInteractor): TransitionInteractor - @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor + @Binds @IntoSet abstract fun fromAod(impl: FromAodTransitionInteractor): TransitionInteractor - @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor + @Binds @IntoSet abstract fun fromGone(impl: FromGoneTransitionInteractor): TransitionInteractor @Binds @IntoSet - abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor + abstract fun fromDreaming(impl: FromDreamingTransitionInteractor): TransitionInteractor @Binds @IntoSet - abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor + abstract fun fromOccluded(impl: FromOccludedTransitionInteractor): TransitionInteractor - @Binds @IntoSet abstract fun dreaming(impl: DreamingTransitionInteractor): TransitionInteractor + @Binds + @IntoSet + abstract fun fromDozing(impl: FromDozingTransitionInteractor): TransitionInteractor } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt index 08ad3d5bdbf6..4d24c14501aa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt @@ -31,4 +31,6 @@ sealed class TransitionInteractor(val name: String) { abstract fun start() fun <A, B, C> toTriple(a: A, bc: Pair<B, C>) = Triple(a, bc.first, bc.second) + + fun <A, B, C> toTriple(ab: Pair<A, B>, c: C) = Triple(ab.first, ab.second, c) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt index dd908c420fcb..c7579862a717 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt @@ -57,4 +57,8 @@ enum class KeyguardState { * with SWIPE security method or face unlock without bypass. */ GONE, + /* + * An activity is displaying over the keyguard. + */ + OCCLUDED, } 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 ae8edfece4cb..b19795c38579 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 @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.SuppressLint import android.graphics.drawable.Animatable2 +import android.os.VibrationEffect import android.util.Size import android.util.TypedValue import android.view.MotionEvent @@ -43,8 +44,11 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.kotlin.pairwise import kotlin.math.pow import kotlin.math.sqrt +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine @@ -93,6 +97,7 @@ object KeyguardBottomAreaViewBinder { view: ViewGroup, viewModel: KeyguardBottomAreaViewModel, falsingManager: FalsingManager?, + vibratorHelper: VibratorHelper?, messageDisplayer: (Int) -> Unit, ): Binding { val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area) @@ -118,22 +123,48 @@ object KeyguardBottomAreaViewBinder { viewModel = buttonModel, falsingManager = falsingManager, messageDisplayer = messageDisplayer, + vibratorHelper = vibratorHelper, ) } } launch { + viewModel.startButton + .map { it.isActivated } + .pairwise() + .collect { (prev, next) -> + when { + !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) + prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) + } + } + } + + launch { viewModel.endButton.collect { buttonModel -> updateButton( view = endButton, viewModel = buttonModel, falsingManager = falsingManager, messageDisplayer = messageDisplayer, + vibratorHelper = vibratorHelper, ) } } launch { + viewModel.endButton + .map { it.isActivated } + .pairwise() + .collect { (prev, next) -> + when { + !prev && next -> vibratorHelper?.vibrate(Vibrations.Activated) + prev && !next -> vibratorHelper?.vibrate(Vibrations.Deactivated) + } + } + } + + launch { viewModel.isOverlayContainerVisible.collect { isVisible -> overlayContainer.visibility = if (isVisible) { @@ -239,6 +270,7 @@ object KeyguardBottomAreaViewBinder { viewModel: KeyguardQuickAffordanceViewModel, falsingManager: FalsingManager?, messageDisplayer: (Int) -> Unit, + vibratorHelper: VibratorHelper?, ) { if (!viewModel.isVisible) { view.isVisible = false @@ -312,7 +344,9 @@ object KeyguardBottomAreaViewBinder { view.isClickable = viewModel.isClickable if (viewModel.isClickable) { if (viewModel.useLongPress) { - view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer)) + view.setOnTouchListener( + OnTouchListener(view, viewModel, messageDisplayer, vibratorHelper) + ) } else { view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) } @@ -328,6 +362,7 @@ object KeyguardBottomAreaViewBinder { private val view: View, private val viewModel: KeyguardQuickAffordanceViewModel, private val messageDisplayer: (Int) -> Unit, + private val vibratorHelper: VibratorHelper?, ) : View.OnTouchListener { private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() @@ -376,25 +411,38 @@ object KeyguardBottomAreaViewBinder { true } MotionEvent.ACTION_UP -> { - if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { - messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short) - val shakeAnimator = - ObjectAnimator.ofFloat( - view, - "translationX", - 0f, - view.context.resources - .getDimensionPixelSize( - R.dimen.keyguard_affordance_shake_amplitude + cancel( + onAnimationEnd = + if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { + Runnable { + messageDisplayer.invoke( + R.string.keyguard_affordance_press_too_short ) - .toFloat(), - 0f, - ) - shakeAnimator.duration = 300 - shakeAnimator.interpolator = CycleInterpolator(5f) - shakeAnimator.start() - } - cancel() + val amplitude = + view.context.resources + .getDimensionPixelSize( + R.dimen.keyguard_affordance_shake_amplitude + ) + .toFloat() + val shakeAnimator = + ObjectAnimator.ofFloat( + view, + "translationX", + -amplitude / 2, + amplitude / 2, + ) + shakeAnimator.duration = + ShakeAnimationDuration.inWholeMilliseconds + shakeAnimator.interpolator = + CycleInterpolator(ShakeAnimationCycles) + shakeAnimator.start() + + vibratorHelper?.vibrate(Vibrations.Shake) + } + } else { + null + } + ) true } MotionEvent.ACTION_CANCEL -> { @@ -405,11 +453,11 @@ object KeyguardBottomAreaViewBinder { } } - private fun cancel() { + private fun cancel(onAnimationEnd: Runnable? = null) { downTimestamp = 0L longPressAnimator?.cancel() longPressAnimator = null - view.animate().scaleX(1f).scaleY(1f) + view.animate().scaleX(1f).scaleY(1f).withEndAction(onAnimationEnd) } companion object { @@ -461,4 +509,58 @@ object KeyguardBottomAreaViewBinder { val indicationTextSizePx: Int, val buttonSizePx: Size, ) + + private val ShakeAnimationDuration = 300.milliseconds + private val ShakeAnimationCycles = 5f + + object Vibrations { + + private const val SmallVibrationScale = 0.3f + private const val BigVibrationScale = 0.6f + + val Shake = + VibrationEffect.startComposition() + .apply { + val vibrationDelayMs = + (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2)) + .toInt() + val vibrationCount = ShakeAnimationCycles.toInt() * 2 + repeat(vibrationCount) { + addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + SmallVibrationScale, + vibrationDelayMs, + ) + } + } + .compose() + + val Activated = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + BigVibrationScale, + 0, + ) + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, + 0.1f, + 0, + ) + .compose() + + val Deactivated = + VibrationEffect.startComposition() + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_TICK, + BigVibrationScale, + 0, + ) + .addPrimitive( + VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, + 0.1f, + 0, + ) + .compose() + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt index 402fac1c8fb4..e164f5d58b07 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AnimationParams import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt new file mode 100644 index 000000000000..e804562bc85f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to + * consume. + */ +@SysUISingleton +class OccludedToLockscreenTransitionViewModel +@Inject +constructor( + private val interactor: KeyguardTransitionInteractor, +) { + /** Lockscreen views y-translation */ + fun lockscreenTranslationY(translatePx: Int): Flow<Float> { + return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> + -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) + } + } + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA) + + private fun flowForAnimation(params: AnimationParams): Flow<Float> { + return interactor.transitionStepAnimation( + interactor.occludedToLockscreenTransition, + params, + totalDuration = TO_LOCKSCREEN_DURATION + ) + } + + companion object { + @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds + val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION) + val LOCKSCREEN_ALPHA = + AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java index 7cc95a158a14..fba5f63ea9c7 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java @@ -27,11 +27,15 @@ import android.view.ViewGroup; import androidx.activity.ComponentActivity; import androidx.lifecycle.ViewModelProvider; +import com.android.systemui.compose.ComposeFacade; import com.android.systemui.people.ui.view.PeopleViewBinder; import com.android.systemui.people.ui.viewmodel.PeopleViewModel; import javax.inject.Inject; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; + /** People Tile Widget configuration activity that shows the user their conversation tiles. */ public class PeopleSpaceActivity extends ComponentActivity { @@ -58,13 +62,18 @@ public class PeopleSpaceActivity extends ComponentActivity { int widgetId = getIntent().getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID); viewModel.onWidgetIdChanged(widgetId); - ViewGroup view = PeopleViewBinder.create(this); - PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, - (result) -> { - finishActivity(result); - return null; - }); - setContentView(view); + Function1<PeopleViewModel.Result, Unit> onResult = (result) -> { + finishActivity(result); + return null; + }; + + if (ComposeFacade.INSTANCE.isComposeAvailable()) { + ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult); + } else { + ViewGroup view = PeopleViewBinder.create(this); + PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult); + setContentView(view); + } } private void finishActivity(PeopleViewModel.Result result) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index a6447a5bf500..5716a1d7260c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -448,6 +448,10 @@ public class ScreenshotController { // Any cleanup needed when the service is being destroyed. void onDestroy() { + if (mSaveInBgTask != null) { + // just log success/failure for the pre-existing screenshot + mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); + } removeWindow(); releaseMediaPlayer(); releaseContext(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt index ba779c6233fb..639172f9e37a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt @@ -19,6 +19,9 @@ package com.android.systemui.shade import android.content.Context import android.view.ViewGroup import com.android.systemui.R +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState.SHADE +import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START @@ -30,17 +33,24 @@ import javax.inject.Inject @SysUIUnfoldScope class NotificationPanelUnfoldAnimationController @Inject -constructor(private val context: Context, progressProvider: NaturalRotationUnfoldProgressProvider) { +constructor( + private val context: Context, + statusBarStateController: StatusBarStateController, + progressProvider: NaturalRotationUnfoldProgressProvider, +) { + + private val filterShade: () -> Boolean = { statusBarStateController.getState() == SHADE || + statusBarStateController.getState() == SHADE_LOCKED } private val translateAnimator by lazy { UnfoldConstantTranslateAnimator( viewsIdToTranslate = setOf( - ViewIdToTranslate(R.id.quick_settings_panel, START), - ViewIdToTranslate(R.id.notification_stack_scroller, END), - ViewIdToTranslate(R.id.rightLayout, END), - ViewIdToTranslate(R.id.clock, START), - ViewIdToTranslate(R.id.date, START)), + ViewIdToTranslate(R.id.quick_settings_panel, START, filterShade), + ViewIdToTranslate(R.id.notification_stack_scroller, END, filterShade), + ViewIdToTranslate(R.id.rightLayout, END, filterShade), + ViewIdToTranslate(R.id.clock, START, filterShade), + ViewIdToTranslate(R.id.date, START, filterShade)), progressProvider = progressProvider) } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 31543dfcb540..d711d1512232 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -145,6 +145,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; +import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; @@ -239,6 +240,8 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Provider; +import kotlinx.coroutines.CoroutineDispatcher; + @CentralSurfacesComponent.CentralSurfacesScope public final class NotificationPanelViewController implements Dumpable { @@ -685,10 +688,13 @@ public final class NotificationPanelViewController implements Dumpable { private boolean mIgnoreXTouchSlop; private boolean mExpandLatencyTracking; private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; + private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; private KeyguardTransitionInteractor mKeyguardTransitionInteractor; - private boolean mIsDreamToLockscreenTransitionRunning = false; + private CoroutineDispatcher mMainDispatcher; + private boolean mIsToLockscreenTransitionRunning = false; private int mDreamingToLockscreenTransitionTranslationY; + private int mOccludedToLockscreenTransitionTranslationY; private boolean mUnocclusionTransitionFlagEnabled = false; private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */, @@ -707,7 +713,13 @@ public final class NotificationPanelViewController implements Dumpable { private final Consumer<TransitionStep> mDreamingToLockscreenTransition = (TransitionStep step) -> { - mIsDreamToLockscreenTransitionRunning = + mIsToLockscreenTransitionRunning = + step.getTransitionState() == TransitionState.RUNNING; + }; + + private final Consumer<TransitionStep> mOccludedToLockscreenTransition = + (TransitionStep step) -> { + mIsToLockscreenTransitionRunning = step.getTransitionState() == TransitionState.RUNNING; }; @@ -781,6 +793,8 @@ public final class NotificationPanelViewController implements Dumpable { KeyguardBottomAreaInteractor keyguardBottomAreaInteractor, AlternateBouncerInteractor alternateBouncerInteractor, DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel, + OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel, + @Main CoroutineDispatcher mainDispatcher, KeyguardTransitionInteractor keyguardTransitionInteractor, DumpManager dumpManager) { keyguardStateController.addCallback(new KeyguardStateController.Callback() { @@ -798,6 +812,7 @@ public final class NotificationPanelViewController implements Dumpable { mShadeHeightLogger = shadeHeightLogger; mGutsManager = gutsManager; mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel; + mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override @@ -876,6 +891,7 @@ public final class NotificationPanelViewController implements Dumpable { mFalsingCollector = falsingCollector; mPowerManager = powerManager; mWakeUpCoordinator = coordinator; + mMainDispatcher = mainDispatcher; mAccessibilityManager = accessibilityManager; mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle()); setPanelAlpha(255, false /* animate */); @@ -1101,15 +1117,27 @@ public final class NotificationPanelViewController implements Dumpable { controller.setup(mNotificationContainerParent)); if (mUnocclusionTransitionFlagEnabled) { + // Dreaming->Lockscreen + collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), + mDreamingToLockscreenTransition, mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(), - dreamingToLockscreenTransitionAlpha(mNotificationStackScrollLayoutController)); - + toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY( mDreamingToLockscreenTransitionTranslationY), - dreamingToLockscreenTransitionY(mNotificationStackScrollLayoutController)); + toLockscreenTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); - collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), - mDreamingToLockscreenTransition); + // Occluded->Lockscreen + collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(), + mOccludedToLockscreenTransition, mMainDispatcher); + collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(), + toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), + mMainDispatcher); + collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY( + mOccludedToLockscreenTransitionTranslationY), + toLockscreenTransitionY(mNotificationStackScrollLayoutController), + mMainDispatcher); } } @@ -1147,6 +1175,8 @@ public final class NotificationPanelViewController implements Dumpable { R.dimen.split_shade_scrim_transition_distance); mDreamingToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize( R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y); + mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize( + R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y); } private void updateViewControllers(KeyguardStatusView keyguardStatusView, @@ -1374,8 +1404,8 @@ public final class NotificationPanelViewController implements Dumpable { mFalsingManager, mLockIconViewController, stringResourceId -> - mKeyguardIndicationController.showTransientIndication(stringResourceId) - ); + mKeyguardIndicationController.showTransientIndication(stringResourceId), + mVibratorHelper); } @VisibleForTesting @@ -1810,7 +1840,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateClock() { - if (mIsDreamToLockscreenTransitionRunning) { + if (mIsToLockscreenTransitionRunning) { return; } float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha; @@ -2701,7 +2731,7 @@ public final class NotificationPanelViewController implements Dumpable { } else if (statusBarState == KEYGUARD || statusBarState == StatusBarState.SHADE_LOCKED) { mKeyguardBottomArea.setVisibility(View.VISIBLE); - if (!mIsDreamToLockscreenTransitionRunning) { + if (!mIsToLockscreenTransitionRunning) { mKeyguardBottomArea.setAlpha(1f); } } else { @@ -3570,7 +3600,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateNotificationTranslucency() { - if (mIsDreamToLockscreenTransitionRunning) { + if (mIsToLockscreenTransitionRunning) { return; } float alpha = 1f; @@ -3628,7 +3658,7 @@ public final class NotificationPanelViewController implements Dumpable { } private void updateKeyguardBottomAreaAlpha() { - if (mIsDreamToLockscreenTransitionRunning) { + if (mIsToLockscreenTransitionRunning) { return; } // There are two possible panel expansion behaviors: @@ -5860,7 +5890,7 @@ public final class NotificationPanelViewController implements Dumpable { mCurrentPanelState = state; } - private Consumer<Float> dreamingToLockscreenTransitionAlpha( + private Consumer<Float> toLockscreenTransitionAlpha( NotificationStackScrollLayoutController stackScroller) { return (Float alpha) -> { mKeyguardStatusViewController.setAlpha(alpha); @@ -5878,7 +5908,7 @@ public final class NotificationPanelViewController implements Dumpable { }; } - private Consumer<Float> dreamingToLockscreenTransitionY( + private Consumer<Float> toLockscreenTransitionY( NotificationStackScrollLayoutController stackScroller) { return (Float translationY) -> { mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 3a011c5daa5f..64b6e6173ab4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -19,6 +19,7 @@ package com.android.systemui.shade; import android.app.StatusBarManager; import android.media.AudioManager; import android.media.session.MediaSessionLegacyHelper; +import android.os.PowerManager; import android.os.SystemClock; import android.util.Log; import android.view.GestureDetector; @@ -242,7 +243,9 @@ public class NotificationShadeWindowViewController { () -> mService.wakeUpIfDozing( SystemClock.uptimeMillis(), mView, - "LOCK_ICON_TOUCH")); + "LOCK_ICON_TOUCH", + PowerManager.WAKE_REASON_GESTURE) + ); // In case we start outside of the view bounds (below the status bar), we need to // dispatch diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt index bf622c941abb..db700650e46c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.hardware.display.AmbientDisplayConfiguration +import android.os.PowerManager import android.os.SystemClock import android.os.UserHandle import android.provider.Settings @@ -89,7 +90,8 @@ class PulsingGestureListener @Inject constructor( centralSurfaces.wakeUpIfDozing( SystemClock.uptimeMillis(), notificationShadeWindowView, - "PULSING_SINGLE_TAP" + "PULSING_SINGLE_TAP", + PowerManager.WAKE_REASON_TAP ) } return true @@ -114,7 +116,9 @@ class PulsingGestureListener @Inject constructor( centralSurfaces.wakeUpIfDozing( SystemClock.uptimeMillis(), notificationShadeWindowView, - "PULSING_DOUBLE_TAP") + "PULSING_DOUBLE_TAP", + PowerManager.WAKE_REASON_TAP + ) return true } return false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index b8302d706e8d..905cc3fc71e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration +import android.os.PowerManager import android.os.SystemClock import android.util.IndentingPrintWriter import android.util.MathUtils @@ -272,7 +273,12 @@ class LockscreenShadeTransitionController @Inject constructor( // Bind the click listener of the shelf to go to the full shade notificationShelfController.setOnClickListener { if (statusBarStateController.state == StatusBarState.KEYGUARD) { - centralSurfaces.wakeUpIfDozing(SystemClock.uptimeMillis(), it, "SHADE_CLICK") + centralSurfaces.wakeUpIfDozing( + SystemClock.uptimeMillis(), + it, + "SHADE_CLICK", + PowerManager.WAKE_REASON_GESTURE, + ) goToLockedShade(it) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java index 56b689efaa79..7d0ac1874056 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java @@ -290,7 +290,8 @@ public class NotificationListener extends NotificationListenerWithPlugins implem false, null, 0, - false + false, + 0 ); } return ranking; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 351603700f98..8f9365cd4dc4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -24,6 +24,7 @@ import android.app.RemoteInput; import android.content.Context; import android.content.Intent; import android.content.pm.UserInfo; +import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; @@ -64,6 +65,8 @@ import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.ListenerSet; +import dagger.Lazy; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -71,8 +74,6 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; -import dagger.Lazy; - /** * Class for handling remote input state over a set of notifications. This class handles things * like keeping notifications temporarily that were cancelled as a response to a remote input @@ -120,7 +121,8 @@ public class NotificationRemoteInputManager implements Dumpable { View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) { mCentralSurfacesOptionalLazy.get().ifPresent( centralSurfaces -> centralSurfaces.wakeUpIfDozing( - SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK")); + SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK", + PowerManager.WAKE_REASON_GESTURE)); final NotificationEntry entry = getNotificationForParent(view.getParent()); mLogger.logInitialClick(entry, pendingIntent); @@ -464,9 +466,6 @@ public class NotificationRemoteInputManager implements Dumpable { riv.getController().setRemoteInputs(inputs); riv.getController().setEditedSuggestionInfo(editedSuggestionInfo); ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null; - if (parent != null) { - riv.setDefocusTargetHeight(parent.getHeight()); - } riv.focusAnimated(parent); if (userMessageContent != null) { riv.setEditTextContent(userMessageContent); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt index c630feba1dcb..976924a2159d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/PulseExpansionHandler.kt @@ -22,7 +22,6 @@ import android.animation.ValueAnimator import android.content.Context import android.content.res.Configuration import android.os.PowerManager -import android.os.PowerManager.WAKE_REASON_GESTURE import android.os.SystemClock import android.util.IndentingPrintWriter import android.view.MotionEvent @@ -249,7 +248,7 @@ constructor( } if (statusBarStateController.isDozing) { wakeUpCoordinator.willWakeUp = true - mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE, + mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:PULSEDRAG") } lockscreenShadeTransitionController.goToLockedShade(startingChild, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index b9074f0b9e2a..6e74542691a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -1301,7 +1301,7 @@ public class NetworkControllerImpl extends BroadcastReceiver } } String wifi = args.getString("wifi"); - if (wifi != null) { + if (wifi != null && !mStatusBarPipelineFlags.runNewWifiIconBackend()) { boolean show = wifi.equals("show"); String level = args.getString("level"); if (level != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java index c3ce593b54fd..705cf92ee869 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationClicker.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification; import android.app.Notification; +import android.os.PowerManager; import android.os.SystemClock; import android.service.notification.StatusBarNotification; import android.util.Log; @@ -70,7 +71,8 @@ public final class NotificationClicker implements View.OnClickListener { } mCentralSurfacesOptional.ifPresent(centralSurfaces -> centralSurfaces.wakeUpIfDozing( - SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK")); + SystemClock.uptimeMillis(), v, "NOTIFICATION_CLICK", + PowerManager.WAKE_REASON_GESTURE)); final ExpandableNotificationRow row = (ExpandableNotificationRow) v; final NotificationEntry entry = row.getEntry(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt new file mode 100644 index 000000000000..1a3927ba9b06 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiChromeViewBinder.kt @@ -0,0 +1,99 @@ +package com.android.systemui.statusbar.notification.fsi + +import android.content.Context +import android.view.LayoutInflater +import android.view.WindowManager +import com.android.systemui.CoreStartable +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.notification.fsi.FsiDebug.Companion.log +import com.android.systemui.statusbar.phone.CentralSurfaces +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.util.concurrent.Executor +import javax.inject.Inject + +@SysUISingleton +class FsiChromeViewBinder +@Inject +constructor( + val context: Context, + val windowManager: WindowManager, + val viewModelFactory: FsiChromeViewModelFactory, + val layoutInflater: LayoutInflater, + val centralSurfaces: CentralSurfaces, + @Main val mainExecutor: Executor, + @Application val scope: CoroutineScope, +) : CoreStartable { + + companion object { + private const val classTag = "FsiChromeViewBinder" + } + + private val fsiChromeView = + layoutInflater.inflate(R.layout.fsi_chrome_view, null /* root */, false /* attachToRoot */) + as FsiChromeView + + var addedToWindowManager = false + var cornerRadius: Int = context.resources.getDimensionPixelSize( + R.dimen.notification_corner_radius) + + override fun start() { + val methodTag = "start" + log("$classTag $methodTag ") + + scope.launch { + log("$classTag $methodTag launch ") + viewModelFactory.viewModelFlow.collect { vm -> updateForViewModel(vm) } + } + } + + private fun updateForViewModel(vm: FsiChromeViewModel?) { + val methodTag = "updateForViewModel" + + if (vm == null) { + log("$classTag $methodTag viewModel is null, removing from window manager") + + if (addedToWindowManager) { + windowManager.removeView(fsiChromeView) + addedToWindowManager = false + } + return + } + + bindViewModel(vm, windowManager) + + if (addedToWindowManager) { + log("$classTag $methodTag already addedToWindowManager") + } else { + windowManager.addView(fsiChromeView, FsiTaskViewConfig.getWmLayoutParams("PackageName")) + addedToWindowManager = true + } + } + + private fun bindViewModel( + vm: FsiChromeViewModel, + windowManager: WindowManager, + ) { + log("$classTag bindViewModel") + + fsiChromeView.appIconImageView.setImageDrawable(vm.appIcon) + fsiChromeView.appNameTextView.text = vm.appName + + fsiChromeView.dismissButton.setOnClickListener { vm.onDismiss() } + fsiChromeView.fullscreenButton.setOnClickListener { vm.onFullscreen() } + + vm.taskView.cornerRadius = cornerRadius.toFloat() + vm.taskView.startActivity( + vm.fsi, + FsiTaskViewConfig.getFillInIntent(), + FsiTaskViewConfig.getActivityOptions(context, windowManager), + FsiTaskViewConfig.getLaunchBounds(windowManager) + ) + + log("$classTag bindViewModel started taskview activity") + fsiChromeView.addView(vm.taskView) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt new file mode 100644 index 000000000000..034ab56d5a65 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/fsi/FsiTaskViewConfig.kt @@ -0,0 +1,75 @@ +package com.android.systemui.statusbar.notification.fsi + +import android.app.ActivityOptions +import android.content.Context +import android.content.Intent +import android.graphics.PixelFormat +import android.graphics.Rect +import android.os.Binder +import android.view.ViewGroup +import android.view.WindowManager + +/** + * Config for adding the FsiChromeView window to WindowManager and starting the FSI activity. + */ +class FsiTaskViewConfig { + + companion object { + + private const val classTag = "FsiTaskViewConfig" + + fun getWmLayoutParams(packageName: String): WindowManager.LayoutParams { + val params: WindowManager.LayoutParams? + params = + WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or + WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED or + WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER, + PixelFormat.TRANSLUCENT + ) + params.setTrustedOverlay() + params.fitInsetsTypes = 0 + params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + params.token = Binder() + params.packageName = packageName + params.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS + params.privateFlags = + params.privateFlags or WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS + return params + } + + fun getFillInIntent(): Intent { + val fillInIntent = Intent() + fillInIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) + fillInIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + // FLAG_ACTIVITY_NEW_TASK is auto-applied because + // we're starting the FSI activity from a non-Activity context + return fillInIntent + } + + fun getLaunchBounds(windowManager: WindowManager): Rect { + // TODO(b/243421660) check this works for non-resizeable activity + return Rect() + } + + fun getActivityOptions(context: Context, windowManager: WindowManager): ActivityOptions { + // Custom options so there is no activity transition animation + val options = + ActivityOptions.makeCustomAnimation(context, 0 /* enterResId */, 0 /* exitResId */) + + options.taskAlwaysOnTop = true + + options.pendingIntentLaunchFlags = + Intent.FLAG_ACTIVITY_NEW_DOCUMENT or + Intent.FLAG_ACTIVITY_MULTIPLE_TASK or + Intent.FLAG_ACTIVITY_NEW_TASK + + options.launchBounds = getLaunchBounds(windowManager) + return options + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index d240d5a69fe6..9a8c5d709f3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -420,7 +420,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Runnable wakeUp = ()-> { if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); - mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, + mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, "android.policy:BIOMETRIC"); } Trace.beginSection("release wake-and-unlock"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index c7c644179e5b..cf2f7742dc59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; +import android.os.PowerManager; import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.view.KeyEvent; @@ -205,7 +206,10 @@ public interface CentralSurfaces extends Dumpable, ActivityStarter, LifecycleOwn @Override Lifecycle getLifecycle(); - void wakeUpIfDozing(long time, View where, String why); + /** + * Wakes up the device if the device was dozing. + */ + void wakeUpIfDozing(long time, View where, String why, @PowerManager.WakeReason int wakeReason); NotificationShadeWindowView getNotificationShadeWindowView(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index c1ed10c2afcc..22ebcab777aa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -887,8 +887,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mKeyguardIndicationController.init(); mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener); - mStatusBarStateController.addCallback(mStateListener, - SysuiStatusBarStateController.RANK_STATUS_BAR); mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); @@ -1507,10 +1505,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { * @param why the reason for the wake up */ @Override - public void wakeUpIfDozing(long time, View where, String why) { + public void wakeUpIfDozing(long time, View where, String why, + @PowerManager.WakeReason int wakeReason) { if (mDozing && mScreenOffAnimationController.allowWakeUpIfDozing()) { mPowerManager.wakeUp( - time, PowerManager.WAKE_REASON_GESTURE, "com.android.systemui:" + why); + time, wakeReason, "com.android.systemui:" + why); mWakeUpComingFromTouch = true; mFalsingCollector.onScreenOnFromTouch(); } @@ -1587,6 +1586,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected void startKeyguard() { Trace.beginSection("CentralSurfaces#startKeyguard"); + mStatusBarStateController.addCallback(mStateListener, + SysuiStatusBarStateController.RANK_STATUS_BAR); mBiometricUnlockController = mBiometricUnlockControllerLazy.get(); mBiometricUnlockController.addBiometricModeListener( new BiometricUnlockController.BiometricModeListener() { @@ -3364,7 +3365,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mStatusBarHideIconsForBouncerManager.setBouncerShowingAndTriggerUpdate(bouncerShowing); mCommandQueue.recomputeDisableFlags(mDisplayId, true /* animate */); if (mBouncerShowing) { - wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE"); + wakeUpIfDozing(SystemClock.uptimeMillis(), null, "BOUNCER_VISIBLE", + PowerManager.WAKE_REASON_GESTURE); } updateScrimController(); if (!mBouncerShowing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index c7be2193e9b5..c72eb054c62c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -42,6 +42,8 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconStat import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; +import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; import java.util.ArrayList; import java.util.List; @@ -56,14 +58,17 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da private final int mIconSize; private StatusBarWifiView mWifiView; + private ModernStatusBarWifiView mModernWifiView; private boolean mDemoMode; private int mColor; private final MobileIconsViewModel mMobileIconsViewModel; + private final StatusBarLocation mLocation; public DemoStatusIcons( LinearLayout statusIcons, MobileIconsViewModel mobileIconsViewModel, + StatusBarLocation location, int iconSize ) { super(statusIcons.getContext()); @@ -71,6 +76,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da mIconSize = iconSize; mColor = DarkIconDispatcher.DEFAULT_ICON_TINT; mMobileIconsViewModel = mobileIconsViewModel; + mLocation = location; if (statusIcons instanceof StatusIconContainer) { setShouldRestrictIcons(((StatusIconContainer) statusIcons).isRestrictingIcons()); @@ -233,14 +239,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da public void addDemoWifiView(WifiIconState state) { Log.d(TAG, "addDemoWifiView: "); - // TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}. StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot); int viewIndex = getChildCount(); // If we have mobile views, put wifi before them for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); - if (child instanceof StatusBarMobileView) { + if (child instanceof StatusBarMobileView + || child instanceof ModernStatusBarMobileView) { viewIndex = i; break; } @@ -287,7 +293,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind( mobileContext, "mobile", - mMobileIconsViewModel.viewModelForSub(subId) + mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); // mobile always goes at the end @@ -296,6 +302,30 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da } /** + * Add a {@link ModernStatusBarWifiView} + */ + public void addModernWifiView(LocationBasedWifiViewModel viewModel) { + Log.d(TAG, "addModernDemoWifiView: "); + ModernStatusBarWifiView view = ModernStatusBarWifiView + .constructAndBind(mContext, "wifi", viewModel); + + int viewIndex = getChildCount(); + // If we have mobile views, put wifi before them + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child instanceof StatusBarMobileView + || child instanceof ModernStatusBarMobileView) { + viewIndex = i; + break; + } + } + + mModernWifiView = view; + mModernWifiView.setStaticDrawableColor(mColor); + addView(view, viewIndex, createLayoutParams()); + } + + /** * Apply an update to a mobile icon view for the given {@link MobileIconState}. For * compatibility with {@link MobileContextProvider}, we have to recreate the view every time we * update it, since the context (and thus the {@link Configuration}) may have changed @@ -317,8 +347,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da public void onRemoveIcon(StatusIconDisplayable view) { if (view.getSlot().equals("wifi")) { - removeView(mWifiView); - mWifiView = null; + if (view instanceof StatusBarWifiView) { + removeView(mWifiView); + mWifiView = null; + } else if (view instanceof ModernStatusBarWifiView) { + Log.d(TAG, "onRemoveIcon: removing modern wifi view"); + removeView(mModernWifiView); + mModernWifiView = null; + } } else if (view instanceof StatusBarMobileView) { StatusBarMobileView mobileView = matchingMobileView(view); if (mobileView != null) { @@ -371,8 +407,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da if (mWifiView != null) { mWifiView.onDarkChanged(areas, darkIntensity, tint); } + if (mModernWifiView != null) { + mModernWifiView.onDarkChanged(areas, darkIntensity, tint); + } for (StatusBarMobileView view : mMobileViews) { view.onDarkChanged(areas, darkIntensity, tint); } + for (ModernStatusBarMobileView view : mModernMobileViews) { + view.onDarkChanged(areas, darkIntensity, tint); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt index 2ce116394236..e4227dce94e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder.bind import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.VibratorHelper /** * Renders the bottom area of the lock-screen. Concerned primarily with the quick affordance UI @@ -65,12 +66,14 @@ constructor( falsingManager: FalsingManager? = null, lockIconViewController: LockIconViewController? = null, messageDisplayer: MessageDisplayer? = null, + vibratorHelper: VibratorHelper? = null, ) { binding = bind( this, viewModel, falsingManager, + vibratorHelper, ) { messageDisplayer?.display(it) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index df3ab493a4da..1a14a0363763 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -359,6 +359,7 @@ public interface StatusBarIconController { // Whether or not these icons show up in dumpsys protected boolean mShouldLog = false; private StatusBarIconController mController; + private final StatusBarLocation mLocation; // Enables SystemUI demo mode to take effect in this group protected boolean mDemoable = true; @@ -381,11 +382,12 @@ public interface StatusBarIconController { mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); + mLocation = location; if (statusBarPipelineFlags.runNewMobileIconsBackend()) { // This starts the flow for the new pipeline, and will notify us of changes if // {@link StatusBarPipelineFlags#useNewMobileIcons} is also true. - mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel(); + mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); } else { mMobileIconsViewModel = null; @@ -394,7 +396,7 @@ public interface StatusBarIconController { if (statusBarPipelineFlags.runNewWifiIconBackend()) { // This starts the flow for the new pipeline, and will notify us of changes if // {@link StatusBarPipelineFlags#useNewWifiIcon} is also true. - mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location); + mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation); } else { mWifiViewModel = null; } @@ -495,6 +497,11 @@ public interface StatusBarIconController { ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot); mGroup.addView(view, index, onCreateLayoutParams()); + + if (mIsInDemoMode) { + mDemoStatusIcons.addModernWifiView(mWifiViewModel); + } + return view; } @@ -569,7 +576,7 @@ public interface StatusBarIconController { .constructAndBind( mobileContext, slot, - mMobileIconsViewModel.viewModelForSub(subId) + mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); } @@ -686,6 +693,9 @@ public interface StatusBarIconController { mIsInDemoMode = true; if (mDemoStatusIcons == null) { mDemoStatusIcons = createDemoStatusIcons(); + if (mStatusBarPipelineFlags.useNewWifiIcon()) { + mDemoStatusIcons.addModernWifiView(mWifiViewModel); + } } mDemoStatusIcons.onDemoModeStarted(); } @@ -705,7 +715,12 @@ public interface StatusBarIconController { } protected DemoStatusIcons createDemoStatusIcons() { - return new DemoStatusIcons((LinearLayout) mGroup, mMobileIconsViewModel, mIconSize); + return new DemoStatusIcons( + (LinearLayout) mGroup, + mMobileIconsViewModel, + mLocation, + mIconSize + ); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index a1e0c5067ef3..da1c361bced7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -20,6 +20,7 @@ import static com.android.systemui.statusbar.phone.CentralSurfaces.MULTIUSER_DEB import android.app.KeyguardManager; import android.content.Context; +import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; @@ -270,7 +271,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, boolean nowExpanded) { mHeadsUpManager.setExpanded(clickedEntry, nowExpanded); mCentralSurfaces.wakeUpIfDozing( - SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK"); + SystemClock.uptimeMillis(), clickedView, "NOTIFICATION_CLICK", + PowerManager.WAKE_REASON_GESTURE); if (nowExpanded) { if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { mShadeTransitionController.goToLockedShade(clickedEntry.getRow()); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index c350c78913d3..0d01715715c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -36,7 +36,7 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyIm import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository -import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import dagger.Binds @@ -56,7 +56,7 @@ abstract class StatusBarPipelineModule { @Binds abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository - @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository + @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository @Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt index 6c37f94007cb..1aa954ff48cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModel.kt @@ -26,6 +26,8 @@ import android.telephony.TelephonyCallback.ServiceStateListener import android.telephony.TelephonyCallback.SignalStrengthsListener import android.telephony.TelephonyDisplayInfo import android.telephony.TelephonyManager +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -79,4 +81,72 @@ data class MobileConnectionModel( * [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon */ val resolvedNetworkType: ResolvedNetworkType = ResolvedNetworkType.UnknownNetworkType, -) +) : Diffable<MobileConnectionModel> { + override fun logDiffs(prevVal: MobileConnectionModel, row: TableRowLogger) { + if (prevVal.dataConnectionState != dataConnectionState) { + row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString()) + } + + if (prevVal.isEmergencyOnly != isEmergencyOnly) { + row.logChange(COL_EMERGENCY, isEmergencyOnly) + } + + if (prevVal.isRoaming != isRoaming) { + row.logChange(COL_ROAMING, isRoaming) + } + + if (prevVal.operatorAlphaShort != operatorAlphaShort) { + row.logChange(COL_OPERATOR, operatorAlphaShort) + } + + if (prevVal.isGsm != isGsm) { + row.logChange(COL_IS_GSM, isGsm) + } + + if (prevVal.cdmaLevel != cdmaLevel) { + row.logChange(COL_CDMA_LEVEL, cdmaLevel) + } + + if (prevVal.primaryLevel != primaryLevel) { + row.logChange(COL_PRIMARY_LEVEL, primaryLevel) + } + + if (prevVal.dataActivityDirection != dataActivityDirection) { + row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString()) + } + + if (prevVal.carrierNetworkChangeActive != carrierNetworkChangeActive) { + row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive) + } + + if (prevVal.resolvedNetworkType != resolvedNetworkType) { + row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString()) + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_CONNECTION_STATE, dataConnectionState.toString()) + row.logChange(COL_EMERGENCY, isEmergencyOnly) + row.logChange(COL_ROAMING, isRoaming) + row.logChange(COL_OPERATOR, operatorAlphaShort) + row.logChange(COL_IS_GSM, isGsm) + row.logChange(COL_CDMA_LEVEL, cdmaLevel) + row.logChange(COL_PRIMARY_LEVEL, primaryLevel) + row.logChange(COL_ACTIVITY_DIRECTION, dataActivityDirection.toString()) + row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChangeActive) + row.logChange(COL_RESOLVED_NETWORK_TYPE, resolvedNetworkType.toString()) + } + + companion object { + const val COL_EMERGENCY = "EmergencyOnly" + const val COL_ROAMING = "Roaming" + const val COL_OPERATOR = "OperatorName" + const val COL_IS_GSM = "IsGsm" + const val COL_CDMA_LEVEL = "CdmaLevel" + const val COL_PRIMARY_LEVEL = "PrimaryLevel" + const val COL_CONNECTION_STATE = "ConnectionState" + const val COL_ACTIVITY_DIRECTION = "DataActivity" + const val COL_CARRIER_NETWORK_CHANGE = "CarrierNetworkChangeActive" + const val COL_RESOLVED_NETWORK_TYPE = "NetworkType" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt index a8cf35ad3029..c50d82a66c76 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt @@ -21,22 +21,48 @@ import android.telephony.TelephonyManager.EXTRA_DATA_SPN import android.telephony.TelephonyManager.EXTRA_PLMN import android.telephony.TelephonyManager.EXTRA_SHOW_PLMN import android.telephony.TelephonyManager.EXTRA_SHOW_SPN +import com.android.systemui.log.table.Diffable +import com.android.systemui.log.table.TableRowLogger /** * Encapsulates the data needed to show a network name for a mobile network. The data is parsed from * the intent sent by [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]. */ -sealed interface NetworkNameModel { +sealed interface NetworkNameModel : Diffable<NetworkNameModel> { val name: String /** The default name is read from [com.android.internal.R.string.lockscreen_carrier_default] */ - data class Default(override val name: String) : NetworkNameModel + data class Default(override val name: String) : NetworkNameModel { + override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) { + if (prevVal !is Default || prevVal.name != name) { + row.logChange(COL_NETWORK_NAME, "Default($name)") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_NAME, "Default($name)") + } + } /** * This name has been derived from telephony intents. see * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED] */ - data class Derived(override val name: String) : NetworkNameModel + data class Derived(override val name: String) : NetworkNameModel { + override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) { + if (prevVal !is Derived || prevVal.name != name) { + row.logChange(COL_NETWORK_NAME, "Derived($name)") + } + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_NAME, "Derived($name)") + } + } + + companion object { + const val COL_NETWORK_NAME = "networkName" + } } fun Intent.toNetworkNameModel(separator: String): NetworkNameModel? { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 2fd415e6777f..40e9ba1a46c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -20,6 +20,7 @@ import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import kotlinx.coroutines.flow.Flow @@ -39,6 +40,13 @@ import kotlinx.coroutines.flow.StateFlow interface MobileConnectionRepository { /** The subscriptionId that this connection represents */ val subId: Int + + /** + * The table log buffer created for this connection. Will have the name "MobileConnectionLog + * [subId]" + */ + val tableLogBuffer: TableLogBuffer + /** * A flow that aggregates all necessary callbacks from [TelephonyCallback] into a single * listener + model. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index d3ee85f19347..b252de8dd389 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -24,6 +24,8 @@ import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel @@ -60,6 +62,7 @@ constructor( private val dataSource: DemoModeMobileConnectionDataSource, @Application private val scope: CoroutineScope, context: Context, + private val logFactory: TableLogBufferFactory, ) : MobileConnectionsRepository { private var demoCommandJob: Job? = null @@ -149,7 +152,16 @@ constructor( override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { return connectionRepoCache[subId] - ?: DemoMobileConnectionRepository(subId).also { connectionRepoCache[subId] = it } + ?: createDemoMobileConnectionRepo(subId).also { connectionRepoCache[subId] = it } + } + + private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository { + val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100) + + return DemoMobileConnectionRepository( + subId, + tableLogBuffer, + ) } override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) @@ -260,7 +272,10 @@ constructor( } } -class DemoMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository { +class DemoMobileConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, +) : MobileConnectionRepository { override val connectionInfo = MutableStateFlow(MobileConnectionModel()) override val dataEnabled = MutableStateFlow(true) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt index a1ae8ed7329a..d4ddb856eecf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSource.kt @@ -24,10 +24,8 @@ import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK import com.android.systemui.demomode.DemoModeController import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel @@ -35,8 +33,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn @@ -52,27 +48,7 @@ constructor( demoModeController: DemoModeController, @Application scope: CoroutineScope, ) { - private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow { - val callback = - object : DemoMode { - override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK) - - override fun dispatchDemoCommand(command: String, args: Bundle) { - trySend(args) - } - - override fun onDemoModeFinished() { - // Handled elsewhere - } - - override fun onDemoModeStarted() { - // Handled elsewhere - } - } - - demoModeController.addCallback(callback) - awaitClose { demoModeController.removeCallback(callback) } - } + private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK) // If the args contains "mobile", then all of the args are relevant. It's just the way demo mode // commands work and it's a little silly diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 7e9a9cea9b95..0b9e1583898e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -36,6 +36,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType @@ -46,7 +49,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameMo import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject @@ -59,6 +61,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach @@ -79,6 +82,7 @@ class MobileConnectionRepositoryImpl( mobileMappingsProxy: MobileMappingsProxy, bgDispatcher: CoroutineDispatcher, logger: ConnectivityPipelineLogger, + mobileLogger: TableLogBuffer, scope: CoroutineScope, ) : MobileConnectionRepository { init { @@ -92,10 +96,11 @@ class MobileConnectionRepositoryImpl( private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + override val tableLogBuffer: TableLogBuffer = mobileLogger + override val connectionInfo: StateFlow<MobileConnectionModel> = run { var state = MobileConnectionModel() conflatedCallbackFlow { - // TODO (b/240569788): log all of these into the connectivity logger val callback = object : TelephonyCallback(), @@ -106,6 +111,7 @@ class MobileConnectionRepositoryImpl( TelephonyCallback.CarrierNetworkListener, TelephonyCallback.DisplayInfoListener { override fun onServiceStateChanged(serviceState: ServiceState) { + logger.logOnServiceStateChanged(serviceState, subId) state = state.copy( isEmergencyOnly = serviceState.isEmergencyOnly, @@ -116,6 +122,7 @@ class MobileConnectionRepositoryImpl( } override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { + logger.logOnSignalStrengthsChanged(signalStrength, subId) val cdmaLevel = signalStrength .getCellSignalStrengths(CellSignalStrengthCdma::class.java) @@ -142,12 +149,14 @@ class MobileConnectionRepositoryImpl( dataState: Int, networkType: Int ) { + logger.logOnDataConnectionStateChanged(dataState, networkType, subId) state = state.copy(dataConnectionState = dataState.toDataConnectionType()) trySend(state) } override fun onDataActivity(direction: Int) { + logger.logOnDataActivity(direction, subId) state = state.copy( dataActivityDirection = direction.toMobileDataActivityModel() @@ -156,6 +165,7 @@ class MobileConnectionRepositoryImpl( } override fun onCarrierNetworkChange(active: Boolean) { + logger.logOnCarrierNetworkChange(active, subId) state = state.copy(carrierNetworkChangeActive = active) trySend(state) } @@ -163,6 +173,7 @@ class MobileConnectionRepositoryImpl( override fun onDisplayInfoChanged( telephonyDisplayInfo: TelephonyDisplayInfo ) { + logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId) val networkType = if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) { @@ -193,7 +204,11 @@ class MobileConnectionRepositoryImpl( awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } .onEach { telephonyCallbackEvent.tryEmit(Unit) } - .logOutputChange(logger, "MobileSubscriptionModel") + .logDiffsForTable( + mobileLogger, + columnPrefix = "MobileConnection ($subId)", + initialValue = state, + ) .stateIn(scope, SharingStarted.WhileSubscribed(), state) } @@ -243,19 +258,43 @@ class MobileConnectionRepositoryImpl( intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName } } + .distinctUntilChanged() + .logDiffsForTable( + mobileLogger, + columnPrefix = "", + initialValue = defaultNetworkName, + ) .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName) - override val dataEnabled: StateFlow<Boolean> = + override val dataEnabled: StateFlow<Boolean> = run { + val initial = dataConnectionAllowed() telephonyPollingEvent .mapLatest { dataConnectionAllowed() } - .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed()) + .distinctUntilChanged() + .logDiffsForTable( + mobileLogger, + columnPrefix = "", + columnName = "dataEnabled", + initialValue = initial, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + } private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed - override val isDefaultDataSubscription: StateFlow<Boolean> = + override val isDefaultDataSubscription: StateFlow<Boolean> = run { + val initialValue = defaultDataSubId.value == subId defaultDataSubId .mapLatest { it == subId } - .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId) + .distinctUntilChanged() + .logDiffsForTable( + mobileLogger, + columnPrefix = "", + columnName = "isDefaultDataSub", + initialValue = initialValue, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue) + } class Factory @Inject @@ -266,6 +305,7 @@ class MobileConnectionRepositoryImpl( private val logger: ConnectivityPipelineLogger, private val globalSettings: GlobalSettings, private val mobileMappingsProxy: MobileMappingsProxy, + private val logFactory: TableLogBufferFactory, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { @@ -276,6 +316,8 @@ class MobileConnectionRepositoryImpl( defaultDataSubId: StateFlow<Int>, globalMobileDataSettingChangedEvent: Flow<Unit>, ): MobileConnectionRepository { + val mobileLogger = logFactory.create(tableBufferLogName(subId), 100) + return MobileConnectionRepositoryImpl( context, subId, @@ -289,8 +331,13 @@ class MobileConnectionRepositoryImpl( mobileMappingsProxy, bgDispatcher, logger, + mobileLogger, scope, ) } } + + companion object { + fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]" + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index a9b3d18774fd..d407abeb2315 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -51,6 +51,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConn import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -120,6 +121,7 @@ constructor( awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } } .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } } + .logInputChange(logger, "onSubscriptionsChanged") .onEach { infos -> dropUnusedReposFromCache(infos) } .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) @@ -136,6 +138,8 @@ constructor( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .distinctUntilChanged() + .logInputChange(logger, "onActiveDataSubscriptionIdChanged") .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = @@ -149,6 +153,7 @@ constructor( intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) } .distinctUntilChanged() + .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED") .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } .stateIn( scope, @@ -157,13 +162,15 @@ constructor( ) private val carrierConfigChangedEvent = - broadcastDispatcher.broadcastFlow( - IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) - ) + broadcastDispatcher + .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) + .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED") override val defaultDataSubRatConfig: StateFlow<Config> = merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) .mapLatest { Config.readConfig(context) } + .distinctUntilChanged() + .logInputChange(logger, "defaultDataSubRatConfig") .stateIn( scope, SharingStarted.WhileSubscribed(), @@ -171,10 +178,16 @@ constructor( ) override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> = - defaultDataSubRatConfig.map { mobileMappingsProxy.mapIconSets(it) } + defaultDataSubRatConfig + .map { mobileMappingsProxy.mapIconSets(it) } + .distinctUntilChanged() + .logInputChange(logger, "defaultMobileIconMapping") override val defaultMobileIconGroup: Flow<MobileIconGroup> = - defaultDataSubRatConfig.map { mobileMappingsProxy.getDefaultIcons(it) } + defaultDataSubRatConfig + .map { mobileMappingsProxy.getDefaultIcons(it) } + .distinctUntilChanged() + .logInputChange(logger, "defaultMobileIconGroup") override fun getRepoForSubId(subId: Int): MobileConnectionRepository { if (!isValidSubId(subId)) { @@ -191,22 +204,24 @@ constructor( * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual * connection repositories also observe the URI for [MOBILE_DATA] + subId. */ - override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { - val observer = - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - trySend(Unit) - } - } + override val globalMobileDataSettingChangedEvent: Flow<Unit> = + conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } - globalSettings.registerContentObserver( - globalSettings.getUriFor(MOBILE_DATA), - true, - observer - ) + globalSettings.registerContentObserver( + globalSettings.getUriFor(MOBILE_DATA), + true, + observer + ) - awaitClose { context.contentResolver.unregisterContentObserver(observer) } - } + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + .logInputChange(logger, "globalMobileDataSettingChangedEvent") @SuppressLint("MissingPermission") override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = @@ -236,6 +251,8 @@ constructor( awaitClose { connectivityManager.unregisterNetworkCallback(callback) } } + .distinctUntilChanged() + .logInputChange(logger, "defaultMobileNetworkConnectivity") .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel()) private fun isValidSubId(subId: Int): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 76e6a96a19d7..e6686dce7bbc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CarrierConfigManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository @@ -35,6 +36,9 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn interface MobileIconInteractor { + /** The table log created for this connection */ + val tableLogBuffer: TableLogBuffer + /** The current mobile data activity */ val activity: Flow<DataActivityModel> @@ -97,6 +101,8 @@ class MobileIconInteractorImpl( ) : MobileIconInteractor { private val connectionInfo = connectionRepository.connectionInfo + override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer + override val activity = connectionInfo.mapLatest { it.dataActivityDirection } override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index 62fa723dbf04..829a5cad6504 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -20,7 +20,6 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarIconController -import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel @@ -70,6 +69,9 @@ constructor( private val mobileSubIdsState: StateFlow<List<Int>> = mobileSubIds.stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + /** In order to keep the logs tame, we will reuse the same top-level mobile icons view model */ + val mobileIconsViewModel = iconsViewModelFactory.create(mobileSubIdsState) + override fun start() { // Only notify the icon controller if we want to *render* the new icons. // Note that this flow may still run if @@ -81,12 +83,4 @@ constructor( } } } - - /** - * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's - * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with - * the old view system. - */ - fun createMobileIconsViewModel(): MobileIconsViewModel = - iconsViewModelFactory.create(mobileSubIdsState) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 545e624273f1..ab442b5ab4de 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -30,7 +30,7 @@ import com.android.settingslib.graph.SignalDrawable import com.android.systemui.R import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -39,7 +39,7 @@ object MobileIconBinder { @JvmStatic fun bind( view: ViewGroup, - viewModel: MobileIconViewModel, + viewModel: LocationBasedMobileViewModel, ) { val activityContainer = view.requireViewById<View>(R.id.inout_container) val activityIn = view.requireViewById<ImageView>(R.id.mobile_in) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index 0ab7bcd96844..e86fee24fe4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -24,7 +24,7 @@ import com.android.systemui.R import com.android.systemui.statusbar.BaseStatusBarFrameLayout import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import java.util.ArrayList class ModernStatusBarMobileView( @@ -71,7 +71,7 @@ class ModernStatusBarMobileView( fun constructAndBind( context: Context, slot: String, - viewModel: MobileIconViewModel, + viewModel: LocationBasedMobileViewModel, ): ModernStatusBarMobileView { return (LayoutInflater.from(context) .inflate(R.layout.status_bar_mobile_signal_group_new, null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt new file mode 100644 index 000000000000..b0dc41f45488 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf + +/** + * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This + * allows the mobile icon to change some view parameters at different locations + * + * @param commonImpl for convenience, this class wraps a base interface that can provides all of the + * common implementations between locations. See [MobileIconViewModel] + */ +abstract class LocationBasedMobileViewModel( + val commonImpl: MobileIconViewModelCommon, + val logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon by commonImpl { + abstract val tint: Flow<Int> + + companion object { + fun viewModelForLocation( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, + loc: StatusBarLocation, + ): LocationBasedMobileViewModel = + when (loc) { + StatusBarLocation.HOME -> HomeMobileIconViewModel(commonImpl, logger) + StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, logger) + StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, logger) + } + } +} + +class HomeMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { + override val tint: Flow<Int> = + flowOf(Color.CYAN) + .distinctUntilChanged() + .logOutputChange(logger, "HOME tint(${commonImpl.subscriptionId})") +} + +class QsMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { + override val tint: Flow<Int> = + flowOf(Color.GREEN) + .distinctUntilChanged() + .logOutputChange(logger, "QS tint(${commonImpl.subscriptionId})") +} + +class KeyguardMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + logger: ConnectivityPipelineLogger, +) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { + override val tint: Flow<Int> = + flowOf(Color.MAGENTA) + .distinctUntilChanged() + .logOutputChange(logger, "KEYGUARD tint(${commonImpl.subscriptionId})") +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 961283f57def..2d6ac4efd512 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -16,23 +16,40 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel -import android.graphics.Color import com.android.settingslib.graph.SignalDrawable import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +/** Common interface for all of the location-based mobile icon view models. */ +interface MobileIconViewModelCommon { + val subscriptionId: Int + /** An int consumable by [SignalDrawable] for display */ + val iconId: Flow<Int> + val roaming: Flow<Boolean> + /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ + val networkTypeIcon: Flow<Icon?> + val activityInVisible: Flow<Boolean> + val activityOutVisible: Flow<Boolean> + val activityContainerVisible: Flow<Boolean> +} /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over @@ -40,25 +57,29 @@ import kotlinx.coroutines.flow.mapLatest * subscription's information. * * There will be exactly one [MobileIconViewModel] per filtered subscription offered from - * [MobileIconsInteractor.filteredSubscriptions] + * [MobileIconsInteractor.filteredSubscriptions]. * - * TODO: figure out where carrier merged and VCN models go (probably here?) + * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon] + * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view + * model gets the exact same information, as well as allows us to log that unified state only once + * per icon. */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileIconViewModel constructor( - val subscriptionId: Int, + override val subscriptionId: Int, iconInteractor: MobileIconInteractor, logger: ConnectivityPipelineLogger, constants: ConnectivityConstants, -) { + scope: CoroutineScope, +) : MobileIconViewModelCommon { /** Whether or not to show the error state of [SignalDrawable] */ private val showExclamationMark: Flow<Boolean> = iconInteractor.isDefaultDataEnabled.mapLatest { !it } - /** An int consumable by [SignalDrawable] for display */ - val iconId: Flow<Int> = + override val iconId: Flow<Int> = run { + val initial = SignalDrawable.getEmptyState(iconInteractor.numberOfLevels.value) combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) { level, numberOfLevels, @@ -66,32 +87,56 @@ constructor( SignalDrawable.getState(level, numberOfLevels, showExclamationMark) } .distinctUntilChanged() - .logOutputChange(logger, "iconId($subscriptionId)") + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "iconId", + initialValue = initial, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + } - /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - val networkTypeIcon: Flow<Icon?> = + override val networkTypeIcon: Flow<Icon?> = combine( - iconInteractor.networkTypeIconGroup, - iconInteractor.isDataConnected, - iconInteractor.isDataEnabled, - iconInteractor.isDefaultConnectionFailed, - iconInteractor.alwaysShowDataRatIcon, - ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow -> - val desc = - if (networkTypeIconGroup.dataContentDescription != 0) - ContentDescription.Resource(networkTypeIconGroup.dataContentDescription) - else null - val icon = Icon.Resource(networkTypeIconGroup.dataType, desc) - return@combine when { - alwaysShow -> icon - !dataConnected -> null - !dataEnabled -> null - failedConnection -> null - else -> icon + iconInteractor.networkTypeIconGroup, + iconInteractor.isDataConnected, + iconInteractor.isDataEnabled, + iconInteractor.isDefaultConnectionFailed, + iconInteractor.alwaysShowDataRatIcon, + ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow -> + val desc = + if (networkTypeIconGroup.dataContentDescription != 0) + ContentDescription.Resource(networkTypeIconGroup.dataContentDescription) + else null + val icon = Icon.Resource(networkTypeIconGroup.dataType, desc) + return@combine when { + alwaysShow -> icon + !dataConnected -> null + !dataEnabled -> null + failedConnection -> null + else -> icon + } } - } + .distinctUntilChanged() + .onEach { + // This is done as an onEach side effect since Icon is not Diffable (yet) + iconInteractor.tableLogBuffer.logChange( + prefix = "", + columnName = "networkTypeIcon", + value = it.toString(), + ) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) - val roaming: Flow<Boolean> = iconInteractor.isRoaming + override val roaming: StateFlow<Boolean> = + iconInteractor.isRoaming + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "roaming", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) private val activity: Flow<DataActivityModel?> = if (!constants.shouldShowActivityConfig) { @@ -100,10 +145,39 @@ constructor( iconInteractor.activity } - val activityInVisible: Flow<Boolean> = activity.map { it?.hasActivityIn ?: false } - val activityOutVisible: Flow<Boolean> = activity.map { it?.hasActivityOut ?: false } - val activityContainerVisible: Flow<Boolean> = - activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) } + override val activityInVisible: Flow<Boolean> = + activity + .map { it?.hasActivityIn ?: false } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "activityInVisible", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val activityOutVisible: Flow<Boolean> = + activity + .map { it?.hasActivityOut ?: false } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "activityOutVisible", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) - val tint: Flow<Int> = flowOf(Color.CYAN) + override val activityContainerVisible: Flow<Boolean> = + activity + .map { it != null && (it.hasActivityIn || it.hasActivityOut) } + .distinctUntilChanged() + .logDiffsForTable( + iconInteractor.tableLogBuffer, + columnPrefix = "", + columnName = "activityContainerVisible", + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 0b41d319f9dc..b9318b181aaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -14,17 +14,19 @@ * limitations under the License. */ -@file:OptIn(InternalCoroutinesApi::class) - package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import javax.inject.Inject -import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch /** * View model for describing the system's current mobile cellular connections. The result is a list @@ -38,15 +40,33 @@ constructor( private val interactor: MobileIconsInteractor, private val logger: ConnectivityPipelineLogger, private val constants: ConnectivityConstants, + @Application private val scope: CoroutineScope, ) { - /** TODO: do we need to cache these? */ - fun viewModelForSub(subId: Int): MobileIconViewModel = - MobileIconViewModel( - subId, - interactor.createMobileConnectionInteractorForSubId(subId), - logger, - constants, - ) + @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>() + + init { + scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } } + } + + fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { + val common = + mobileIconSubIdCache[subId] + ?: MobileIconViewModel( + subId, + interactor.createMobileConnectionInteractorForSubId(subId), + logger, + constants, + scope, + ) + .also { mobileIconSubIdCache[subId] = it } + + return LocationBasedMobileViewModel.viewModelForLocation(common, logger, location) + } + + private fun removeInvalidModelsFromCache(subIds: List<Int>) { + val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) } + subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) } + } class Factory @Inject @@ -54,6 +74,7 @@ constructor( private val interactor: MobileIconsInteractor, private val logger: ConnectivityPipelineLogger, private val constants: ConnectivityConstants, + @Application private val scope: CoroutineScope, ) { fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel { return MobileIconsViewModel( @@ -61,6 +82,7 @@ constructor( interactor, logger, constants, + scope, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt index d3cf32fb44ce..d3ff3573dae2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt @@ -18,8 +18,11 @@ package com.android.systemui.statusbar.pipeline.shared import android.net.Network import android.net.NetworkCapabilities -import com.android.systemui.log.dagger.StatusBarConnectivityLog +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.TelephonyDisplayInfo import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.dagger.StatusBarConnectivityLog import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.toString @@ -28,7 +31,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onEach @SysUISingleton -class ConnectivityPipelineLogger @Inject constructor( +class ConnectivityPipelineLogger +@Inject +constructor( @StatusBarConnectivityLog private val buffer: LogBuffer, ) { /** @@ -37,34 +42,23 @@ class ConnectivityPipelineLogger @Inject constructor( * Use this method for inputs that don't have any extra information besides their callback name. */ fun logInputChange(callbackName: String) { - buffer.log( - SB_LOGGING_TAG, - LogLevel.INFO, - { str1 = callbackName }, - { "Input: $str1" } - ) + buffer.log(SB_LOGGING_TAG, LogLevel.INFO, { str1 = callbackName }, { "Input: $str1" }) } - /** - * Logs a change in one of the **raw inputs** to the connectivity pipeline. - */ + /** Logs a change in one of the **raw inputs** to the connectivity pipeline. */ fun logInputChange(callbackName: String, changeInfo: String?) { buffer.log( - SB_LOGGING_TAG, - LogLevel.INFO, - { - str1 = callbackName - str2 = changeInfo - }, - { - "Input: $str1: $str2" - } + SB_LOGGING_TAG, + LogLevel.INFO, + { + str1 = callbackName + str2 = changeInfo + }, + { "Input: $str1: $str2" } ) } - /** - * Logs a **data transformation** that we performed within the connectivity pipeline. - */ + /** Logs a **data transformation** that we performed within the connectivity pipeline. */ fun logTransformation(transformationName: String, oldValue: Any?, newValue: Any?) { if (oldValue == newValue) { buffer.log( @@ -74,9 +68,7 @@ class ConnectivityPipelineLogger @Inject constructor( str1 = transformationName str2 = oldValue.toString() }, - { - "Transform: $str1: $str2 (transformation didn't change it)" - } + { "Transform: $str1: $str2 (transformation didn't change it)" } ) } else { buffer.log( @@ -87,27 +79,21 @@ class ConnectivityPipelineLogger @Inject constructor( str2 = oldValue.toString() str3 = newValue.toString() }, - { - "Transform: $str1: $str2 -> $str3" - } + { "Transform: $str1: $str2 -> $str3" } ) } } - /** - * Logs a change in one of the **outputs** to the connectivity pipeline. - */ + /** Logs a change in one of the **outputs** to the connectivity pipeline. */ fun logOutputChange(outputParamName: String, changeInfo: String) { buffer.log( - SB_LOGGING_TAG, - LogLevel.INFO, - { - str1 = outputParamName - str2 = changeInfo - }, - { - "Output: $str1: $str2" - } + SB_LOGGING_TAG, + LogLevel.INFO, + { + str1 = outputParamName + str2 = changeInfo + }, + { "Output: $str1: $str2" } ) } @@ -119,9 +105,7 @@ class ConnectivityPipelineLogger @Inject constructor( int1 = network.getNetId() str1 = networkCapabilities.toString() }, - { - "onCapabilitiesChanged: net=$int1 capabilities=$str1" - } + { "onCapabilitiesChanged: net=$int1 capabilities=$str1" } ) } @@ -129,21 +113,93 @@ class ConnectivityPipelineLogger @Inject constructor( buffer.log( SB_LOGGING_TAG, LogLevel.INFO, + { int1 = network.getNetId() }, + { "onLost: net=$int1" } + ) + } + + fun logOnServiceStateChanged(serviceState: ServiceState, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, { - int1 = network.getNetId() + int1 = subId + bool1 = serviceState.isEmergencyOnly + bool2 = serviceState.roaming + str1 = serviceState.operatorAlphaShort }, { - "onLost: net=$int1" + "onServiceStateChanged: subId=$int1 emergencyOnly=$bool1 roaming=$bool2" + + " operator=$str1" } ) } + fun logOnSignalStrengthsChanged(signalStrength: SignalStrength, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + str1 = signalStrength.toString() + }, + { "onSignalStrengthsChanged: subId=$int1 strengths=$str1" } + ) + } + + fun logOnDataConnectionStateChanged(dataState: Int, networkType: Int, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + int2 = dataState + str1 = networkType.toString() + }, + { "onDataConnectionStateChanged: subId=$int1 dataState=$int2 networkType=$str1" }, + ) + } + + fun logOnDataActivity(direction: Int, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + int2 = direction + }, + { "onDataActivity: subId=$int1 direction=$int2" }, + ) + } + + fun logOnCarrierNetworkChange(active: Boolean, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + bool1 = active + }, + { "onCarrierNetworkChange: subId=$int1 active=$bool1" }, + ) + } + + fun logOnDisplayInfoChanged(displayInfo: TelephonyDisplayInfo, subId: Int) { + buffer.log( + SB_LOGGING_TAG, + LogLevel.INFO, + { + int1 = subId + str1 = displayInfo.toString() + }, + { "onDisplayInfoChanged: subId=$int1 displayInfo=$str1" }, + ) + } + companion object { const val SB_LOGGING_TAG = "SbConnectivity" - /** - * Log a change in one of the **inputs** to the connectivity pipeline. - */ + /** Log a change in one of the **inputs** to the connectivity pipeline. */ fun Flow<Unit>.logInputChange( logger: ConnectivityPipelineLogger, inputParamName: String, @@ -155,26 +211,26 @@ class ConnectivityPipelineLogger @Inject constructor( * Log a change in one of the **inputs** to the connectivity pipeline. * * @param prettyPrint an optional function to transform the value into a readable string. - * [toString] is used if no custom function is provided. + * [toString] is used if no custom function is provided. */ fun <T> Flow<T>.logInputChange( logger: ConnectivityPipelineLogger, inputParamName: String, prettyPrint: (T) -> String = { it.toString() } ): Flow<T> { - return this.onEach {logger.logInputChange(inputParamName, prettyPrint(it)) } + return this.onEach { logger.logInputChange(inputParamName, prettyPrint(it)) } } /** * Log a change in one of the **outputs** to the connectivity pipeline. * * @param prettyPrint an optional function to transform the value into a readable string. - * [toString] is used if no custom function is provided. + * [toString] is used if no custom function is provided. */ fun <T> Flow<T>.logOutputChange( - logger: ConnectivityPipelineLogger, - outputParamName: String, - prettyPrint: (T) -> String = { it.toString() } + logger: ConnectivityPipelineLogger, + outputParamName: String, + prettyPrint: (T) -> String = { it.toString() } ): Flow<T> { return this.onEach { logger.logOutputChange(outputParamName, prettyPrint(it)) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt new file mode 100644 index 000000000000..73bcdfd2b78e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository + +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.demomode.DemoMode +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** + * Provides the [WifiRepository] interface either through the [DemoWifiRepository] implementation, + * or the [WifiRepositoryImpl]'s prod implementation, based on the current demo mode value. In this + * way, downstream clients can all consist of real implementations and not care about which + * repository is responsible for the data. Graphically: + * + * ``` + * RealRepository + * │ + * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel + * │ + * DemoRepository + * ``` + * + * When demo mode turns on, every flow will [flatMapLatest] to the current provider's version of + * that flow. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +class WifiRepositorySwitcher +@Inject +constructor( + private val realImpl: WifiRepositoryImpl, + private val demoImpl: DemoWifiRepository, + private val demoModeController: DemoModeController, + @Application scope: CoroutineScope, +) : WifiRepository { + private val isDemoMode = + conflatedCallbackFlow { + val callback = + object : DemoMode { + override fun dispatchDemoCommand(command: String?, args: Bundle?) { + // Don't care + } + + override fun onDemoModeStarted() { + demoImpl.startProcessingCommands() + trySend(true) + } + + override fun onDemoModeFinished() { + demoImpl.stopProcessingCommands() + trySend(false) + } + } + + demoModeController.addCallback(callback) + awaitClose { demoModeController.removeCallback(callback) } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode) + + @VisibleForTesting + val activeRepo = + isDemoMode + .mapLatest { isDemoMode -> + if (isDemoMode) { + demoImpl + } else { + realImpl + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl) + + override val isWifiEnabled: StateFlow<Boolean> = + activeRepo + .flatMapLatest { it.isWifiEnabled } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiEnabled.value) + + override val isWifiDefault: StateFlow<Boolean> = + activeRepo + .flatMapLatest { it.isWifiDefault } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiDefault.value) + + override val wifiNetwork: StateFlow<WifiNetworkModel> = + activeRepo + .flatMapLatest { it.wifiNetwork } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiNetwork.value) + + override val wifiActivity: StateFlow<DataActivityModel> = + activeRepo + .flatMapLatest { it.wifiActivity } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiActivity.value) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt new file mode 100644 index 000000000000..c588945fbd67 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo + +import android.net.wifi.WifiManager +import android.os.Bundle +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn + +/** Data source to map between demo mode commands and inputs into [DemoWifiRepository]'s flows */ +@SysUISingleton +class DemoModeWifiDataSource +@Inject +constructor( + demoModeController: DemoModeController, + @Application scope: CoroutineScope, +) { + private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK) + private val _wifiCommands = demoCommandStream.map { args -> args.toWifiEvent() } + val wifiEvents = _wifiCommands.shareIn(scope, SharingStarted.WhileSubscribed()) + + private fun Bundle.toWifiEvent(): FakeWifiEventModel? { + val wifi = getString("wifi") ?: return null + return if (wifi == "show") { + activeWifiEvent() + } else { + FakeWifiEventModel.WifiDisabled + } + } + + private fun Bundle.activeWifiEvent(): FakeWifiEventModel.Wifi { + val level = getString("level")?.toInt() + val activity = getString("activity")?.toActivity() + val ssid = getString("ssid") + val validated = getString("fully").toBoolean() + + return FakeWifiEventModel.Wifi( + level = level, + activity = activity, + ssid = ssid, + validated = validated, + ) + } + + private fun String.toActivity(): Int = + when (this) { + "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT + "in" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN + "out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT + else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt new file mode 100644 index 000000000000..7890074cf8a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +/** Demo-able wifi repository to support SystemUI demo mode commands. */ +class DemoWifiRepository +@Inject +constructor( + private val dataSource: DemoModeWifiDataSource, + @Application private val scope: CoroutineScope, +) : WifiRepository { + private var demoCommandJob: Job? = null + + private val _isWifiEnabled = MutableStateFlow(false) + override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled + + private val _isWifiDefault = MutableStateFlow(false) + override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault + + private val _wifiNetwork = MutableStateFlow<WifiNetworkModel>(WifiNetworkModel.Inactive) + override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork + + private val _wifiActivity = + MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + override val wifiActivity: StateFlow<DataActivityModel> = _wifiActivity + + fun startProcessingCommands() { + demoCommandJob = + scope.launch { + dataSource.wifiEvents.filterNotNull().collect { event -> processEvent(event) } + } + } + + fun stopProcessingCommands() { + demoCommandJob?.cancel() + } + + private fun processEvent(event: FakeWifiEventModel) = + when (event) { + is FakeWifiEventModel.Wifi -> processEnabledWifiState(event) + is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState() + } + + private fun processDisabledWifiState() { + _isWifiEnabled.value = false + _isWifiDefault.value = false + _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false) + _wifiNetwork.value = WifiNetworkModel.Inactive + } + + private fun processEnabledWifiState(event: FakeWifiEventModel.Wifi) { + _isWifiEnabled.value = true + _isWifiDefault.value = true + _wifiActivity.value = + event.activity?.toWifiDataActivityModel() + ?: DataActivityModel(hasActivityIn = false, hasActivityOut = false) + _wifiNetwork.value = event.toWifiNetworkModel() + } + + private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel = + WifiNetworkModel.Active( + networkId = DEMO_NET_ID, + isValidated = validated ?: true, + level = level, + ssid = ssid, + + // These fields below aren't supported in demo mode, since they aren't needed to satisfy + // the interface. + isPasspointAccessPoint = false, + isOnlineSignUpForPasspointAccessPoint = false, + passpointProviderFriendlyName = null, + ) + + companion object { + private const val DEMO_NET_ID = 1234 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt new file mode 100644 index 000000000000..2353fb82f3b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model + +/** + * Model for demo wifi commands, ported from [NetworkControllerImpl] + * + * Nullable fields represent optional command line arguments + */ +sealed interface FakeWifiEventModel { + data class Wifi( + val level: Int?, + val activity: Int?, + val ssid: String?, + val validated: Boolean?, + ) : FakeWifiEventModel + + object WifiDisabled : FakeWifiEventModel +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt index 3c0eb910ad89..4f7fe28c1e7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt @@ -38,16 +38,12 @@ class WifiConstants @Inject constructor( dumpManager.registerDumpable("${SB_LOGGING_TAG}WifiConstants", this) } - /** True if we should show the activityIn/activityOut icons and false otherwise. */ - val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity) - /** True if we should always show the wifi icon when wifi is enabled and false otherwise. */ val alwaysShowIconIfEnabled = context.resources.getBoolean(R.bool.config_showWifiIndicatorWhenEnabled) override fun dump(pw: PrintWriter, args: Array<out String>) { pw.apply { - println("shouldShowActivityConfig=$shouldShowActivityConfig") println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index 07a7595a2e00..ab464cc78905 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -148,7 +148,7 @@ constructor( /** The wifi activity status. Null if we shouldn't display the activity status. */ private val activity: Flow<DataActivityModel?> = - if (!wifiConstants.shouldShowActivityConfig) { + if (!connectivityConstants.shouldShowActivityConfig) { flowOf(null) } else { combine(interactor.activity, interactor.ssid) { activity, ssid -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index d8a8c5de90d7..c9ed0cb4155d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -47,6 +47,7 @@ import android.view.OnReceiveContentListener; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; +import android.view.ViewGroupOverlay; import android.view.ViewRootImpl; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; @@ -57,7 +58,6 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; -import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -133,6 +133,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private RevealParams mRevealParams; private Rect mContentBackgroundBounds; private boolean mIsFocusAnimationFlagActive; + private boolean mIsAnimatingAppearance = false; // TODO(b/193539698): move these to a Controller private RemoteInputController mController; @@ -142,10 +143,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private boolean mSending; private NotificationViewWrapper mWrapper; - private Integer mDefocusTargetHeight = null; - private boolean mIsAnimatingAppearance = false; - - // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places // that need the controller shouldn't have access to the view private RemoteInputViewController mViewController; @@ -423,18 +420,6 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene return mIsAnimatingAppearance; } - /** - * View will ensure to use at most the provided defocusTargetHeight, when defocusing animated. - * This is to ensure that the parent can resize itself to the targetHeight while the defocus - * animation of the RemoteInputView is running. - * - * @param defocusTargetHeight The target height the parent will resize itself to. If null, the - * RemoteInputView will not resize itself. - */ - public void setDefocusTargetHeight(Integer defocusTargetHeight) { - mDefocusTargetHeight = defocusTargetHeight; - } - @VisibleForTesting void onDefocus(boolean animate, boolean logClose) { mController.removeRemoteInput(mEntry, mToken); @@ -443,35 +428,28 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene // During removal, we get reattached and lose focus. Not hiding in that // case to prevent flicker. if (!mRemoved) { - if (animate && mIsFocusAnimationFlagActive) { - Animator animator = getDefocusAnimator(); - - // When defocusing, the notification needs to shrink. Therefore, we need to free - // up the space that is needed for the RemoteInputView. This is done by setting - // a negative top margin of the height difference of the RemoteInputView and its - // sibling (the actions_container_layout containing the Reply button) - if (mDefocusTargetHeight != null && mDefocusTargetHeight < getHeight() - && mDefocusTargetHeight >= 0 - && getLayoutParams() instanceof FrameLayout.LayoutParams) { - int heightToShrink = getHeight() - mDefocusTargetHeight; - FrameLayout.LayoutParams layoutParams = - (FrameLayout.LayoutParams) getLayoutParams(); - layoutParams.topMargin = -heightToShrink; - setLayoutParams(layoutParams); - ((ViewGroup) getParent().getParent()).setClipChildren(false); - } + ViewGroup parent = (ViewGroup) getParent(); + if (animate && parent != null && mIsFocusAnimationFlagActive) { + + + ViewGroup grandParent = (ViewGroup) parent.getParent(); + ViewGroupOverlay overlay = parent.getOverlay(); + + // After adding this RemoteInputView to the overlay of the parent (and thus removing + // it from the parent itself), the parent will shrink in height. This causes the + // overlay to be moved. To correct the position of the overlay we need to offset it. + int overlayOffsetY = getMaxSiblingHeight() - getHeight(); + overlay.add(this); + if (grandParent != null) grandParent.setClipChildren(false); + Animator animator = getDefocusAnimator(overlayOffsetY); + View self = this; animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - //reset top margin after the animation - if (getLayoutParams() instanceof FrameLayout.LayoutParams) { - FrameLayout.LayoutParams layoutParams = - (FrameLayout.LayoutParams) getLayoutParams(); - layoutParams.topMargin = 0; - setLayoutParams(layoutParams); - ((ViewGroup) getParent().getParent()).setClipChildren(true); - } + overlay.remove(self); + parent.addView(self); + if (grandParent != null) grandParent.setClipChildren(true); setVisibility(GONE); if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); @@ -609,7 +587,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } /** - * Sets whether the feature flag for the updated inline reply animation is active or not. + * Sets whether the feature flag for the revised inline reply animation is active or not. * @param active */ public void setIsFocusAnimationFlagActive(boolean active) { @@ -846,6 +824,23 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } } + /** + * @return max sibling height (0 in case of no siblings) + */ + public int getMaxSiblingHeight() { + ViewGroup parentView = (ViewGroup) getParent(); + int maxHeight = 0; + if (parentView == null) return 0; + for (int i = 0; i < parentView.getChildCount(); i++) { + View siblingView = parentView.getChildAt(i); + if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight()); + } + return maxHeight; + } + + /** + * Creates an animator for the focus animation. + */ private Animator getFocusAnimator(View crossFadeView) { final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f); alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY); @@ -854,7 +849,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f); scaleAnimator.addUpdateListener(valueAnimator -> { - setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue()); + setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), 0); }); scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); @@ -875,21 +870,26 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene return animatorSet; } - private Animator getDefocusAnimator() { + /** + * Creates an animator for the defocus animation. + * + * @param offsetY The RemoteInputView will be offset by offsetY during the animation + */ + private Animator getDefocusAnimator(int offsetY) { final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); scaleAnimator.addUpdateListener(valueAnimator -> { - setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue()); + setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), offsetY); }); scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); scaleAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation, boolean isReverse) { - setFocusAnimationScaleY(1f); + setFocusAnimationScaleY(1f /* scaleY */, 0 /* verticalOffset */); } }); @@ -901,15 +901,21 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene /** * Sets affected view properties for a vertical scale animation * - * @param scaleY desired vertical view scale + * @param scaleY desired vertical view scale + * @param verticalOffset vertical offset to apply to the RemoteInputView during the animation */ - private void setFocusAnimationScaleY(float scaleY) { + private void setFocusAnimationScaleY(float scaleY, int verticalOffset) { int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight()); - mContentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(), + Rect contentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(), mContentView.getHeight() - verticalBoundOffset); - mContentBackground.setBounds(mContentBackgroundBounds); + mContentBackground.setBounds(contentBackgroundBounds); mContentView.setBackground(mContentBackground); - setTranslationY(verticalBoundOffset); + if (scaleY == 1f) { + mContentBackgroundBounds = null; + } else { + mContentBackgroundBounds = contentBackgroundBounds; + } + setTranslationY(verticalBoundOffset + verticalOffset); } /** Handler for button click on send action in IME. */ diff --git a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java index 1f444340653d..246488600eef 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/DemoModeFragment.java @@ -33,6 +33,7 @@ import com.android.systemui.R; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeAvailabilityTracker; import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.util.settings.GlobalSettings; public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener { @@ -54,13 +55,15 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference private SwitchPreference mOnSwitch; private DemoModeController mDemoModeController; + private GlobalSettings mGlobalSettings; private Tracker mDemoModeTracker; // We are the only ones who ever call this constructor, so don't worry about the warning @SuppressLint("ValidFragment") - public DemoModeFragment(DemoModeController demoModeController) { + public DemoModeFragment(DemoModeController demoModeController, GlobalSettings globalSettings) { super(); mDemoModeController = demoModeController; + mGlobalSettings = globalSettings; } @@ -80,7 +83,7 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference screen.addPreference(mOnSwitch); setPreferenceScreen(screen); - mDemoModeTracker = new Tracker(context); + mDemoModeTracker = new Tracker(context, mGlobalSettings); mDemoModeTracker.startTracking(); updateDemoModeEnabled(); updateDemoModeOn(); @@ -202,8 +205,8 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference } private class Tracker extends DemoModeAvailabilityTracker { - Tracker(Context context) { - super(context); + Tracker(Context context, GlobalSettings globalSettings) { + super(context, globalSettings); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java index 3231aecdc4b2..32ecb6786a51 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerActivity.java @@ -33,6 +33,7 @@ import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.util.settings.GlobalSettings; import javax.inject.Inject; @@ -44,12 +45,18 @@ public class TunerActivity extends Activity implements private final DemoModeController mDemoModeController; private final TunerService mTunerService; + private final GlobalSettings mGlobalSettings; @Inject - TunerActivity(DemoModeController demoModeController, TunerService tunerService) { + TunerActivity( + DemoModeController demoModeController, + TunerService tunerService, + GlobalSettings globalSettings + ) { super(); mDemoModeController = demoModeController; mTunerService = tunerService; + mGlobalSettings = globalSettings; } protected void onCreate(Bundle savedInstanceState) { @@ -69,7 +76,7 @@ public class TunerActivity extends Activity implements boolean showDemoMode = action != null && action.equals( "com.android.settings.action.DEMO_MODE"); final PreferenceFragment fragment = showDemoMode - ? new DemoModeFragment(mDemoModeController) + ? new DemoModeFragment(mDemoModeController, mGlobalSettings) : new TunerFragment(mTunerService); getFragmentManager().beginTransaction().replace(R.id.content_frame, fragment, TAG_TUNER).commit(); diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt index 9653985cb6e6..d6b3b22bfd02 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt @@ -21,6 +21,8 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.lifecycle.repeatWhenAttached import java.util.function.Consumer +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collect @@ -34,7 +36,10 @@ fun <T> collectFlow( view: View, flow: Flow<T>, consumer: Consumer<T>, + coroutineContext: CoroutineContext = EmptyCoroutineContext, state: Lifecycle.State = Lifecycle.State.CREATED, ) { - view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } } + view.repeatWhenAttached(coroutineContext) { + repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt index 6c1f008e9337..bb03f288147e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt @@ -22,9 +22,13 @@ import android.view.ViewGroup import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState.KEYGUARD +import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -33,7 +37,6 @@ import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations /** @@ -50,7 +53,9 @@ class KeyguardUnfoldTransitionTest : SysuiTestCase() { @Mock private lateinit var parent: ViewGroup - private lateinit var keyguardUnfoldTransition: KeyguardUnfoldTransition + @Mock private lateinit var statusBarStateController: StatusBarStateController + + private lateinit var underTest: KeyguardUnfoldTransition private lateinit var progressListener: TransitionProgressListener private var xTranslationMax = 0f @@ -61,10 +66,10 @@ class KeyguardUnfoldTransitionTest : SysuiTestCase() { xTranslationMax = context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat() - keyguardUnfoldTransition = KeyguardUnfoldTransition(context, progressProvider) + underTest = KeyguardUnfoldTransition(context, statusBarStateController, progressProvider) - keyguardUnfoldTransition.setup(parent) - keyguardUnfoldTransition.statusViewCentered = false + underTest.setup(parent) + underTest.statusViewCentered = false verify(progressProvider).addCallback(capture(progressListenerCaptor)) progressListener = progressListenerCaptor.value @@ -72,10 +77,11 @@ class KeyguardUnfoldTransitionTest : SysuiTestCase() { @Test fun onTransition_centeredViewDoesNotMove() { - keyguardUnfoldTransition.statusViewCentered = true + whenever(statusBarStateController.getState()).thenReturn(KEYGUARD) + underTest.statusViewCentered = true val view = View(context) - `when`(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view) + whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view) progressListener.onTransitionStarted() assertThat(view.translationX).isZero() @@ -89,4 +95,44 @@ class KeyguardUnfoldTransitionTest : SysuiTestCase() { progressListener.onTransitionFinished() assertThat(view.translationX).isZero() } + + @Test + fun whenInShadeState_viewDoesNotMove() { + whenever(statusBarStateController.getState()).thenReturn(SHADE) + + val view = View(context) + whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view) + + progressListener.onTransitionStarted() + assertThat(view.translationX).isZero() + + progressListener.onTransitionProgress(0f) + assertThat(view.translationX).isZero() + + progressListener.onTransitionProgress(0.5f) + assertThat(view.translationX).isZero() + + progressListener.onTransitionFinished() + assertThat(view.translationX).isZero() + } + + @Test + fun whenInKeyguardState_viewDoesMove() { + whenever(statusBarStateController.getState()).thenReturn(KEYGUARD) + + val view = View(context) + whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view) + + progressListener.onTransitionStarted() + assertThat(view.translationX).isZero() + + progressListener.onTransitionProgress(0f) + assertThat(view.translationX).isEqualTo(xTranslationMax) + + progressListener.onTransitionProgress(0.5f) + assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax) + + progressListener.onTransitionFinished() + assertThat(view.translationX).isZero() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java index e7e6918325a7..bdd496ec219b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardListenerTest.java @@ -18,6 +18,8 @@ package com.android.systemui.clipboardoverlay; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_ENABLED; +import static com.google.android.setupcompat.util.WizardManagerHelper.SETTINGS_SECURE_USER_SETUP_COMPLETE; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -32,6 +34,7 @@ import android.content.ClipDescription; import android.content.ClipboardManager; import android.os.PersistableBundle; import android.provider.DeviceConfig; +import android.provider.Settings; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -66,6 +69,8 @@ public class ClipboardListenerTest extends SysuiTestCase { @Mock private ClipboardOverlayController mOverlayController; @Mock + private ClipboardToast mClipboardToast; + @Mock private UiEventLogger mUiEventLogger; @Mock private FeatureFlags mFeatureFlags; @@ -84,6 +89,8 @@ public class ClipboardListenerTest extends SysuiTestCase { @Spy private Provider<ClipboardOverlayController> mOverlayControllerProvider; + private ClipboardListener mClipboardListener; + @Before public void setup() { @@ -93,7 +100,8 @@ public class ClipboardListenerTest extends SysuiTestCase { when(mClipboardOverlayControllerLegacyFactory.create(any())) .thenReturn(mOverlayControllerLegacy); when(mClipboardManager.hasPrimaryClip()).thenReturn(true); - + Settings.Secure.putInt( + mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 1); mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, new ClipData.Item("Test Item")); @@ -101,16 +109,17 @@ public class ClipboardListenerTest extends SysuiTestCase { when(mClipboardManager.getPrimaryClipSource()).thenReturn(mSampleSource); mDeviceConfigProxy = new DeviceConfigProxyFake(); + + mClipboardListener = new ClipboardListener(getContext(), mDeviceConfigProxy, + mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, + mClipboardToast, mClipboardManager, mUiEventLogger, mFeatureFlags); } @Test public void test_disabled() { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "false", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); verifyZeroInteractions(mClipboardManager); verifyZeroInteractions(mUiEventLogger); } @@ -119,10 +128,7 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_enabled() { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); verify(mClipboardManager).addPrimaryClipChangedListener(any()); verifyZeroInteractions(mUiEventLogger); } @@ -133,11 +139,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); - listener.onPrimaryClipChanged(); + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerLegacyFactory).create(any()); @@ -152,14 +155,14 @@ public class ClipboardListenerTest extends SysuiTestCase { // Should clear the overlay controller mRunnableCaptor.getValue().run(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mClipboardOverlayControllerLegacyFactory, times(2)).create(any()); verifyZeroInteractions(mOverlayControllerProvider); @@ -171,11 +174,8 @@ public class ClipboardListenerTest extends SysuiTestCase { mDeviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_ENABLED, "true", false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); - listener.onPrimaryClipChanged(); + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); verify(mOverlayControllerProvider).get(); @@ -190,14 +190,14 @@ public class ClipboardListenerTest extends SysuiTestCase { // Should clear the overlay controller mRunnableCaptor.getValue().run(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mOverlayControllerProvider, times(2)).get(); // Not calling the runnable here, just change the clip again and verify that the overlay is // NOT recreated. - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mOverlayControllerProvider, times(2)).get(); verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory); @@ -233,13 +233,10 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_logging_enterAndReenter() { when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(false); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); - listener.onPrimaryClipChanged(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mUiEventLogger, times(1)).log( ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); @@ -251,17 +248,29 @@ public class ClipboardListenerTest extends SysuiTestCase { public void test_logging_enterAndReenter_new() { when(mFeatureFlags.isEnabled(Flags.CLIPBOARD_OVERLAY_REFACTOR)).thenReturn(true); - ClipboardListener listener = new ClipboardListener(getContext(), mDeviceConfigProxy, - mOverlayControllerProvider, mClipboardOverlayControllerLegacyFactory, - mClipboardManager, mUiEventLogger, mFeatureFlags); - listener.start(); + mClipboardListener.start(); - listener.onPrimaryClipChanged(); - listener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); + mClipboardListener.onPrimaryClipChanged(); verify(mUiEventLogger, times(1)).log( ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ENTERED, 0, mSampleSource); verify(mUiEventLogger, times(1)).log( ClipboardOverlayEvent.CLIPBOARD_OVERLAY_UPDATED, 0, mSampleSource); } + + @Test + public void test_userSetupIncomplete_showsToast() { + Settings.Secure.putInt( + mContext.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0); + + mClipboardListener.start(); + mClipboardListener.onPrimaryClipChanged(); + + verify(mUiEventLogger, times(1)).log( + ClipboardOverlayEvent.CLIPBOARD_TOAST_SHOWN, 0, mSampleSource); + verify(mClipboardToast, times(1)).showCopiedToast(); + verifyZeroInteractions(mOverlayControllerProvider); + verifyZeroInteractions(mClipboardOverlayControllerLegacyFactory); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt new file mode 100644 index 000000000000..87c66b5a98ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/demomode/DemoModeControllerTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.demomode + +import android.content.Intent +import android.os.Bundle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.demomode.DemoMode.ACTION_DEMO +import com.android.systemui.demomode.DemoMode.COMMAND_STATUS +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +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.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@SmallTest +class DemoModeControllerTest : SysuiTestCase() { + private lateinit var underTest: DemoModeController + + @Mock private lateinit var dumpManager: DumpManager + + private val globalSettings = FakeSettings() + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + + MockitoAnnotations.initMocks(this) + + globalSettings.putInt(DemoModeController.DEMO_MODE_ALLOWED, 1) + globalSettings.putInt(DemoModeController.DEMO_MODE_ON, 1) + + underTest = + DemoModeController( + context = context, + dumpManager = dumpManager, + globalSettings = globalSettings, + broadcastDispatcher = fakeBroadcastDispatcher + ) + + underTest.initialize() + } + + @Test + fun `demo command flow - returns args`() = + testScope.runTest { + var latest: Bundle? = null + val flow = underTest.demoFlowForCommand(TEST_COMMAND) + val job = launch { flow.collect { latest = it } } + + sendDemoCommand(args = mapOf("key1" to "val1")) + assertThat(latest!!.getString("key1")).isEqualTo("val1") + + sendDemoCommand(args = mapOf("key2" to "val2")) + assertThat(latest!!.getString("key2")).isEqualTo("val2") + + job.cancel() + } + + private fun sendDemoCommand(command: String? = TEST_COMMAND, args: Map<String, String>) { + val intent = Intent(ACTION_DEMO) + intent.putExtra("command", command) + args.forEach { arg -> intent.putExtra(arg.key, arg.value) } + + fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive(context, intent) } + } + + companion object { + // Use a valid command until we properly fake out everything + const val TEST_COMMAND = COMMAND_STATUS + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt index 003efbfdc4d7..9f534ef14f54 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.dreams import android.testing.AndroidTestingRunner 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 @@ -30,23 +31,27 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) -class DreamCallbackControllerTest : SysuiTestCase() { +class DreamOverlayCallbackControllerTest : SysuiTestCase() { - @Mock private lateinit var callback: DreamCallbackController.DreamCallback + @Mock private lateinit var callback: DreamOverlayCallbackController.Callback - private lateinit var underTest: DreamCallbackController + private lateinit var underTest: DreamOverlayCallbackController @Before fun setUp() { MockitoAnnotations.initMocks(this) - underTest = DreamCallbackController() + underTest = DreamOverlayCallbackController() } @Test - fun testOnWakeUpInvokesCallback() { + fun onWakeUpInvokesCallback() { + underTest.onStartDream() + assertThat(underTest.isDreaming).isEqualTo(true) + underTest.addCallback(callback) underTest.onWakeUp() verify(callback).onWakeUp() + assertThat(underTest.isDreaming).isEqualTo(false) // Adding twice should not invoke twice reset(callback) @@ -60,4 +65,27 @@ class DreamCallbackControllerTest : SysuiTestCase() { underTest.onWakeUp() verify(callback, never()).onWakeUp() } + + @Test + fun onStartDreamInvokesCallback() { + underTest.addCallback(callback) + + assertThat(underTest.isDreaming).isEqualTo(false) + + underTest.onStartDream() + verify(callback).onStartDream() + assertThat(underTest.isDreaming).isEqualTo(true) + + // Adding twice should not invoke twice + reset(callback) + underTest.addCallback(callback) + underTest.onStartDream() + verify(callback, times(1)).onStartDream() + + // After remove, no call to callback + reset(callback) + underTest.removeCallback(callback) + underTest.onStartDream() + verify(callback, never()).onStartDream() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index 52663ceffbba..8f97026ef3e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -140,7 +140,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { UiEventLogger mUiEventLogger; @Mock - DreamCallbackController mDreamCallbackController; + DreamOverlayCallbackController mDreamOverlayCallbackController; @Captor ArgumentCaptor<View> mViewCaptor; @@ -186,7 +186,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { mUiEventLogger, mTouchInsetManager, LOW_LIGHT_COMPONENT, - mDreamCallbackController); + mDreamOverlayCallbackController); } @Test @@ -398,7 +398,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { mService.onWakeUp(callback); mMainExecutor.runAllReady(); verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor); - verify(mDreamCallbackController).onWakeUp(); + verify(mDreamOverlayCallbackController).onWakeUp(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 563d44d3f15f..be712f699b7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -28,8 +28,7 @@ import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback import com.android.systemui.doze.DozeTransitionListener -import com.android.systemui.dreams.DreamCallbackController -import com.android.systemui.dreams.DreamCallbackController.DreamCallback +import com.android.systemui.dreams.DreamOverlayCallbackController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource @@ -68,7 +67,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var dozeTransitionListener: DozeTransitionListener @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var dreamCallbackController: DreamCallbackController + @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController private lateinit var underTest: KeyguardRepositoryImpl @@ -86,7 +85,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardUpdateMonitor, dozeTransitionListener, authController, - dreamCallbackController, + dreamOverlayCallbackController, ) } @@ -171,6 +170,29 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isKeyguardOccluded() = + runTest(UnconfinedTestDispatcher()) { + whenever(keyguardStateController.isOccluded).thenReturn(false) + var latest: Boolean? = null + val job = underTest.isKeyguardOccluded.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + val captor = argumentCaptor<KeyguardStateController.Callback>() + verify(keyguardStateController).addCallback(captor.capture()) + + whenever(keyguardStateController.isOccluded).thenReturn(true) + captor.value.onKeyguardShowingChanged() + assertThat(latest).isTrue() + + whenever(keyguardStateController.isOccluded).thenReturn(false) + captor.value.onKeyguardShowingChanged() + assertThat(latest).isFalse() + + job.cancel() + } + + @Test fun isDozing() = runTest(UnconfinedTestDispatcher()) { var latest: Boolean? = null @@ -343,19 +365,22 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test - fun isDreamingFromDreamCallbackController() = + fun isDreamingFromDreamOverlayCallbackController() = runTest(UnconfinedTestDispatcher()) { - whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(true) + whenever(dreamOverlayCallbackController.isDreaming).thenReturn(false) var latest: Boolean? = null - val job = underTest.isDreaming.onEach { latest = it }.launchIn(this) + val job = underTest.isDreamingWithOverlay.onEach { latest = it }.launchIn(this) - assertThat(latest).isTrue() + assertThat(latest).isFalse() val listener = - withArgCaptor<DreamCallbackController.DreamCallback> { - verify(dreamCallbackController).addCallback(capture()) + withArgCaptor<DreamOverlayCallbackController.Callback> { + verify(dreamOverlayCallbackController).addCallback(capture()) } + listener.onStartDream() + assertThat(latest).isTrue() + listener.onWakeUp() assertThat(latest).isFalse() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index a6cf84053861..d2b7838274a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -23,6 +23,8 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl +import com.android.systemui.keyguard.shared.model.DozeStateModel +import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.WakeSleepReason @@ -42,6 +44,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Mock +import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -64,8 +67,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { // Used to verify transition requests for test output @Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository - private lateinit var lockscreenBouncerTransitionInteractor: - LockscreenBouncerTransitionInteractor + private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor + private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor @Before fun setUp() { @@ -79,25 +82,82 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { transitionRepository = KeyguardTransitionRepositoryImpl() runner = KeyguardTransitionRunner(transitionRepository) - lockscreenBouncerTransitionInteractor = - LockscreenBouncerTransitionInteractor( + fromLockscreenTransitionInteractor = + FromLockscreenTransitionInteractor( scope = testScope, keyguardInteractor = KeyguardInteractor(keyguardRepository), shadeRepository = shadeRepository, keyguardTransitionRepository = mockTransitionRepository, keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), ) - lockscreenBouncerTransitionInteractor.start() + fromLockscreenTransitionInteractor.start() + + fromDreamingTransitionInteractor = + FromDreamingTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromDreamingTransitionInteractor.start() } @Test + fun `DREAMING to LOCKSCREEN`() = + testScope.runTest { + // GIVEN a device is dreaming and occluded + keyguardRepository.setDreamingWithOverlay(true) + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + + // GIVEN a prior transition has run to DREAMING + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN doze is complete + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + // AND dreaming has stopped + keyguardRepository.setDreamingWithOverlay(false) + // AND occluded has stopped + keyguardRepository.setKeyguardOccluded(false) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to BOUNCER should occur + assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.DREAMING) + assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun `LOCKSCREEN to BOUNCER via bouncer showing call`() = testScope.runTest { // GIVEN a device that has at least woken up keyguardRepository.setWakefulnessModel(startingToWake()) runCurrent() - // GIVEN a transition has run to LOCKSCREEN + // GIVEN a prior transition has run to LOCKSCREEN runner.startTransition( testScope, TransitionInfo( @@ -122,7 +182,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { verify(mockTransitionRepository).startTransition(capture()) } // THEN a transition to BOUNCER should occur - assertThat(info.ownerName).isEqualTo("LockscreenBouncerTransitionInteractor") + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) assertThat(info.to).isEqualTo(KeyguardState.BOUNCER) assertThat(info.animator).isNotNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt index c54e456416c7..557166301d64 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -21,7 +21,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt new file mode 100644 index 000000000000..98d292d689e4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.AnimationParams +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.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA +import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { + private lateinit var underTest: OccludedToLockscreenTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + val interactor = KeyguardTransitionInteractor(repository) + underTest = OccludedToLockscreenTransitionViewModel(interactor) + } + + @Test + fun lockscreenFadeIn() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) + + repository.sendTransitionStep(step(0f)) + // Should start running here... + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.4f)) + repository.sendTransitionStep(step(0.5f)) + // ...up to here + repository.sendTransitionStep(step(0.6f)) + repository.sendTransitionStep(step(1f)) + + // Only two values should be present, since the dream overlay runs for a small fraction + // of the overall animation time + assertThat(values.size).isEqualTo(3) + assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA)) + assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA)) + assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA)) + + job.cancel() + } + + @Test + fun lockscreenTranslationY() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf<Float>() + + val pixels = 100 + val job = + underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.5f)) + repository.sendTransitionStep(step(1f)) + + assertThat(values.size).isEqualTo(4) + assertThat(values[0]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(0f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[1]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[2]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + assertThat(values[3]) + .isEqualTo( + -pixels + + EMPHASIZED_DECELERATE.getInterpolation( + animValue(1f, LOCKSCREEN_TRANSLATION_Y) + ) * pixels + ) + + job.cancel() + } + + private fun animValue(stepValue: Float, params: AnimationParams): Float { + val totalDuration = TO_LOCKSCREEN_DURATION + val startValue = (params.startTime / totalDuration).toFloat() + + val multiplier = (totalDuration / params.duration).toFloat() + return (stepValue - startValue) * multiplier + } + + private fun step(value: Float): TransitionStep { + return TransitionStep( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + value = value, + transitionState = TransitionState.RUNNING, + ownerName = "OccludedToLockscreenTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt new file mode 100644 index 000000000000..db6fc136e651 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.testing.AndroidTestingRunner +import android.view.View +import android.view.ViewGroup +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState.KEYGUARD +import com.android.systemui.statusbar.StatusBarState.SHADE +import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.whenever +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 + +/** + * Translates items away/towards the hinge when the device is opened/closed. This is controlled by + * the set of ids, which also dictact which direction to move and when, via a filter fn. + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationPanelUnfoldAnimationControllerTest : SysuiTestCase() { + + @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider + + @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener> + + @Mock private lateinit var parent: ViewGroup + + @Mock private lateinit var statusBarStateController: StatusBarStateController + + private lateinit var underTest: NotificationPanelUnfoldAnimationController + private lateinit var progressListener: TransitionProgressListener + private var xTranslationMax = 0f + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + xTranslationMax = + context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings).toFloat() + + underTest = + NotificationPanelUnfoldAnimationController( + context, + statusBarStateController, + progressProvider + ) + underTest.setup(parent) + + verify(progressProvider).addCallback(capture(progressListenerCaptor)) + progressListener = progressListenerCaptor.value + } + + @Test + fun whenInKeyguardState_viewDoesNotMove() { + whenever(statusBarStateController.getState()).thenReturn(KEYGUARD) + + val view = View(context) + whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view) + + progressListener.onTransitionStarted() + assertThat(view.translationX).isZero() + + progressListener.onTransitionProgress(0f) + assertThat(view.translationX).isZero() + + progressListener.onTransitionProgress(0.5f) + assertThat(view.translationX).isZero() + + progressListener.onTransitionFinished() + assertThat(view.translationX).isZero() + } + + @Test + fun whenInShadeState_viewDoesMove() { + whenever(statusBarStateController.getState()).thenReturn(SHADE) + + val view = View(context) + whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view) + + progressListener.onTransitionStarted() + assertThat(view.translationX).isZero() + + progressListener.onTransitionProgress(0f) + assertThat(view.translationX).isEqualTo(xTranslationMax) + + progressListener.onTransitionProgress(0.5f) + assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax) + + progressListener.onTransitionFinished() + assertThat(view.translationX).isZero() + } + + @Test + fun whenInShadeLockedState_viewDoesMove() { + whenever(statusBarStateController.getState()).thenReturn(SHADE_LOCKED) + + val view = View(context) + whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view) + + progressListener.onTransitionStarted() + assertThat(view.translationX).isZero() + + progressListener.onTransitionProgress(0f) + assertThat(view.translationX).isEqualTo(xTranslationMax) + + progressListener.onTransitionProgress(0.5f) + assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax) + + progressListener.onTransitionFinished() + assertThat(view.translationX).isZero() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 0586a0cbfb8e..53ab19c47106 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -108,6 +108,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInterac import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel; +import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; @@ -189,6 +190,8 @@ import org.mockito.stubbing.Answer; import java.util.List; import java.util.Optional; +import kotlinx.coroutines.CoroutineDispatcher; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @@ -291,7 +294,9 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor; @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel; + @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + @Mock private CoroutineDispatcher mMainDispatcher; @Mock private MotionEvent mDownMotionEvent; @Captor private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener> @@ -510,6 +515,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mKeyguardBottomAreaInteractor, mAlternateBouncerInteractor, mDreamingToLockscreenTransitionViewModel, + mOccludedToLockscreenTransitionViewModel, + mMainDispatcher, mKeyguardTransitionInteractor, mDumpManager); mNotificationPanelViewController.initDependencies( diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt index 43c694245eba..3e769e94b6ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt @@ -21,6 +21,7 @@ import android.provider.Settings.Secure.DOZE_DOUBLE_TAP_GESTURE import android.provider.Settings.Secure.DOZE_TAP_SCREEN_GESTURE import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.os.PowerManager import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -36,9 +37,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong -import org.mockito.ArgumentMatchers.anyObject import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.never @@ -106,7 +107,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onSingleTapUp(upEv) // THEN wake up device if dozing - verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces).wakeUpIfDozing( + anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP)) } @Test @@ -125,7 +127,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onDoubleTapEvent(upEv) // THEN wake up device if dozing - verify(centralSurfaces).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces).wakeUpIfDozing( + anyLong(), any(), anyString(), eq(PowerManager.WAKE_REASON_TAP)) } @Test @@ -156,7 +159,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onSingleTapUp(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } @Test @@ -203,7 +207,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onDoubleTapEvent(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } @Test @@ -222,7 +227,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onSingleTapUp(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } @Test @@ -241,7 +247,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { underTest.onDoubleTapEvent(upEv) // THEN the device doesn't wake up - verify(centralSurfaces, never()).wakeUpIfDozing(anyLong(), anyObject(), anyString()) + verify(centralSurfaces, never()).wakeUpIfDozing( + anyLong(), any(), anyString(), anyInt()) } fun updateSettings() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt index 8275c0c24339..9b3626bfc9ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/TargetSdkResolverTest.kt @@ -127,6 +127,6 @@ class TargetSdkResolverTest : SysuiTestCase() { NotificationManager.IMPORTANCE_DEFAULT, null, null, null, null, null, true, 0, false, -1, false, null, null, false, false, - false, null, 0, false) + false, null, 0, false, 0) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 1ce460c60a54..3a1f9b748e49 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -31,6 +31,7 @@ import static junit.framework.TestCase.fail; 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.clearInvocations; @@ -177,6 +178,8 @@ import com.android.systemui.volume.VolumeComponent; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.StartingSurface; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -189,8 +192,6 @@ import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.Optional; -import dagger.Lazy; - @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -306,6 +307,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private ViewRootImpl mViewRootImpl; @Mock private WindowOnBackInvokedDispatcher mOnBackInvokedDispatcher; @Captor private ArgumentCaptor<OnBackInvokedCallback> mOnBackInvokedCallback; + @Mock IPowerManager mPowerManagerService; private ShadeController mShadeController; private final FakeSystemClock mFakeSystemClock = new FakeSystemClock(); @@ -319,9 +321,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { public void setup() throws Exception { MockitoAnnotations.initMocks(this); - IPowerManager powerManagerService = mock(IPowerManager.class); IThermalService thermalService = mock(IThermalService.class); - mPowerManager = new PowerManager(mContext, powerManagerService, thermalService, + mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService, Handler.createAsync(Looper.myLooper())); mNotificationInterruptStateProvider = @@ -363,7 +364,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { when(mStackScrollerController.getView()).thenReturn(mStackScroller); when(mStackScroller.generateLayoutParams(any())).thenReturn(new LayoutParams(0, 0)); when(mNotificationPanelView.getLayoutParams()).thenReturn(new LayoutParams(0, 0)); - when(powerManagerService.isInteractive()).thenReturn(true); + when(mPowerManagerService.isInteractive()).thenReturn(true); when(mStackScroller.getActivatedChild()).thenReturn(null); doAnswer(invocation -> { @@ -1190,6 +1191,34 @@ public class CentralSurfacesImplTest extends SysuiTestCase { verify(mStatusBarStateController).setState(SHADE); } + @Test + public void dozing_wakeUp() throws RemoteException { + // GIVEN can wakeup when dozing & is dozing + when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true); + setDozing(true); + + // WHEN wakeup is requested + final int wakeReason = PowerManager.WAKE_REASON_TAP; + mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason); + + // THEN power manager receives wakeup + verify(mPowerManagerService).wakeUp(eq(0L), eq(wakeReason), anyString(), anyString()); + } + + @Test + public void notDozing_noWakeUp() throws RemoteException { + // GIVEN can wakeup when dozing and NOT dozing + when(mScreenOffAnimationController.allowWakeUpIfDozing()).thenReturn(true); + setDozing(false); + + // WHEN wakeup is requested + final int wakeReason = PowerManager.WAKE_REASON_TAP; + mCentralSurfaces.wakeUpIfDozing(0, null, "", wakeReason); + + // THEN power manager receives wakeup + verify(mPowerManagerService, never()).wakeUp(anyLong(), anyInt(), anyString(), anyString()); + } + /** * Configures the appropriate mocks and then calls {@link CentralSurfacesImpl#updateIsKeyguard} * to reconfigure the keyguard to reflect the requested showing/occluded states. @@ -1226,6 +1255,13 @@ public class CentralSurfacesImplTest extends SysuiTestCase { states); } + private void setDozing(boolean isDozing) { + ArgumentCaptor<StatusBarStateController.StateListener> callbackCaptor = + ArgumentCaptor.forClass(StatusBarStateController.StateListener.class); + verify(mStatusBarStateController).addCallback(callbackCaptor.capture(), anyInt()); + callbackCaptor.getValue().onDozingChanged(isDozing); + } + public static class TestableNotificationInterruptStateProviderImpl extends NotificationInterruptStateProviderImpl { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt new file mode 100644 index 000000000000..f822ba0f0a62 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectionModelTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.model + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableRowLogger +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ACTIVITY_DIRECTION +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CARRIER_NETWORK_CHANGE +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CDMA_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_CONNECTION_STATE +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_IS_GSM +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_RESOLVED_NETWORK_TYPE +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_ROAMING +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class MobileConnectionModelTest : SysuiTestCase() { + + @Test + fun `log diff - initial log contains all columns`() { + val logger = TestLogger() + val connection = MobileConnectionModel() + + connection.logFull(logger) + + assertThat(logger.changes) + .contains(Pair(COL_EMERGENCY, connection.isEmergencyOnly.toString())) + assertThat(logger.changes).contains(Pair(COL_ROAMING, connection.isRoaming.toString())) + assertThat(logger.changes) + .contains(Pair(COL_OPERATOR, connection.operatorAlphaShort.toString())) + assertThat(logger.changes).contains(Pair(COL_IS_GSM, connection.isGsm.toString())) + assertThat(logger.changes).contains(Pair(COL_CDMA_LEVEL, connection.cdmaLevel.toString())) + assertThat(logger.changes) + .contains(Pair(COL_PRIMARY_LEVEL, connection.primaryLevel.toString())) + assertThat(logger.changes) + .contains(Pair(COL_CONNECTION_STATE, connection.dataConnectionState.toString())) + assertThat(logger.changes) + .contains(Pair(COL_ACTIVITY_DIRECTION, connection.dataActivityDirection.toString())) + assertThat(logger.changes) + .contains( + Pair(COL_CARRIER_NETWORK_CHANGE, connection.carrierNetworkChangeActive.toString()) + ) + assertThat(logger.changes) + .contains(Pair(COL_RESOLVED_NETWORK_TYPE, connection.resolvedNetworkType.toString())) + } + + @Test + fun `log diff - primary level changes - only level is logged`() { + val logger = TestLogger() + val connectionOld = MobileConnectionModel(primaryLevel = 1) + + val connectionNew = MobileConnectionModel(primaryLevel = 2) + + connectionNew.logDiffs(connectionOld, logger) + + assertThat(logger.changes).isEqualTo(listOf(Pair(COL_PRIMARY_LEVEL, "2"))) + } + + private class TestLogger : TableRowLogger { + val changes = mutableListOf<Pair<String, String>>() + + override fun logChange(columnName: String, value: String?) { + changes.add(Pair(columnName, value.toString())) + } + + override fun logChange(columnName: String, value: Int) { + changes.add(Pair(columnName, value.toString())) + } + + override fun logChange(columnName: String, value: Boolean) { + changes.add(Pair(columnName, value.toString())) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index 59eec5327c12..d6a9ee325b2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -16,12 +16,16 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository -class FakeMobileConnectionRepository(override val subId: Int) : MobileConnectionRepository { +class FakeMobileConnectionRepository( + override val subId: Int, + override val tableLogBuffer: TableLogBuffer, +) : MobileConnectionRepository { private val _connectionInfo = MutableStateFlow(MobileConnectionModel()) override val connectionInfo = _connectionInfo diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 04d3cdd89ab7..7f93328ee95e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -22,14 +22,17 @@ import android.telephony.TelephonyManager import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository -class FakeMobileConnectionsRepository(mobileMappings: MobileMappingsProxy) : - MobileConnectionsRepository { +class FakeMobileConnectionsRepository( + mobileMappings: MobileMappingsProxy, + val tableLogBuffer: TableLogBuffer, +) : MobileConnectionsRepository { val GSM_KEY = mobileMappings.toIconKey(GSM) val LTE_KEY = mobileMappings.toIconKey(LTE) val UMTS_KEY = mobileMappings.toIconKey(UMTS) @@ -63,7 +66,7 @@ class FakeMobileConnectionsRepository(mobileMappings: MobileMappingsProxy) : private val subIdRepos = mutableMapOf<Int, MobileConnectionRepository>() override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] - ?: FakeMobileConnectionRepository(subId).also { subIdRepos[subId] = it } + ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it } } private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt index 18ae90db881a..5d377a8658a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt @@ -24,6 +24,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource @@ -37,6 +39,7 @@ import com.android.systemui.util.mockito.kotlinArgumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -69,12 +72,14 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { private lateinit var realRepo: MobileConnectionsRepositoryImpl private lateinit var demoRepo: DemoMobileConnectionsRepository private lateinit var mockDataSource: DemoModeMobileConnectionDataSource + private lateinit var logFactory: TableLogBufferFactory @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var demoModeController: DemoModeController + @Mock private lateinit var dumpManager: DumpManager private val globalSettings = FakeSettings() private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null) @@ -86,6 +91,8 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock()) + // Never start in demo mode whenever(demoModeController.isInDemoMode).thenReturn(false) @@ -114,6 +121,7 @@ class MobileRepositorySwitcherTest : SysuiTestCase() { dataSource = mockDataSource, scope = scope, context = context, + logFactory = logFactory, ) underTest = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt index 3d5316d1f19d..210208532dd4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel @@ -54,6 +56,9 @@ import org.junit.runners.Parameterized.Parameters @RunWith(Parameterized::class) internal class DemoMobileConnectionParameterizedTest(private val testCase: TestCase) : SysuiTestCase() { + + private val logFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -76,6 +81,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC dataSource = mockDataSource, scope = testScope.backgroundScope, context = context, + logFactory = logFactory, ) connectionsRepo.startProcessingCommands() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt index 34f30eb7c0a6..cdbe75e855bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt @@ -23,6 +23,8 @@ import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons.THREE_G import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -32,6 +34,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import junit.framework.Assert import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -47,6 +50,9 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) @SmallTest class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { + private val dumpManager: DumpManager = mock() + private val logFactory = TableLogBufferFactory(dumpManager, FakeSystemClock()) + private val testDispatcher = UnconfinedTestDispatcher() private val testScope = TestScope(testDispatcher) @@ -68,6 +74,7 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { dataSource = mockDataSource, scope = testScope.backgroundScope, context = context, + logFactory = logFactory, ) underTest.startProcessingCommands() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index 7fa80653f29c..7970443f69b1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -50,6 +50,7 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel @@ -87,14 +88,15 @@ import org.mockito.MockitoAnnotations @SmallTest class MobileConnectionRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionRepositoryImpl + private lateinit var connectionsRepo: FakeMobileConnectionsRepository @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var tableLogger: TableLogBuffer private val scope = CoroutineScope(IMMEDIATE) private val mobileMappings = FakeMobileMappingsProxy() private val globalSettings = FakeSettings() - private val connectionsRepo = FakeMobileConnectionsRepository(mobileMappings) @Before fun setUp() { @@ -102,6 +104,8 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { globalSettings.userId = UserHandle.USER_ALL whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) + connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger) + underTest = MobileConnectionRepositoryImpl( context, @@ -116,6 +120,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { mobileMappings, IMMEDIATE, logger, + tableLogger, scope, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 3cc1e8b74668..b8cd7a4f6e0a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -34,12 +34,15 @@ import com.android.internal.telephony.PhoneConstants import com.android.settingslib.R import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings @@ -57,6 +60,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -72,6 +76,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var logBufferFactory: TableLogBufferFactory private val mobileMappings = FakeMobileMappingsProxy() @@ -89,6 +94,10 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } } + whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ -> + mock<TableLogBuffer>() + } + connectionFactory = MobileConnectionRepositoryImpl.Factory( fakeBroadcastDispatcher, @@ -99,6 +108,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { logger = logger, mobileMappingsProxy = mobileMappings, scope = scope, + logFactory = logBufferFactory, ) underTest = @@ -271,6 +281,32 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `connection repository - log buffer contains sub id in its name`() = + runBlocking(IMMEDIATE) { + val job = underTest.subscriptions.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // Get repos to trigger creation + underTest.getRepoForSubId(SUB_1_ID) + verify(logBufferFactory) + .create( + eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)), + anyInt(), + ) + underTest.getRepoForSubId(SUB_2_ID) + verify(logBufferFactory) + .create( + eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)), + anyInt(), + ) + + job.cancel() + } + + @Test fun testDefaultDataSubId_updatesOnBroadcast() = runBlocking(IMMEDIATE) { var latest: Int? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index c3519b7c8176..c49458909c78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -19,11 +19,14 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CellSignalStrength import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileIconInteractor : MobileIconInteractor { +class FakeMobileIconInteractor( + override val tableLogBuffer: TableLogBuffer, +) : MobileIconInteractor { override val alwaysShowDataRatIcon = MutableStateFlow(false) override val activity = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 9f300e9e0cf3..19e5516b58a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -22,12 +22,15 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_LTE import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor { +class FakeMobileIconsInteractor( + mobileMappings: MobileMappingsProxy, + val tableLogBuffer: TableLogBuffer, +) : MobileIconsInteractor { val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) val LTE_KEY = mobileMappings.toIconKey(LTE) val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) @@ -48,8 +51,7 @@ class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIco override val isDefaultConnectionFailed = MutableStateFlow(false) - private val _filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf()) - override val filteredSubscriptions: Flow<List<SubscriptionModel>> = _filteredSubscriptions + override val filteredSubscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf()) private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false) override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled @@ -67,7 +69,7 @@ class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIco /** Always returns a new fake interactor */ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor { - return FakeMobileIconInteractor() + return FakeMobileIconInteractor(tableLogBuffer) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 4dca780425e5..83c5055a6eda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -49,8 +49,8 @@ import org.junit.Test class MobileIconInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconInteractor private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) - private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID) + private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy, mock()) + private val connectionRepository = FakeMobileConnectionRepository(SUB_1_ID, mock()) private val scope = CoroutineScope(IMMEDIATE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 85578942ba86..2fa3467587cc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -20,6 +20,7 @@ import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.settingslib.mobile.MobileMappings import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository @@ -28,6 +29,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSe import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -44,9 +46,9 @@ import org.mockito.MockitoAnnotations @SmallTest class MobileIconsInteractorTest : SysuiTestCase() { private lateinit var underTest: MobileIconsInteractor + private lateinit var connectionsRepository: FakeMobileConnectionsRepository private val userSetupRepository = FakeUserSetupRepository() private val mobileMappingsProxy = FakeMobileMappingsProxy() - private val connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy) private val scope = CoroutineScope(IMMEDIATE) @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker @@ -55,6 +57,7 @@ class MobileIconsInteractorTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) + connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer) connectionsRepository.setMobileConnectionRepositoryMap( mapOf( SUB_1_ID to CONNECTION_1, @@ -290,21 +293,23 @@ class MobileIconsInteractorTest : SysuiTestCase() { companion object { private val IMMEDIATE = Dispatchers.Main.immediate + private val tableLogBuffer = + TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock()) private const val SUB_1_ID = 1 private val SUB_1 = SubscriptionModel(subscriptionId = SUB_1_ID) - private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID) + private val CONNECTION_1 = FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer) private const val SUB_2_ID = 2 private val SUB_2 = SubscriptionModel(subscriptionId = SUB_2_ID) - private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID) + private val CONNECTION_2 = FakeMobileConnectionRepository(SUB_2_ID, tableLogBuffer) private const val SUB_3_ID = 3 private val SUB_3_OPP = SubscriptionModel(subscriptionId = SUB_3_ID, isOpportunistic = true) - private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID) + private val CONNECTION_3 = FakeMobileConnectionRepository(SUB_3_ID, tableLogBuffer) private const val SUB_4_ID = 4 private val SUB_4_OPP = SubscriptionModel(subscriptionId = SUB_4_ID, isOpportunistic = true) - private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID) + private val CONNECTION_4 = FakeMobileConnectionRepository(SUB_4_ID, tableLogBuffer) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt new file mode 100644 index 000000000000..043d55a73076 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class LocationBasedMobileIconViewModelTest : SysuiTestCase() { + private lateinit var commonImpl: MobileIconViewModelCommon + private lateinit var homeIcon: HomeMobileIconViewModel + private lateinit var qsIcon: QsMobileIconViewModel + private lateinit var keyguardIcon: KeyguardMobileIconViewModel + private lateinit var interactor: FakeMobileIconInteractor + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var constants: ConnectivityConstants + @Mock private lateinit var tableLogBuffer: TableLogBuffer + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + interactor = FakeMobileIconInteractor(tableLogBuffer) + interactor.apply { + setLevel(1) + setIsDefaultDataEnabled(true) + setIsFailedConnection(false) + setIconGroup(TelephonyIcons.THREE_G) + setIsEmergencyOnly(false) + setNumberOfLevels(4) + isDataConnected.value = true + } + commonImpl = + MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope) + + homeIcon = HomeMobileIconViewModel(commonImpl, logger) + qsIcon = QsMobileIconViewModel(commonImpl, logger) + keyguardIcon = KeyguardMobileIconViewModel(commonImpl, logger) + } + + @Test + fun `location based view models receive same icon id when common impl updates`() = + testScope.runTest { + var latestHome: Int? = null + val homeJob = homeIcon.iconId.onEach { latestHome = it }.launchIn(this) + + var latestQs: Int? = null + val qsJob = qsIcon.iconId.onEach { latestQs = it }.launchIn(this) + + var latestKeyguard: Int? = null + val keyguardJob = keyguardIcon.iconId.onEach { latestKeyguard = it }.launchIn(this) + + var expected = defaultSignal(level = 1) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + interactor.setLevel(2) + expected = defaultSignal(level = 2) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + homeJob.cancel() + qsJob.cancel() + keyguardJob.cancel() + } + + companion object { + private const val SUB_1_ID = 1 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 415ce75345b2..50221bc97bad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -22,32 +22,42 @@ import com.android.settingslib.mobile.TelephonyIcons.THREE_G import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.MockitoAnnotations +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest class MobileIconViewModelTest : SysuiTestCase() { private lateinit var underTest: MobileIconViewModel - private val interactor = FakeMobileIconInteractor() + private lateinit var interactor: FakeMobileIconInteractor @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var constants: ConnectivityConstants + @Mock private lateinit var tableLogBuffer: TableLogBuffer + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun setUp() { MockitoAnnotations.initMocks(this) + interactor = FakeMobileIconInteractor(tableLogBuffer) interactor.apply { setLevel(1) setIsDefaultDataEnabled(true) @@ -57,12 +67,13 @@ class MobileIconViewModelTest : SysuiTestCase() { setNumberOfLevels(4) isDataConnected.value = true } - underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + underTest = + MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope) } @Test fun iconId_correctLevel_notCutout() = - runBlocking(IMMEDIATE) { + testScope.runTest { var latest: Int? = null val job = underTest.iconId.onEach { latest = it }.launchIn(this) val expected = defaultSignal() @@ -74,7 +85,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun iconId_cutout_whenDefaultDataDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIsDefaultDataEnabled(false) var latest: Int? = null @@ -88,7 +99,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_dataEnabled_groupIsRepresented() = - runBlocking(IMMEDIATE) { + testScope.runTest { val expected = Icon.Resource( THREE_G.dataType, @@ -106,7 +117,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_nullWhenDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsDataEnabled(false) var latest: Icon? = null @@ -119,7 +130,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_nullWhenFailedConnection() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsDataEnabled(true) interactor.setIsFailedConnection(true) @@ -133,7 +144,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_nullWhenDataDisconnects() = - runBlocking(IMMEDIATE) { + testScope.runTest { val initial = Icon.Resource( THREE_G.dataType, @@ -157,7 +168,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_null_changeToDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { val expected = Icon.Resource( THREE_G.dataType, @@ -180,7 +191,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_alwaysShow_shownEvenWhenDisabled() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsDataEnabled(true) interactor.alwaysShowDataRatIcon.value = true @@ -200,7 +211,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_alwaysShow_shownEvenWhenDisconnected() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.isDataConnected.value = false interactor.alwaysShowDataRatIcon.value = true @@ -220,7 +231,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun networkType_alwaysShow_shownEvenWhenFailedConnection() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.setIconGroup(THREE_G) interactor.setIsFailedConnection(true) interactor.alwaysShowDataRatIcon.value = true @@ -240,7 +251,7 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun roaming() = - runBlocking(IMMEDIATE) { + testScope.runTest { interactor.isRoaming.value = true var latest: Boolean? = null val job = underTest.roaming.onEach { latest = it }.launchIn(this) @@ -256,10 +267,17 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun `data activity - null when config is off`() = - runBlocking(IMMEDIATE) { + testScope.runTest { // Create a new view model here so the constants are properly read whenever(constants.shouldShowActivityConfig).thenReturn(false) - underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + underTest = + MobileIconViewModel( + SUB_1_ID, + interactor, + logger, + constants, + testScope.backgroundScope, + ) var inVisible: Boolean? = null val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) @@ -288,10 +306,17 @@ class MobileIconViewModelTest : SysuiTestCase() { @Test fun `data activity - config on - test indicators`() = - runBlocking(IMMEDIATE) { + testScope.runTest { // Create a new view model here so the constants are properly read whenever(constants.shouldShowActivityConfig).thenReturn(true) - underTest = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + underTest = + MobileIconViewModel( + SUB_1_ID, + interactor, + logger, + constants, + testScope.backgroundScope, + ) var inVisible: Boolean? = null val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) @@ -340,16 +365,15 @@ class MobileIconViewModelTest : SysuiTestCase() { containerJob.cancel() } - /** Convenience constructor for these tests */ - private fun defaultSignal( - level: Int = 1, - connected: Boolean = true, - ): Int { - return SignalDrawable.getState(level, /* numLevels */ 4, !connected) - } - companion object { - private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 + + /** Convenience constructor for these tests */ + fun defaultSignal( + level: Int = 1, + connected: Boolean = true, + ): Int { + return SignalDrawable.getState(level, /* numLevels */ 4, !connected) + } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt new file mode 100644 index 000000000000..d6cb76260f0b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class MobileIconsViewModelTest : SysuiTestCase() { + private lateinit var underTest: MobileIconsViewModel + private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) + + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var constants: ConnectivityConstants + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + val subscriptionIdsFlow = + interactor.filteredSubscriptions + .map { subs -> subs.map { it.subscriptionId } } + .stateIn(testScope.backgroundScope, SharingStarted.WhileSubscribed(), listOf()) + + underTest = + MobileIconsViewModel( + subscriptionIdsFlow, + interactor, + logger, + constants, + testScope.backgroundScope, + ) + + interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + } + + @Test + fun `caching - mobile icon view model is reused for same sub id`() = + testScope.runTest { + val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) + val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS) + + assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl) + } + + @Test + fun `caching - invalid view models are removed from cache when sub disappears`() = + testScope.runTest { + // Retrieve models to trigger caching + val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) + val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS) + + // Both impls are cached + assertThat(underTest.mobileIconSubIdCache) + .containsExactly(1, model1.commonImpl, 2, model2.commonImpl) + + // SUB_1 is removed from the list... + interactor.filteredSubscriptions.value = listOf(SUB_2) + + // ... and dropped from the cache + assertThat(underTest.mobileIconSubIdCache).containsExactly(2, model2.commonImpl) + } + + companion object { + private val SUB_1 = SubscriptionModel(subscriptionId = 1, isOpportunistic = false) + private val SUB_2 = SubscriptionModel(subscriptionId = 2, isOpportunistic = false) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt new file mode 100644 index 000000000000..b935442fd73a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.wifi.data.repository + +import android.net.ConnectivityManager +import android.net.wifi.WifiManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.demomode.DemoMode +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class WifiRepositorySwitcherTest : SysuiTestCase() { + private lateinit var underTest: WifiRepositorySwitcher + private lateinit var realImpl: WifiRepositoryImpl + private lateinit var demoImpl: DemoWifiRepository + + @Mock private lateinit var demoModeController: DemoModeController + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var tableLogger: TableLogBuffer + @Mock private lateinit var connectivityManager: ConnectivityManager + @Mock private lateinit var wifiManager: WifiManager + @Mock private lateinit var demoModeWifiDataSource: DemoModeWifiDataSource + private val demoModelFlow = MutableStateFlow<FakeWifiEventModel?>(null) + + private val mainExecutor = FakeExecutor(FakeSystemClock()) + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + // Never start in demo mode + whenever(demoModeController.isInDemoMode).thenReturn(false) + + realImpl = + WifiRepositoryImpl( + fakeBroadcastDispatcher, + connectivityManager, + logger, + tableLogger, + mainExecutor, + testScope.backgroundScope, + wifiManager, + ) + + whenever(demoModeWifiDataSource.wifiEvents).thenReturn(demoModelFlow) + + demoImpl = + DemoWifiRepository( + demoModeWifiDataSource, + testScope.backgroundScope, + ) + + underTest = + WifiRepositorySwitcher( + realImpl, + demoImpl, + demoModeController, + testScope.backgroundScope, + ) + } + + @Test + fun `switcher active repo - updates when demo mode changes`() = + testScope.runTest { + assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl) + + var latest: WifiRepository? = null + val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + + startDemoMode() + + assertThat(latest).isSameInstanceAs(demoImpl) + + finishDemoMode() + + assertThat(latest).isSameInstanceAs(realImpl) + + job.cancel() + } + + private fun startDemoMode() { + whenever(demoModeController.isInDemoMode).thenReturn(true) + getDemoModeCallback().onDemoModeStarted() + } + + private fun finishDemoMode() { + whenever(demoModeController.isInDemoMode).thenReturn(false) + getDemoModeCallback().onDemoModeFinished() + } + + private fun getDemoModeCallback(): DemoMode { + val captor = kotlinArgumentCaptor<DemoMode>() + Mockito.verify(demoModeController).addCallback(captor.capture()) + return captor.value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index b47f177bbf24..41584347c0f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -146,7 +146,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -183,7 +183,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(false) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(false) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -225,7 +225,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_nullSsid_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null)) @@ -268,7 +268,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activity_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -308,7 +308,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityIn_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -330,7 +330,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityIn_hasActivityInFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -352,7 +352,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityOut_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -374,7 +374,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityOut_hasActivityOutFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -396,7 +396,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -418,7 +418,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -440,7 +440,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_inAndOutTrue_outputsTrue() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) @@ -462,7 +462,7 @@ class WifiViewModelTest : SysuiTestCase() { @Test fun activityContainer_inAndOutFalse_outputsFalse() = runBlocking(IMMEDIATE) { - whenever(wifiConstants.shouldShowActivityConfig).thenReturn(true) + whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 2c472049cdda..4b32ee262cdc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -55,6 +55,7 @@ import android.view.ViewRootImpl; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.widget.EditText; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; @@ -414,7 +415,9 @@ public class RemoteInputViewTest extends SysuiTestCase { mDependency, TestableLooper.get(this)); ExpandableNotificationRow row = helper.createRow(); + FrameLayout remoteInputViewParent = new FrameLayout(mContext); RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); + remoteInputViewParent.addView(view); bindController(view, row.getEntry()); // Start defocus animation 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 55019490bdcd..39d2ecaef51a 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 @@ -46,12 +46,18 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isKeyguardShowing = MutableStateFlow(false) override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing + private val _isKeyguardOccluded = MutableStateFlow(false) + override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded + private val _isDozing = MutableStateFlow(false) override val isDozing: Flow<Boolean> = _isDozing private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming + private val _isDreamingWithOverlay = MutableStateFlow(false) + override val isDreamingWithOverlay: Flow<Boolean> = _isDreamingWithOverlay + private val _dozeAmount = MutableStateFlow(0f) override val linearDozeAmount: Flow<Float> = _dozeAmount @@ -112,10 +118,18 @@ class FakeKeyguardRepository : KeyguardRepository { _isKeyguardShowing.value = isShowing } + fun setKeyguardOccluded(isOccluded: Boolean) { + _isKeyguardOccluded.value = isOccluded + } + fun setDozing(isDozing: Boolean) { _isDozing.value = isDozing } + fun setDreamingWithOverlay(isDreaming: Boolean) { + _isDreamingWithOverlay.value = isDreaming + } + fun setDozeAmount(dozeAmount: Float) { _dozeAmount.value = dozeAmount } @@ -144,6 +158,10 @@ class FakeKeyguardRepository : KeyguardRepository { _fingerprintSensorLocation.tryEmit(location) } + fun setDozeTransitionModel(model: DozeTransitionModel) { + _dozeTransitionModel.value = model + } + override fun isUdfpsSupported(): Boolean { return _isUdfpsSupported.value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java index 045e6f19c667..7bcad456ff6e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar; +import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; + import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationChannel; @@ -57,6 +59,7 @@ public class RankingBuilder { private ShortcutInfo mShortcutInfo = null; private int mRankingAdjustment = 0; private boolean mIsBubble = false; + private int mProposedImportance = IMPORTANCE_UNSPECIFIED; public RankingBuilder() { } @@ -86,6 +89,7 @@ public class RankingBuilder { mShortcutInfo = ranking.getConversationShortcutInfo(); mRankingAdjustment = ranking.getRankingAdjustment(); mIsBubble = ranking.isBubble(); + mProposedImportance = ranking.getProposedImportance(); } public Ranking build() { @@ -114,7 +118,8 @@ public class RankingBuilder { mIsConversation, mShortcutInfo, mRankingAdjustment, - mIsBubble); + mIsBubble, + mProposedImportance); return ranking; } @@ -214,6 +219,11 @@ public class RankingBuilder { return this; } + public RankingBuilder setProposedImportance(@Importance int importance) { + mProposedImportance = importance; + return this; + } + public RankingBuilder setUserSentiment(int userSentiment) { mUserSentiment = userSentiment; return this; diff --git a/services/api/current.txt b/services/api/current.txt index b5798d56f0c9..090a4499a591 100644 --- a/services/api/current.txt +++ b/services/api/current.txt @@ -72,16 +72,90 @@ package com.android.server.pm { package com.android.server.pm.pkg { public interface AndroidPackage { + method @Nullable public String getAppComponentFactory(); + method @Nullable public String getApplicationClassName(); + method @Nullable public String getBackupAgentName(); + method @DrawableRes public int getBannerRes(); + method public int getBaseRevisionCode(); + method public int getCategory(); + method @Nullable public String getClassLoaderName(); + method @Dimension(unit=android.annotation.Dimension.DP) public int getCompatibleWidthLimitDp(); + method @XmlRes public int getDataExtractionRulesRes(); + method @StringRes public int getDescriptionRes(); + method @XmlRes public int getFullBackupContentRes(); + method public int getGwpAsanMode(); + method @DrawableRes public int getIconRes(); + method @StringRes public int getLabelRes(); + method @Dimension(unit=android.annotation.Dimension.DP) public int getLargestWidthLimitDp(); method @NonNull public java.util.List<java.lang.String> getLibraryNames(); + method @XmlRes public int getLocaleConfigRes(); + method @DrawableRes public int getLogoRes(); + method public long getLongVersionCode(); + method public float getMaxAspectRatio(); + method public float getMinAspectRatio(); + method public int getNativeHeapZeroInitialized(); + method @XmlRes public int getNetworkSecurityConfigRes(); + method @Nullable public String getRequiredAccountType(); + method @Dimension(unit=android.annotation.Dimension.DP) public int getRequiresSmallestWidthDp(); + method @Nullable public String getRestrictedAccountType(); + method @DrawableRes public int getRoundIconRes(); method @Nullable public String getSdkLibraryName(); + method @Nullable public String getSharedUserId(); + method @StringRes public int getSharedUserLabelRes(); method @NonNull public java.util.List<com.android.server.pm.pkg.AndroidPackageSplit> getSplits(); method @Nullable public String getStaticSharedLibraryName(); method @NonNull public java.util.UUID getStorageUuid(); method public int getTargetSdkVersion(); + method @StyleRes public int getThemeRes(); + method public int getUiOptions(); + method @Nullable public String getVersionName(); + method @Nullable public String getZygotePreloadName(); + method public boolean isAllowAudioPlaybackCapture(); + method public boolean isAllowBackup(); + method public boolean isAllowClearUserData(); + method public boolean isAllowClearUserDataOnFailedRestore(); + method public boolean isAllowNativeHeapPointerTagging(); + method public boolean isAllowTaskReparenting(); + method public boolean isAnyDensity(); + method public boolean isAttributionsUserVisible(); + method public boolean isBackupInForeground(); + method public boolean isCantSaveState(); + method public boolean isCoreApp(); + method public boolean isCrossProfile(); method public boolean isDebuggable(); + method public boolean isDefaultToDeviceProtectedStorage(); + method public boolean isDirectBootAware(); + method public boolean isExtractNativeLibs(); + method public boolean isFactoryTest(); + method public boolean isForceQueryable(); + method public boolean isFullBackupOnly(); + method public boolean isHardwareAccelerated(); + method public boolean isHasCode(); + method public boolean isHasFragileUserData(); method public boolean isIsolatedSplitLoading(); + method public boolean isKillAfterRestore(); + method public boolean isLargeHeap(); + method public boolean isLeavingSharedUser(); + method public boolean isMultiArch(); + method public boolean isNativeLibraryRootRequiresIsa(); + method public boolean isOnBackInvokedCallbackEnabled(); + method public boolean isPersistent(); + method public boolean isProfileable(); + method public boolean isProfileableByShell(); + method public boolean isRequestLegacyExternalStorage(); + method public boolean isRequiredForAllUsers(); + method public boolean isResetEnabledSettingsOnAppDataCleared(); + method public boolean isRestoreAnyVersion(); method public boolean isSignedWithPlatformKey(); + method public boolean isSupportsExtraLargeScreens(); + method public boolean isSupportsLargeScreens(); + method public boolean isSupportsNormalScreens(); + method public boolean isSupportsRtl(); + method public boolean isSupportsSmallScreens(); + method public boolean isTestOnly(); + method public boolean isUse32BitAbi(); method public boolean isUseEmbeddedDex(); + method public boolean isUsesCleartextTraffic(); method public boolean isUsesNonSdkApi(); method public boolean isVmSafeMode(); } diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index 1c571a7036ad..53f5fe10d3e9 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -25,6 +25,8 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.app.backup.BackupManager; +import android.app.backup.BackupRestoreEventLogger; +import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupManager; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IBackupObserver; @@ -1556,6 +1558,22 @@ public class BackupManagerService extends IBackupManager.Stub { } } + public void reportDelayedRestoreResult(String packageName, List<DataTypeResult> results) { + int userId = Binder.getCallingUserHandle().getIdentifier(); + if (!isUserReadyForBackup(userId)) { + Slog.w(TAG, "Returning from reportDelayedRestoreResult as backup for user" + userId + + " is not initialized yet"); + return; + } + UserBackupManagerService userBackupManagerService = + getServiceForUserIfCallerHasPermission(userId, + /* caller */ "reportDelayedRestoreResult()"); + + if (userBackupManagerService != null) { + userBackupManagerService.reportDelayedRestoreResult(packageName, results); + } + } + /** * Returns the {@link UserBackupManagerService} instance for the specified user {@code userId}. * If the user is not registered with the service (either the user is locked or not eligible for diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index ce3e6289d40e..6ba01d712e92 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -49,6 +49,7 @@ import android.app.backup.BackupAgent; import android.app.backup.BackupAnnotations.BackupDestination; import android.app.backup.BackupManager; import android.app.backup.BackupManagerMonitor; +import android.app.backup.BackupRestoreEventLogger; import android.app.backup.FullBackup; import android.app.backup.IBackupManager; import android.app.backup.IBackupManagerMonitor; @@ -505,13 +506,14 @@ public class UserBackupManagerService { @VisibleForTesting UserBackupManagerService(Context context, PackageManager packageManager, - LifecycleOperationStorage operationStorage) { + LifecycleOperationStorage operationStorage, TransportManager transportManager) { mContext = context; mUserId = 0; mRegisterTransportsRequestedTime = 0; mPackageManager = packageManager; mOperationStorage = operationStorage; + mTransportManager = transportManager; mBaseStateDir = null; mDataDir = null; @@ -521,7 +523,6 @@ public class UserBackupManagerService { mRunInitReceiver = null; mRunInitIntent = null; mAgentTimeoutParameters = null; - mTransportManager = null; mActivityManagerInternal = null; mAlarmManager = null; mConstants = null; @@ -3038,6 +3039,37 @@ public class UserBackupManagerService { mBackupPreferences.addExcludedKeys(packageName, keys); } + public void reportDelayedRestoreResult(String packageName, + List<BackupRestoreEventLogger.DataTypeResult> results) { + String transport = mTransportManager.getCurrentTransportName(); + if (transport == null) { + Slog.w(TAG, "Failed to send delayed restore logs as no transport selected"); + return; + } + + TransportConnection transportConnection = null; + try { + PackageInfo packageInfo = getPackageManager().getPackageInfoAsUser(packageName, + PackageManager.PackageInfoFlags.of(/* value */ 0), getUserId()); + + transportConnection = mTransportManager.getTransportClientOrThrow( + transport, /* caller */"BMS.reportDelayedRestoreResult"); + BackupTransportClient transportClient = transportConnection.connectOrThrow( + /* caller */ "BMS.reportDelayedRestoreResult"); + + IBackupManagerMonitor monitor = transportClient.getBackupManagerMonitor(); + BackupManagerMonitorUtils.sendAgentLoggingResults(monitor, packageInfo, results); + } catch (NameNotFoundException | TransportNotAvailableException + | TransportNotRegisteredException | RemoteException e) { + Slog.w(TAG, "Failed to send delayed restore logs: " + e); + } finally { + if (transportConnection != null) { + mTransportManager.disposeOfTransportClient(transportConnection, + /* caller */"BMS.reportDelayedRestoreResult"); + } + } + } + private boolean startConfirmationUi(int token, String action) { try { Intent confIntent = new Intent(action); diff --git a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java index 8eda5b9a3219..57ad89b0a482 100644 --- a/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java +++ b/services/backup/java/com/android/server/backup/utils/BackupManagerMonitorUtils.java @@ -24,10 +24,11 @@ import static android.app.backup.BackupManagerMonitor.LOG_EVENT_ID_AGENT_LOGGING import static com.android.server.backup.BackupManagerService.DEBUG; import static com.android.server.backup.BackupManagerService.TAG; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.IBackupAgent; import android.app.backup.BackupManagerMonitor; -import android.app.backup.BackupRestoreEventLogger; +import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupManagerMonitor; import android.content.pm.PackageInfo; import android.os.Bundle; @@ -119,19 +120,11 @@ public class BackupManagerMonitorUtils { } try { - AndroidFuture<List<BackupRestoreEventLogger.DataTypeResult>> resultsFuture = + AndroidFuture<List<DataTypeResult>> resultsFuture = new AndroidFuture<>(); agent.getLoggerResults(resultsFuture); - Bundle loggerResultsBundle = new Bundle(); - loggerResultsBundle.putParcelableList( - EXTRA_LOG_AGENT_LOGGING_RESULTS, + return sendAgentLoggingResults(monitor, pkg, resultsFuture.get(AGENT_LOGGER_RESULTS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return BackupManagerMonitorUtils.monitorEvent( - monitor, - LOG_EVENT_ID_AGENT_LOGGING_RESULTS, - pkg, - LOG_EVENT_CATEGORY_AGENT, - loggerResultsBundle); } catch (TimeoutException e) { Slog.w(TAG, "Timeout while waiting to retrieve logging results from agent", e); } catch (Exception e) { @@ -140,6 +133,19 @@ public class BackupManagerMonitorUtils { return monitor; } + public static IBackupManagerMonitor sendAgentLoggingResults( + @NonNull IBackupManagerMonitor monitor, PackageInfo pkg, List<DataTypeResult> results) { + Bundle loggerResultsBundle = new Bundle(); + loggerResultsBundle.putParcelableList( + EXTRA_LOG_AGENT_LOGGING_RESULTS, results); + return monitorEvent( + monitor, + LOG_EVENT_ID_AGENT_LOGGING_RESULTS, + pkg, + LOG_EVENT_CATEGORY_AGENT, + loggerResultsBundle); + } + /** * Adds given key-value pair in the bundle and returns the bundle. If bundle was null it will * be created. 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 4d173d66fcb4..cdd54719e15f 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -22,9 +22,9 @@ import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_SAME_MANAGE import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; +import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.RequiresPermission; import android.annotation.StringRes; import android.app.Activity; import android.app.ActivityOptions; @@ -62,6 +62,7 @@ import android.os.Binder; import android.os.IBinder; import android.os.LocaleList; import android.os.Looper; +import android.os.PermissionEnforcer; import android.os.PowerManager; import android.os.RemoteException; import android.os.ResultReceiver; @@ -198,6 +199,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub IVirtualDeviceActivityListener activityListener, Consumer<ArraySet<Integer>> runningAppsChangedCallback, VirtualDeviceParams params) { + super(PermissionEnforcer.fromContext(context)); UserHandle ownerUserHandle = UserHandle.getUserHandleForUid(ownerUid); mContext = context.createContextAsUser(ownerUserHandle, 0); mAssociationInfo = associationInfo; @@ -337,11 +339,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close() { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to close the virtual device"); - + super.close_enforcePermission(); synchronized (mVirtualDeviceLock) { if (!mPerDisplayWakelocks.isEmpty()) { mPerDisplayWakelocks.forEach((displayId, wakeLock) -> { @@ -389,14 +389,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return mWindowPolicyControllers; } - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void onAudioSessionStarting(int displayId, @NonNull IAudioRoutingCallback routingCallback, @Nullable IAudioConfigChangedCallback configChangedCallback) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to start audio session"); + super.onAudioSessionStarting_enforcePermission(); synchronized (mVirtualDeviceLock) { if (!mVirtualDisplayIds.contains(displayId)) { throw new SecurityException( @@ -413,12 +411,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } - @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void onAudioSessionEnded() { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to stop audio session"); + super.onAudioSessionEnded_enforcePermission(); synchronized (mVirtualDeviceLock) { if (mVirtualAudioController != null) { mVirtualAudioController.stopListening(); @@ -428,9 +424,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to create a virtual dpad"); + super.createVirtualDpad_enforcePermission(); synchronized (mVirtualDeviceLock) { if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) { throw new SecurityException( @@ -448,9 +444,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void createVirtualKeyboard(VirtualKeyboardConfig config, @NonNull IBinder deviceToken) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to create a virtual keyboard"); + super.createVirtualKeyboard_enforcePermission(); synchronized (mVirtualDeviceLock) { if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) { throw new SecurityException( @@ -470,9 +466,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void createVirtualMouse(VirtualMouseConfig config, @NonNull IBinder deviceToken) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to create a virtual mouse"); + super.createVirtualMouse_enforcePermission(); synchronized (mVirtualDeviceLock) { if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) { throw new SecurityException( @@ -490,10 +486,10 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void createVirtualTouchscreen(VirtualTouchscreenConfig config, @NonNull IBinder deviceToken) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to create a virtual touchscreen"); + super.createVirtualTouchscreen_enforcePermission(); synchronized (mVirtualDeviceLock) { if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) { throw new SecurityException( @@ -501,30 +497,29 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub + "this virtual device"); } } - int screenHeightPixels = config.getHeightInPixels(); - int screenWidthPixels = config.getWidthInPixels(); - if (screenHeightPixels <= 0 || screenWidthPixels <= 0) { + 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: " - + "(" + screenWidthPixels + ", " + screenHeightPixels + ")"); + + "(" + screenWidth + ", " + screenHeight + ")"); } final long ident = Binder.clearCallingIdentity(); try { mInputController.createTouchscreen(config.getInputDeviceName(), config.getVendorId(), config.getProductId(), deviceToken, config.getAssociatedDisplayId(), - screenHeightPixels, screenWidthPixels); + screenHeight, screenWidth); } finally { Binder.restoreCallingIdentity(ident); } } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config, @NonNull IBinder deviceToken) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to create a virtual navigation touchpad"); + super.createVirtualNavigationTouchpad_enforcePermission(); synchronized (mVirtualDeviceLock) { if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) { throw new SecurityException( @@ -552,11 +547,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterInputDevice(IBinder token) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to unregister this input device"); - + super.unregisterInputDevice_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { mInputController.unregisterInputDevice(token); @@ -577,7 +570,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) { + super.sendDpadKeyEvent_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendDpadKeyEvent(token, event); @@ -587,7 +582,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) { + super.sendKeyEvent_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendKeyEvent(token, event); @@ -597,7 +594,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) { + super.sendButtonEvent_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendButtonEvent(token, event); @@ -607,7 +606,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) { + super.sendTouchEvent_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendTouchEvent(token, event); @@ -617,7 +618,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) { + super.sendRelativeEvent_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendRelativeEvent(token, event); @@ -627,7 +630,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) { + super.sendScrollEvent_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { return mInputController.sendScrollEvent(token, event); @@ -647,11 +652,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean showPointerIcon) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to unregister this input device"); - + super.setShowPointerIcon_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { synchronized (mVirtualDeviceLock) { @@ -666,12 +669,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void createVirtualSensor( @NonNull IBinder deviceToken, @NonNull VirtualSensorConfig config) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to create a virtual sensor"); + super.createVirtualSensor_enforcePermission(); Objects.requireNonNull(config); Objects.requireNonNull(deviceToken); final long ident = Binder.clearCallingIdentity(); @@ -683,10 +685,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterSensor(@NonNull IBinder token) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to unregister a virtual sensor"); + super.unregisterSensor_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { mSensorController.unregisterSensor(token); @@ -696,10 +697,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) { - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to send a virtual sensor event"); + super.sendSensorEvent_enforcePermission(); final long ident = Binder.clearCallingIdentity(); try { return mSensorController.sendSensorEvent(token, event); @@ -709,25 +709,23 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(IVirtualDeviceIntentInterceptor intentInterceptor, IntentFilter filter) { + super.registerIntentInterceptor_enforcePermission(); Objects.requireNonNull(intentInterceptor); Objects.requireNonNull(filter); - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to register intent interceptor"); synchronized (mVirtualDeviceLock) { mIntentInterceptors.put(intentInterceptor.asBinder(), filter); } } @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterIntentInterceptor( @NonNull IVirtualDeviceIntentInterceptor intentInterceptor) { + super.unregisterIntentInterceptor_enforcePermission(); Objects.requireNonNull(intentInterceptor); - mContext.enforceCallingOrSelfPermission( - android.Manifest.permission.CREATE_VIRTUAL_DEVICE, - "Permission required to unregister intent interceptor"); synchronized (mVirtualDeviceLock) { mIntentInterceptors.remove(intentInterceptor.asBinder()); } diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 9bedbd0ec584..7b8ca912ecf2 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -1000,6 +1000,10 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { @Override public void notifySubscriptionInfoChanged() { if (VDBG) log("notifySubscriptionInfoChanged:"); + if (!checkNotifyPermission("notifySubscriptionInfoChanged()")) { + return; + } + synchronized (mRecords) { if (!mHasNotifySubscriptionInfoChangedOccurred) { log("notifySubscriptionInfoChanged: first invocation mRecords.size=" @@ -1026,6 +1030,10 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { @Override public void notifyOpportunisticSubscriptionInfoChanged() { if (VDBG) log("notifyOpptSubscriptionInfoChanged:"); + if (!checkNotifyPermission("notifyOpportunisticSubscriptionInfoChanged()")) { + return; + } + synchronized (mRecords) { if (!mHasNotifyOpportunisticSubscriptionInfoChangedOccurred) { log("notifyOpptSubscriptionInfoChanged: first invocation mRecords.size=" diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java index 45b11e1dd0c5..e06ce2c91a40 100644 --- a/services/core/java/com/android/server/am/AppProfiler.java +++ b/services/core/java/com/android/server/am/AppProfiler.java @@ -2314,7 +2314,8 @@ public class AppProfiler { void printCurrentCpuState(StringBuilder report, long time) { synchronized (mProcessCpuTracker) { - report.append(mProcessCpuTracker.printCurrentState(time)); + // Only print the first 10 processes + report.append(mProcessCpuTracker.printCurrentState(time, /* maxProcesses= */10)); } } diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index d53f8fb1c882..b89084c60bf0 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -42,6 +42,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; import java.util.Iterator; +import java.util.List; import java.util.Objects; /** @@ -124,6 +125,12 @@ class BroadcastProcessQueue { private final ArrayDeque<SomeArgs> mPendingOffload = new ArrayDeque<>(4); /** + * List of all queues holding broadcasts that are waiting to be dispatched. + */ + private final List<ArrayDeque<SomeArgs>> mPendingQueues = List.of( + mPendingUrgent, mPending, mPendingOffload); + + /** * Broadcast actively being dispatched to this process. */ private @Nullable BroadcastRecord mActive; @@ -218,11 +225,11 @@ class BroadcastProcessQueue { * given count of other receivers have reached a terminal state; typically * used for ordered broadcasts and priority traunches. */ - public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex) { + public void enqueueOrReplaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, + @NonNull BroadcastConsumer replacedBroadcastConsumer) { if (record.isReplacePending()) { - boolean didReplace = replaceBroadcastInQueue(mPending, record, recordIndex) - || replaceBroadcastInQueue(mPendingUrgent, record, recordIndex) - || replaceBroadcastInQueue(mPendingOffload, record, recordIndex); + final boolean didReplace = replaceBroadcast(record, recordIndex, + replacedBroadcastConsumer); if (didReplace) { return; } @@ -243,6 +250,26 @@ class BroadcastProcessQueue { } /** + * Searches from newest to oldest in the pending broadcast queues, and at the first matching + * pending broadcast it finds, replaces it in-place and returns -- does not attempt to handle + * "duplicate" broadcasts in the queue. + * <p> + * @return {@code true} if it found and replaced an existing record in the queue; + * {@code false} otherwise. + */ + private boolean replaceBroadcast(@NonNull BroadcastRecord record, int recordIndex, + @NonNull BroadcastConsumer replacedBroadcastConsumer) { + final int count = mPendingQueues.size(); + for (int i = 0; i < count; ++i) { + final ArrayDeque<SomeArgs> queue = mPendingQueues.get(i); + if (replaceBroadcastInQueue(queue, record, recordIndex, replacedBroadcastConsumer)) { + return true; + } + } + return false; + } + + /** * Searches from newest to oldest, and at the first matching pending broadcast * it finds, replaces it in-place and returns -- does not attempt to handle * "duplicate" broadcasts in the queue. @@ -251,7 +278,8 @@ class BroadcastProcessQueue { * {@code false} otherwise. */ private boolean replaceBroadcastInQueue(@NonNull ArrayDeque<SomeArgs> queue, - @NonNull BroadcastRecord record, int recordIndex) { + @NonNull BroadcastRecord record, int recordIndex, + @NonNull BroadcastConsumer replacedBroadcastConsumer) { final Iterator<SomeArgs> it = queue.descendingIterator(); final Object receiver = record.receivers.get(recordIndex); while (it.hasNext()) { @@ -262,12 +290,14 @@ class BroadcastProcessQueue { if ((record.callingUid == testRecord.callingUid) && (record.userId == testRecord.userId) && record.intent.filterEquals(testRecord.intent) - && isReceiverEquals(receiver, testReceiver)) { + && isReceiverEquals(receiver, testReceiver) + && testRecord.allReceiversPending()) { // Exact match found; perform in-place swap args.arg1 = record; args.argi1 = recordIndex; onBroadcastDequeued(testRecord, testRecordIndex); onBroadcastEnqueued(record, recordIndex); + replacedBroadcastConsumer.accept(testRecord, testRecordIndex); return true; } } diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index a850c8aac21e..8f241b2bbad5 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -64,6 +64,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.text.format.DateUtils; +import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.MathUtils; import android.util.Pair; @@ -629,30 +630,26 @@ class BroadcastQueueModernImpl extends BroadcastQueue { applyDeliveryGroupPolicy(r); - if (r.isReplacePending()) { - // Leave the skipped broadcasts intact in queue, so that we can - // replace them at their current position during enqueue below - forEachMatchingBroadcast(QUEUE_PREDICATE_ANY, (testRecord, testIndex) -> { - // We only allow caller to replace broadcasts they enqueued - return (r.callingUid == testRecord.callingUid) - && (r.userId == testRecord.userId) - && r.intent.filterEquals(testRecord.intent); - }, mBroadcastConsumerSkipAndCanceled, false); - } - r.enqueueTime = SystemClock.uptimeMillis(); r.enqueueRealTime = SystemClock.elapsedRealtime(); r.enqueueClockTime = System.currentTimeMillis(); + final ArraySet<BroadcastRecord> replacedBroadcasts = new ArraySet<>(); + final BroadcastConsumer replacedBroadcastConsumer = + (record, i) -> replacedBroadcasts.add(record); for (int i = 0; i < r.receivers.size(); i++) { final Object receiver = r.receivers.get(i); final BroadcastProcessQueue queue = getOrCreateProcessQueue( getReceiverProcessName(receiver), getReceiverUid(receiver)); - queue.enqueueOrReplaceBroadcast(r, i); + queue.enqueueOrReplaceBroadcast(r, i, replacedBroadcastConsumer); updateRunnableList(queue); enqueueUpdateRunningList(); } + // Skip any broadcasts that have been replaced by newer broadcasts with + // FLAG_RECEIVER_REPLACE_PENDING. + skipAndCancelReplacedBroadcasts(replacedBroadcasts); + // If nothing to dispatch, send any pending result immediately if (r.receivers.isEmpty()) { scheduleResultTo(r); @@ -662,6 +659,17 @@ class BroadcastQueueModernImpl extends BroadcastQueue { traceEnd(cookie); } + private void skipAndCancelReplacedBroadcasts(ArraySet<BroadcastRecord> replacedBroadcasts) { + for (int i = 0; i < replacedBroadcasts.size(); ++i) { + final BroadcastRecord r = replacedBroadcasts.valueAt(i); + r.resultCode = Activity.RESULT_CANCELED; + r.resultData = null; + r.resultExtras = null; + scheduleResultTo(r); + notifyFinishBroadcast(r); + } + } + private void applyDeliveryGroupPolicy(@NonNull BroadcastRecord r) { if (mService.shouldIgnoreDeliveryGroupPolicy(r.intent.getAction())) { return; diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index 24cf3d280063..37225d176920 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -907,6 +907,17 @@ final class BroadcastRecord extends Binder { return record.options == null ? null : record.options.getDeliveryGroupMatchingFilter(); } + /** + * Returns {@code true} if all the receivers are still waiting to receive the broadcast. + * Otherwise {@code false}. + */ + boolean allReceiversPending() { + // We could also count the number of receivers with deliver state DELIVERY_PENDING, but + // checking how many receivers have finished (either skipped or cancelled) and whether or + // not the dispatch has been started should be sufficient. + return (terminalCount == 0 && dispatchTime <= 0); + } + @Override public String toString() { if (mCachedToString == null) { diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java new file mode 100644 index 000000000000..ac479b2a63c4 --- /dev/null +++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceLoggingDecorator.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.appop; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + +import java.io.PrintWriter; + +/** + * Logging decorator for {@link AppOpsCheckingServiceInterface}. + */ +public class AppOpsCheckingServiceLoggingDecorator implements AppOpsCheckingServiceInterface { + private static final String LOG_TAG = + AppOpsCheckingServiceLoggingDecorator.class.getSimpleName(); + + @NonNull + private final AppOpsCheckingServiceInterface mService; + + public AppOpsCheckingServiceLoggingDecorator(@NonNull AppOpsCheckingServiceInterface service) { + mService = service; + } + + @Override + public SparseIntArray getNonDefaultUidModes(int uid) { + Log.i(LOG_TAG, "getNonDefaultUidModes(uid = " + uid + ")"); + return mService.getNonDefaultUidModes(uid); + } + + @Override + public int getUidMode(int uid, int op) { + Log.i(LOG_TAG, "getUidMode(uid = " + uid + ", op = " + op + ")"); + return mService.getUidMode(uid, op); + } + + @Override + public boolean setUidMode(int uid, int op, int mode) { + Log.i(LOG_TAG, "setUidMode(uid = " + uid + ", op = " + op + ", mode = " + mode + ")"); + return mService.setUidMode(uid, op, mode); + } + + @Override + public int getPackageMode(@NonNull String packageName, int op, int userId) { + Log.i(LOG_TAG, "getPackageMode(packageName = " + packageName + ", op = " + op + + ", userId = " + userId + ")"); + return mService.getPackageMode(packageName, op, userId); + } + + @Override + public void setPackageMode(@NonNull String packageName, int op, int mode, int userId) { + Log.i(LOG_TAG, "setPackageMode(packageName = " + packageName + ", op = " + op + ", mode = " + + mode + ", userId = " + userId + ")"); + mService.setPackageMode(packageName, op, mode, userId); + } + + @Override + public boolean removePackage(@NonNull String packageName, int userId) { + Log.i(LOG_TAG, "removePackage(packageName = " + packageName + ", userId = " + userId + ")"); + return mService.removePackage(packageName, userId); + } + + @Override + public void removeUid(int uid) { + Log.i(LOG_TAG, "removeUid(uid = " + uid + ")"); + mService.removeUid(uid); + } + + @Override + public boolean areUidModesDefault(int uid) { + Log.i(LOG_TAG, "areUidModesDefault(uid = " + uid + ")"); + return mService.areUidModesDefault(uid); + } + + @Override + public boolean arePackageModesDefault(String packageName, int userId) { + Log.i(LOG_TAG, "arePackageModesDefault(packageName = " + packageName + ", userId = " + + userId + ")"); + return mService.arePackageModesDefault(packageName, userId); + } + + @Override + public void clearAllModes() { + Log.i(LOG_TAG, "clearAllModes()"); + mService.clearAllModes(); + } + + @Override + public void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener, + int op) { + Log.i(LOG_TAG, "startWatchingOpModeChanged(changedListener = " + changedListener + ", op = " + + op + ")"); + mService.startWatchingOpModeChanged(changedListener, op); + } + + @Override + public void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener, + @NonNull String packageName) { + Log.i(LOG_TAG, "startWatchingPackageModeChanged(changedListener = " + changedListener + + ", packageName = " + packageName + ")"); + mService.startWatchingPackageModeChanged(changedListener, packageName); + } + + @Override + public void removeListener(@NonNull OnOpModeChangedListener changedListener) { + Log.i(LOG_TAG, "removeListener(changedListener = " + changedListener + ")"); + mService.removeListener(changedListener); + } + + @Override + public ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op) { + Log.i(LOG_TAG, "getOpModeChangedListeners(op = " + op + ")"); + return mService.getOpModeChangedListeners(op); + } + + @Override + public ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners( + @NonNull String packageName) { + Log.i(LOG_TAG, "getPackageModeChangedListeners(packageName = " + packageName + ")"); + return mService.getPackageModeChangedListeners(packageName); + } + + @Override + public void notifyWatchersOfChange(int op, int uid) { + Log.i(LOG_TAG, "notifyWatchersOfChange(op = " + op + ", uid = " + uid + ")"); + mService.notifyWatchersOfChange(op, uid); + } + + @Override + public void notifyOpChanged(@NonNull OnOpModeChangedListener changedListener, int op, int uid, + @Nullable String packageName) { + Log.i(LOG_TAG, "notifyOpChanged(changedListener = " + changedListener + ", op = " + op + + ", uid = " + uid + ", packageName = " + packageName + ")"); + mService.notifyOpChanged(changedListener, op, uid, packageName); + } + + @Override + public void notifyOpChangedForAllPkgsInUid(int op, int uid, boolean onlyForeground, + @Nullable OnOpModeChangedListener callbackToIgnore) { + Log.i(LOG_TAG, "notifyOpChangedForAllPkgsInUid(op = " + op + ", uid = " + uid + + ", onlyForeground = " + onlyForeground + ", callbackToIgnore = " + + callbackToIgnore + ")"); + mService.notifyOpChangedForAllPkgsInUid(op, uid, onlyForeground, callbackToIgnore); + } + + @Override + public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) { + Log.i(LOG_TAG, "evalForegroundUidOps(uid = " + uid + ", foregroundOps = " + foregroundOps + + ")"); + return mService.evalForegroundUidOps(uid, foregroundOps); + } + + @Override + public SparseBooleanArray evalForegroundPackageOps(String packageName, + SparseBooleanArray foregroundOps, int userId) { + Log.i(LOG_TAG, "evalForegroundPackageOps(packageName = " + packageName + + ", foregroundOps = " + foregroundOps + ", userId = " + userId + ")"); + return mService.evalForegroundPackageOps(packageName, foregroundOps, userId); + } + + @Override + public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, + PrintWriter printWriter) { + Log.i(LOG_TAG, "dumpListeners(dumpOp = " + dumpOp + ", dumpUid = " + dumpUid + + ", dumpPackage = " + dumpPackage + ", printWriter = " + printWriter + ")"); + return mService.dumpListeners(dumpOp, dumpUid, dumpPackage, printWriter); + } +} diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 18839a80640d..9c6cae355225 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -956,6 +956,8 @@ public class AppOpsService extends IAppOpsService.Stub implements PersistenceSch } mAppOpsCheckingService = new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps); + //mAppOpsCheckingService = new AppOpsCheckingServiceLoggingDecorator( + // LocalServices.getService(AppOpsCheckingServiceInterface.class)); mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler, mAppOpsCheckingService); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 418027fb37f6..9877ed39e5be 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -36,6 +36,7 @@ import android.media.BluetoothProfileConnectionInfo; import android.media.IAudioRoutesObserver; import android.media.ICapturePresetDevicesRoleDispatcher; import android.media.ICommunicationDeviceDispatcher; +import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.MediaMetrics; import android.media.audiopolicy.AudioProductStrategy; @@ -871,6 +872,16 @@ import java.util.concurrent.atomic.AtomicBoolean; return mDeviceInventory.removePreferredDevicesForStrategySync(strategy); } + /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy, + @NonNull AudioDeviceAttributes device) { + return mDeviceInventory.setDeviceAsNonDefaultForStrategySync(strategy, device); + } + + /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy, + @NonNull AudioDeviceAttributes device) { + return mDeviceInventory.removeDeviceAsNonDefaultForStrategySync(strategy, device); + } + /*package*/ void registerStrategyPreferredDevicesDispatcher( @NonNull IStrategyPreferredDevicesDispatcher dispatcher) { mDeviceInventory.registerStrategyPreferredDevicesDispatcher(dispatcher); @@ -881,6 +892,16 @@ import java.util.concurrent.atomic.AtomicBoolean; mDeviceInventory.unregisterStrategyPreferredDevicesDispatcher(dispatcher); } + /*package*/ void registerStrategyNonDefaultDevicesDispatcher( + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { + mDeviceInventory.registerStrategyNonDefaultDevicesDispatcher(dispatcher); + } + + /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher( + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { + mDeviceInventory.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher); + } + /*package*/ int setPreferredDevicesForCapturePresetSync(int capturePreset, @NonNull List<AudioDeviceAttributes> devices) { return mDeviceInventory.setPreferredDevicesForCapturePresetSync(capturePreset, devices); @@ -1039,6 +1060,17 @@ import java.util.concurrent.atomic.AtomicBoolean; sendIMsgNoDelay(MSG_I_SAVE_REMOVE_PREF_DEVICES_FOR_STRATEGY, SENDMSG_QUEUE, strategy); } + /*package*/ void postSaveSetDeviceAsNonDefaultForStrategy( + int strategy, AudioDeviceAttributes device) { + sendILMsgNoDelay(MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device); + } + + /*package*/ void postSaveRemoveDeviceAsNonDefaultForStrategy( + int strategy, AudioDeviceAttributes device) { + sendILMsgNoDelay( + MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY, SENDMSG_QUEUE, strategy, device); + } + /*package*/ void postSaveSetPreferredDevicesForCapturePreset( int capturePreset, List<AudioDeviceAttributes> devices) { sendILMsgNoDelay( @@ -1508,6 +1540,16 @@ import java.util.concurrent.atomic.AtomicBoolean; final int strategy = msg.arg1; mDeviceInventory.onSaveRemovePreferredDevices(strategy); } break; + case MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY: { + final int strategy = msg.arg1; + final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj; + mDeviceInventory.onSaveSetDeviceAsNonDefault(strategy, device); + } break; + case MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY: { + final int strategy = msg.arg1; + final AudioDeviceAttributes device = (AudioDeviceAttributes) msg.obj; + mDeviceInventory.onSaveRemoveDeviceAsNonDefault(strategy, device); + } break; case MSG_CHECK_MUTE_MUSIC: checkMessagesMuteMusic(0); break; @@ -1593,6 +1635,9 @@ import java.util.concurrent.atomic.AtomicBoolean; // process set volume for Le Audio, obj is BleVolumeInfo private static final int MSG_II_SET_LE_AUDIO_OUT_VOLUME = 46; + private static final int MSG_IL_SAVE_NDEF_DEVICE_FOR_STRATEGY = 47; + private static final int MSG_IL_SAVE_REMOVE_NDEF_DEVICE_FOR_STRATEGY = 48; + private static boolean isMessageHandledUnderWakelock(int msgId) { switch(msgId) { case MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE: diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 34457b09ce17..f9270c9b32bb 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -31,8 +31,11 @@ import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; import android.media.ICapturePresetDevicesRoleDispatcher; +import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.MediaMetrics; +import android.media.permission.ClearCallingIdentityContext; +import android.media.permission.SafeCloseable; import android.os.Binder; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -142,6 +145,10 @@ public class AudioDeviceInventory { private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevices = new ArrayMap<>(); + // List of non-default devices for strategies + private final ArrayMap<Integer, List<AudioDeviceAttributes>> mNonDefaultDevices = + new ArrayMap<>(); + // List of preferred devices of capture preset private final ArrayMap<Integer, List<AudioDeviceAttributes>> mPreferredDevicesForCapturePreset = new ArrayMap<>(); @@ -156,10 +163,14 @@ public class AudioDeviceInventory { final RemoteCallbackList<IAudioRoutesObserver> mRoutesObservers = new RemoteCallbackList<IAudioRoutesObserver>(); - // Monitoring of strategy-preferred device + // Monitoring of preferred device for strategies final RemoteCallbackList<IStrategyPreferredDevicesDispatcher> mPrefDevDispatchers = new RemoteCallbackList<IStrategyPreferredDevicesDispatcher>(); + // Monitoring of non-default device for strategies + final RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher> mNonDefDevDispatchers = + new RemoteCallbackList<IStrategyNonDefaultDevicesDispatcher>(); + // Monitoring of devices for role and capture preset final RemoteCallbackList<ICapturePresetDevicesRoleDispatcher> mDevRoleCapturePresetDispatchers = new RemoteCallbackList<ICapturePresetDevicesRoleDispatcher>(); @@ -254,6 +265,9 @@ public class AudioDeviceInventory { pw.println("\n" + prefix + "Preferred devices for strategy:"); mPreferredDevices.forEach((strategy, device) -> { pw.println(" " + prefix + "strategy:" + strategy + " device:" + device); }); + pw.println("\n" + prefix + "Non-default devices for strategy:"); + mNonDefaultDevices.forEach((strategy, device) -> { + pw.println(" " + prefix + "strategy:" + strategy + " device:" + device); }); pw.println("\n" + prefix + "Connected devices:"); mConnectedDevices.forEach((key, deviceInfo) -> { pw.println(" " + prefix + deviceInfo.toString()); }); @@ -291,6 +305,11 @@ public class AudioDeviceInventory { mAudioSystem.setDevicesRoleForStrategy( strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); }); } + synchronized (mNonDefaultDevices) { + mNonDefaultDevices.forEach((strategy, devices) -> { + mAudioSystem.setDevicesRoleForStrategy( + strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); }); + } synchronized (mPreferredDevicesForCapturePreset) { // TODO: call audiosystem to restore } @@ -608,6 +627,18 @@ public class AudioDeviceInventory { /*package*/ void onSaveSetPreferredDevices(int strategy, @NonNull List<AudioDeviceAttributes> devices) { mPreferredDevices.put(strategy, devices); + List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy); + if (nonDefaultDevices != null) { + nonDefaultDevices.removeAll(devices); + + if (nonDefaultDevices.isEmpty()) { + mNonDefaultDevices.remove(strategy); + } else { + mNonDefaultDevices.put(strategy, nonDefaultDevices); + } + dispatchNonDefaultDevice(strategy, nonDefaultDevices); + } + dispatchPreferredDevice(strategy, devices); } @@ -616,6 +647,40 @@ public class AudioDeviceInventory { dispatchPreferredDevice(strategy, new ArrayList<AudioDeviceAttributes>()); } + /*package*/ void onSaveSetDeviceAsNonDefault(int strategy, + @NonNull AudioDeviceAttributes device) { + List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy); + if (nonDefaultDevices == null) { + nonDefaultDevices = new ArrayList<>(); + } + + if (!nonDefaultDevices.contains(device)) { + nonDefaultDevices.add(device); + } + + mNonDefaultDevices.put(strategy, nonDefaultDevices); + dispatchNonDefaultDevice(strategy, nonDefaultDevices); + + List<AudioDeviceAttributes> preferredDevices = mPreferredDevices.get(strategy); + + if (preferredDevices != null) { + preferredDevices.remove(device); + mPreferredDevices.put(strategy, preferredDevices); + + dispatchPreferredDevice(strategy, preferredDevices); + } + } + + /*package*/ void onSaveRemoveDeviceAsNonDefault(int strategy, + @NonNull AudioDeviceAttributes device) { + List<AudioDeviceAttributes> nonDefaultDevices = mNonDefaultDevices.get(strategy); + if (nonDefaultDevices != null) { + nonDefaultDevices.remove(device); + mNonDefaultDevices.put(strategy, nonDefaultDevices); + dispatchNonDefaultDevice(strategy, nonDefaultDevices); + } + } + /*package*/ void onSaveSetPreferredDevicesForCapturePreset( int capturePreset, @NonNull List<AudioDeviceAttributes> devices) { mPreferredDevicesForCapturePreset.put(capturePreset, devices); @@ -631,18 +696,19 @@ public class AudioDeviceInventory { } //------------------------------------------------------------ - // preferred device(s) + // preferred/non-default device(s) /*package*/ int setPreferredDevicesForStrategySync(int strategy, @NonNull List<AudioDeviceAttributes> devices) { - final long identity = Binder.clearCallingIdentity(); + int status = AudioSystem.ERROR; - AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( - "setPreferredDevicesForStrategySync, strategy: " + strategy - + " devices: " + devices)).printLog(TAG)); - final int status = mAudioSystem.setDevicesRoleForStrategy( - strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); - Binder.restoreCallingIdentity(identity); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( + "setPreferredDevicesForStrategySync, strategy: " + strategy + + " devices: " + devices)).printLog(TAG)); + status = mAudioSystem.setDevicesRoleForStrategy( + strategy, AudioSystem.DEVICE_ROLE_PREFERRED, devices); + } if (status == AudioSystem.SUCCESS) { mDeviceBroker.postSaveSetPreferredDevicesForStrategy(strategy, devices); @@ -651,15 +717,16 @@ public class AudioDeviceInventory { } /*package*/ int removePreferredDevicesForStrategySync(int strategy) { - final long identity = Binder.clearCallingIdentity(); + int status = AudioSystem.ERROR; - AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( - "removePreferredDevicesForStrategySync, strategy: " - + strategy)).printLog(TAG)); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( + "removePreferredDevicesForStrategySync, strategy: " + + strategy)).printLog(TAG)); - final int status = mAudioSystem.removeDevicesRoleForStrategy( - strategy, AudioSystem.DEVICE_ROLE_PREFERRED); - Binder.restoreCallingIdentity(identity); + status = mAudioSystem.clearDevicesRoleForStrategy( + strategy, AudioSystem.DEVICE_ROLE_PREFERRED); + } if (status == AudioSystem.SUCCESS) { mDeviceBroker.postSaveRemovePreferredDevicesForStrategy(strategy); @@ -667,6 +734,50 @@ public class AudioDeviceInventory { return status; } + /*package*/ int setDeviceAsNonDefaultForStrategySync(int strategy, + @NonNull AudioDeviceAttributes device) { + int status = AudioSystem.ERROR; + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + List<AudioDeviceAttributes> devices = new ArrayList<>(); + devices.add(device); + + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( + "setDeviceAsNonDefaultForStrategySync, strategy: " + strategy + + " device: " + device)).printLog(TAG)); + status = mAudioSystem.setDevicesRoleForStrategy( + strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); + } + + if (status == AudioSystem.SUCCESS) { + mDeviceBroker.postSaveSetDeviceAsNonDefaultForStrategy(strategy, device); + } + return status; + } + + /*package*/ int removeDeviceAsNonDefaultForStrategySync(int strategy, + @NonNull AudioDeviceAttributes device) { + int status = AudioSystem.ERROR; + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + List<AudioDeviceAttributes> devices = new ArrayList<>(); + devices.add(device); + + AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent( + "removeDeviceAsNonDefaultForStrategySync, strategy: " + + strategy + " devices: " + device)).printLog(TAG)); + + status = mAudioSystem.removeDevicesRoleForStrategy( + strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); + } + + if (status == AudioSystem.SUCCESS) { + mDeviceBroker.postSaveRemoveDeviceAsNonDefaultForStrategy(strategy, device); + } + return status; + } + + /*package*/ void registerStrategyPreferredDevicesDispatcher( @NonNull IStrategyPreferredDevicesDispatcher dispatcher) { mPrefDevDispatchers.register(dispatcher); @@ -677,12 +788,24 @@ public class AudioDeviceInventory { mPrefDevDispatchers.unregister(dispatcher); } + /*package*/ void registerStrategyNonDefaultDevicesDispatcher( + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { + mNonDefDevDispatchers.register(dispatcher); + } + + /*package*/ void unregisterStrategyNonDefaultDevicesDispatcher( + @NonNull IStrategyNonDefaultDevicesDispatcher dispatcher) { + mNonDefDevDispatchers.unregister(dispatcher); + } + /*package*/ int setPreferredDevicesForCapturePresetSync( int capturePreset, @NonNull List<AudioDeviceAttributes> devices) { - final long identity = Binder.clearCallingIdentity(); - final int status = mAudioSystem.setDevicesRoleForCapturePreset( - capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); - Binder.restoreCallingIdentity(identity); + int status = AudioSystem.ERROR; + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + status = mAudioSystem.setDevicesRoleForCapturePreset( + capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED, devices); + } if (status == AudioSystem.SUCCESS) { mDeviceBroker.postSaveSetPreferredDevicesForCapturePreset(capturePreset, devices); @@ -691,10 +814,12 @@ public class AudioDeviceInventory { } /*package*/ int clearPreferredDevicesForCapturePresetSync(int capturePreset) { - final long identity = Binder.clearCallingIdentity(); - final int status = mAudioSystem.clearDevicesRoleForCapturePreset( - capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED); - Binder.restoreCallingIdentity(identity); + int status = AudioSystem.ERROR; + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + status = mAudioSystem.clearDevicesRoleForCapturePreset( + capturePreset, AudioSystem.DEVICE_ROLE_PREFERRED); + } if (status == AudioSystem.SUCCESS) { mDeviceBroker.postSaveClearPreferredDevicesForCapturePreset(capturePreset); @@ -1523,6 +1648,19 @@ public class AudioDeviceInventory { mPrefDevDispatchers.finishBroadcast(); } + private void dispatchNonDefaultDevice(int strategy, + @NonNull List<AudioDeviceAttributes> devices) { + final int nbDispatchers = mNonDefDevDispatchers.beginBroadcast(); + for (int i = 0; i < nbDispatchers; i++) { + try { + mNonDefDevDispatchers.getBroadcastItem(i).dispatchNonDefDevicesChanged( + strategy, devices); + } catch (RemoteException e) { + } + } + mNonDefDevDispatchers.finishBroadcast(); + } + private void dispatchDevicesRoleForCapturePreset( int capturePreset, int role, @NonNull List<AudioDeviceAttributes> devices) { final int nbDispatchers = mDevRoleCapturePresetDispatchers.beginBroadcast(); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 43c8032db546..24c7d2c4914e 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -116,6 +116,7 @@ import android.media.ISpatializerHeadToSoundStagePoseCallback; import android.media.ISpatializerHeadTrackerAvailableCallback; import android.media.ISpatializerHeadTrackingModeCallback; import android.media.ISpatializerOutputCallback; +import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.IVolumeController; import android.media.MediaMetrics; @@ -2800,11 +2801,12 @@ public class AudioService extends IAudioService.Stub * @see AudioManager#setPreferredDevicesForStrategy(AudioProductStrategy, * List<AudioDeviceAttributes>) */ + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public int setPreferredDevicesForStrategy(int strategy, List<AudioDeviceAttributes> devices) { + super.setPreferredDevicesForStrategy_enforcePermission(); if (devices == null) { return AudioSystem.ERROR; } - enforceModifyAudioRoutingPermission(); final String logString = String.format( "setPreferredDeviceForStrategy u/pid:%d/%d strat:%d dev:%s", Binder.getCallingUid(), Binder.getCallingPid(), strategy, @@ -2862,6 +2864,81 @@ public class AudioService extends IAudioService.Stub } } + /** + * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy, + * AudioDeviceAttributes) + * @see AudioManager#setDeviceAsNonDefaultForStrategy(AudioProductStrategy, + * List<AudioDeviceAttributes>) + */ + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public int setDeviceAsNonDefaultForStrategy(int strategy, + @NonNull AudioDeviceAttributes device) { + super.setDeviceAsNonDefaultForStrategy_enforcePermission(); + Objects.requireNonNull(device); + final String logString = String.format( + "setDeviceAsNonDefaultForStrategy u/pid:%d/%d strat:%d dev:%s", + Binder.getCallingUid(), Binder.getCallingPid(), strategy, device.toString()); + sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); + if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) { + Log.e(TAG, "Unsupported input routing in " + logString); + return AudioSystem.ERROR; + } + + final int status = mDeviceBroker.setDeviceAsNonDefaultForStrategySync(strategy, device); + if (status != AudioSystem.SUCCESS) { + Log.e(TAG, String.format("Error %d in %s)", status, logString)); + } + + return status; + } + + /** + * @see AudioManager#removeDeviceAsNonDefaultForStrategy(AudioProductStrategy, + * AudioDeviceAttributes) + */ + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public int removeDeviceAsNonDefaultForStrategy(int strategy, + AudioDeviceAttributes device) { + super.removeDeviceAsNonDefaultForStrategy_enforcePermission(); + Objects.requireNonNull(device); + final String logString = String.format( + "removeDeviceAsNonDefaultForStrategy strat:%d dev:%s", strategy, device.toString()); + sDeviceLogger.enqueue(new EventLogger.StringEvent(logString).printLog(TAG)); + if (device.getRole() == AudioDeviceAttributes.ROLE_INPUT) { + Log.e(TAG, "Unsupported input routing in " + logString); + return AudioSystem.ERROR; + } + + final int status = mDeviceBroker.removeDeviceAsNonDefaultForStrategySync(strategy, device); + if (status != AudioSystem.SUCCESS) { + Log.e(TAG, String.format("Error %d in %s)", status, logString)); + } + return status; + } + + /** + * @see AudioManager#getNonDefaultDevicesForStrategy(AudioProductStrategy) + */ + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public List<AudioDeviceAttributes> getNonDefaultDevicesForStrategy(int strategy) { + super.getNonDefaultDevicesForStrategy_enforcePermission(); + List<AudioDeviceAttributes> devices = new ArrayList<>(); + int status = AudioSystem.ERROR; + + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + status = AudioSystem.getDevicesForRoleAndStrategy( + strategy, AudioSystem.DEVICE_ROLE_DISABLED, devices); + } + + if (status != AudioSystem.SUCCESS) { + Log.e(TAG, String.format("Error %d in getNonDefaultDeviceForStrategy(%d)", + status, strategy)); + return new ArrayList<AudioDeviceAttributes>(); + } else { + return devices; + } + } + /** @see AudioManager#addOnPreferredDevicesForStrategyChangedListener( * Executor, AudioManager.OnPreferredDevicesForStrategyChangedListener) */ @@ -2886,6 +2963,30 @@ public class AudioService extends IAudioService.Stub mDeviceBroker.unregisterStrategyPreferredDevicesDispatcher(dispatcher); } + /** @see AudioManager#addOnNonDefaultDevicesForStrategyChangedListener( + * Executor, AudioManager.OnNonDefaultDevicesForStrategyChangedListener) + */ + public void registerStrategyNonDefaultDevicesDispatcher( + @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + enforceModifyAudioRoutingPermission(); + mDeviceBroker.registerStrategyNonDefaultDevicesDispatcher(dispatcher); + } + + /** @see AudioManager#removeOnNonDefaultDevicesForStrategyChangedListener( + * AudioManager.OnNonDefaultDevicesForStrategyChangedListener) + */ + public void unregisterStrategyNonDefaultDevicesDispatcher( + @Nullable IStrategyNonDefaultDevicesDispatcher dispatcher) { + if (dispatcher == null) { + return; + } + enforceModifyAudioRoutingPermission(); + mDeviceBroker.unregisterStrategyNonDefaultDevicesDispatcher(dispatcher); + } + /** * @see AudioManager#setPreferredDeviceForCapturePreset(int, AudioDeviceAttributes) */ diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index c176f29c3dff..7fefc556a02f 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -279,14 +279,27 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** - * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int)} + * Same as {@link AudioSystem#removeDevicesRoleForStrategy(int, int, List)} + * @param strategy + * @param role + * @param devices + * @return + */ + public int removeDevicesRoleForStrategy(int strategy, int role, + @NonNull List<AudioDeviceAttributes> devices) { + invalidateRoutingCache(); + return AudioSystem.removeDevicesRoleForStrategy(strategy, role, devices); + } + + /** + * Same as {@link AudioSystem#clearDevicesRoleForStrategy(int, int)} * @param strategy * @param role * @return */ - public int removeDevicesRoleForStrategy(int strategy, int role) { + public int clearDevicesRoleForStrategy(int strategy, int role) { invalidateRoutingCache(); - return AudioSystem.removeDevicesRoleForStrategy(strategy, role); + return AudioSystem.clearDevicesRoleForStrategy(strategy, role); } /** diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java index 3c0fda863cdc..c0a238feb9b5 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java @@ -307,6 +307,9 @@ final class ConversionUtils { static ProgramIdentifier identifierToHalProgramIdentifier(ProgramSelector.Identifier id) { ProgramIdentifier hwId = new ProgramIdentifier(); hwId.type = id.getType(); + if (hwId.type == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) { + hwId.type = IdentifierType.DAB_SID_EXT; + } hwId.value = id.getValue(); return hwId; } @@ -317,9 +320,49 @@ final class ConversionUtils { if (id.type == IdentifierType.INVALID) { return null; } - return new ProgramSelector.Identifier(id.type, id.value); + int idType; + if (id.type == IdentifierType.DAB_SID_EXT) { + idType = ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT; + } else { + idType = id.type; + } + return new ProgramSelector.Identifier(idType, id.value); } + private static boolean isVendorIdentifierType(int idType) { + return idType >= IdentifierType.VENDOR_START && idType <= IdentifierType.VENDOR_END; + } + + private static boolean isValidHalProgramSelector( + android.hardware.broadcastradio.ProgramSelector sel) { + if (sel.primaryId.type != IdentifierType.AMFM_FREQUENCY_KHZ + && sel.primaryId.type != IdentifierType.RDS_PI + && sel.primaryId.type != IdentifierType.HD_STATION_ID_EXT + && sel.primaryId.type != IdentifierType.DAB_SID_EXT + && sel.primaryId.type != IdentifierType.DRMO_SERVICE_ID + && sel.primaryId.type != IdentifierType.SXM_SERVICE_ID + && !isVendorIdentifierType(sel.primaryId.type)) { + return false; + } + if (sel.primaryId.type == IdentifierType.DAB_SID_EXT) { + boolean hasEnsemble = false; + boolean hasFrequency = false; + for (int i = 0; i < sel.secondaryIds.length; i++) { + if (sel.secondaryIds[i].type == IdentifierType.DAB_ENSEMBLE) { + hasEnsemble = true; + } else if (sel.secondaryIds[i].type == IdentifierType.DAB_FREQUENCY_KHZ) { + hasFrequency = true; + } + if (hasEnsemble && hasFrequency) { + return true; + } + } + return false; + } + return true; + } + + @Nullable static android.hardware.broadcastradio.ProgramSelector programSelectorToHalProgramSelector( ProgramSelector sel) { android.hardware.broadcastradio.ProgramSelector hwSel = @@ -332,6 +375,9 @@ final class ConversionUtils { secondaryIdList.add(identifierToHalProgramIdentifier(secondaryIds[i])); } hwSel.secondaryIds = secondaryIdList.toArray(ProgramIdentifier[]::new); + if (!isValidHalProgramSelector(hwSel)) { + return null; + } return hwSel; } @@ -344,7 +390,7 @@ final class ConversionUtils { @Nullable static ProgramSelector programSelectorFromHalProgramSelector( android.hardware.broadcastradio.ProgramSelector sel) { - if (isEmpty(sel)) { + if (isEmpty(sel) || !isValidHalProgramSelector(sel)) { return null; } @@ -432,7 +478,34 @@ final class ConversionUtils { return builder.build(); } + private static boolean isValidLogicallyTunedTo(ProgramIdentifier id) { + return id.type == IdentifierType.AMFM_FREQUENCY_KHZ || id.type == IdentifierType.RDS_PI + || id.type == IdentifierType.HD_STATION_ID_EXT + || id.type == IdentifierType.DAB_SID_EXT + || id.type == IdentifierType.DRMO_SERVICE_ID + || id.type == IdentifierType.SXM_SERVICE_ID + || isVendorIdentifierType(id.type); + } + + private static boolean isValidPhysicallyTunedTo(ProgramIdentifier id) { + return id.type == IdentifierType.AMFM_FREQUENCY_KHZ + || id.type == IdentifierType.DAB_FREQUENCY_KHZ + || id.type == IdentifierType.DRMO_FREQUENCY_KHZ + || id.type == IdentifierType.SXM_CHANNEL + || isVendorIdentifierType(id.type); + } + + private static boolean isValidHalProgramInfo(ProgramInfo info) { + return isValidHalProgramSelector(info.selector) + && isValidLogicallyTunedTo(info.logicallyTunedTo) + && isValidPhysicallyTunedTo(info.physicallyTunedTo); + } + + @Nullable static RadioManager.ProgramInfo programInfoFromHalProgramInfo(ProgramInfo info) { + if (!isValidHalProgramInfo(info)) { + return null; + } Collection<ProgramSelector.Identifier> relatedContent = new ArrayList<>(); if (info.relatedContent != null) { for (int i = 0; i < info.relatedContent.length; i++) { @@ -485,7 +558,14 @@ final class ConversionUtils { static ProgramList.Chunk chunkFromHalProgramListChunk(ProgramListChunk chunk) { Set<RadioManager.ProgramInfo> modified = new ArraySet<>(chunk.modified.length); for (int i = 0; i < chunk.modified.length; i++) { - modified.add(programInfoFromHalProgramInfo(chunk.modified[i])); + RadioManager.ProgramInfo modifiedInfo = + programInfoFromHalProgramInfo(chunk.modified[i]); + if (modifiedInfo == null) { + Slogf.w(TAG, "Program info %s in program list chunk is not valid", + chunk.modified[i]); + continue; + } + modified.add(modifiedInfo); } Set<ProgramSelector.Identifier> removed = new ArraySet<>(); if (chunk.removed != null) { @@ -547,10 +627,22 @@ final class ConversionUtils { if (isAtLeastU(targetSdkVersion)) { return chunk; } - Set<RadioManager.ProgramInfo> modified = chunk.getModified(); - modified.removeIf(info -> !programInfoMeetsSdkVersionRequirement(info, targetSdkVersion)); - Set<ProgramSelector.Identifier> removed = chunk.getRemoved(); - removed.removeIf(id -> isNewIdentifierInU(id)); + Set<RadioManager.ProgramInfo> modified = new ArraySet<>(); + Iterator<RadioManager.ProgramInfo> modifiedIterator = chunk.getModified().iterator(); + while (modifiedIterator.hasNext()) { + RadioManager.ProgramInfo info = modifiedIterator.next(); + if (programInfoMeetsSdkVersionRequirement(info, targetSdkVersion)) { + modified.add(info); + } + } + Set<ProgramSelector.Identifier> removed = new ArraySet<>(); + Iterator<ProgramSelector.Identifier> removedIterator = chunk.getRemoved().iterator(); + while (removedIterator.hasNext()) { + ProgramSelector.Identifier id = removedIterator.next(); + if (!isNewIdentifierInU(id)) { + removed.add(id); + } + } return new ProgramList.Chunk(chunk.isPurge(), chunk.isComplete(), modified, removed); } @@ -558,7 +650,7 @@ final class ConversionUtils { Announcement hwAnnouncement) { return new android.hardware.radio.Announcement( Objects.requireNonNull(programSelectorFromHalProgramSelector( - hwAnnouncement.selector)), + hwAnnouncement.selector), "Program selector can not be null"), hwAnnouncement.type, vendorInfoFromHalVendorKeyValues(hwAnnouncement.vendorInfo) ); diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java index 095a5fa1cf30..39b13547cf93 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/ProgramInfoCache.java @@ -24,6 +24,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.utils.Slogf; import java.util.ArrayList; import java.util.Collection; @@ -37,6 +38,7 @@ import java.util.Set; */ final class ProgramInfoCache { + private static final String TAG = "BcRadioAidlSrv.cache"; /** * Maximum number of {@link RadioManager#ProgramInfo} elements that will be put into a * ProgramList.Chunk.mModified array. Used to try to ensure a single ProgramList.Chunk @@ -124,6 +126,10 @@ final class ProgramInfoCache { for (int i = 0; i < chunk.modified.length; i++) { RadioManager.ProgramInfo programInfo = ConversionUtils.programInfoFromHalProgramInfo(chunk.modified[i]); + if (programInfo == null) { + Slogf.e(TAG, "Program info in program info %s in chunk is not valid", + chunk.modified[i]); + } mProgramInfoMap.put(programInfo.getSelector().getPrimaryId(), programInfo); } if (chunk.removed != null) { diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java index 6193f234730d..f8c19ae2b8f1 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java @@ -97,10 +97,10 @@ final class RadioModule { public void onTuneFailed(int result, ProgramSelector programSelector) { fireLater(() -> { + android.hardware.radio.ProgramSelector csel = + ConversionUtils.programSelectorFromHalProgramSelector(programSelector); + int tunerResult = ConversionUtils.halResultToTunerResult(result); synchronized (mLock) { - android.hardware.radio.ProgramSelector csel = - ConversionUtils.programSelectorFromHalProgramSelector(programSelector); - int tunerResult = ConversionUtils.halResultToTunerResult(result); fanoutAidlCallbackLocked((cb, sdkVersion) -> { if (csel != null && !ConversionUtils .programSelectorMeetsSdkVersionRequirement(csel, sdkVersion)) { @@ -117,10 +117,12 @@ final class RadioModule { @Override public void onCurrentProgramInfoChanged(ProgramInfo halProgramInfo) { fireLater(() -> { + RadioManager.ProgramInfo currentProgramInfo = + ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo); + Objects.requireNonNull(currentProgramInfo, + "Program info from AIDL HAL is invalid"); synchronized (mLock) { - mCurrentProgramInfo = - ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo); - RadioManager.ProgramInfo currentProgramInfo = mCurrentProgramInfo; + mCurrentProgramInfo = currentProgramInfo; fanoutAidlCallbackLocked((cb, sdkVersion) -> { if (!ConversionUtils.programInfoMeetsSdkVersionRequirement( currentProgramInfo, sdkVersion)) { diff --git a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java index d700ed01b3c3..fe8c23845d2a 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java @@ -203,10 +203,15 @@ final class TunerSession extends ITuner.Stub { Slogf.w(TAG, "Cannot tune on AIDL HAL client from non-current user"); return; } + android.hardware.broadcastradio.ProgramSelector hwSel = + ConversionUtils.programSelectorToHalProgramSelector(selector); + if (hwSel == null) { + throw new IllegalArgumentException("tune: INVALID_ARGUMENTS for program selector"); + } synchronized (mLock) { checkNotClosedLocked(); try { - mService.tune(ConversionUtils.programSelectorToHalProgramSelector(selector)); + mService.tune(hwSel); } catch (RuntimeException ex) { throw ConversionUtils.throwOnError(ex, /* action= */ "tune"); } diff --git a/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java new file mode 100644 index 000000000000..11a429481edb --- /dev/null +++ b/services/core/java/com/android/server/grammaticalinflection/GrammaticalInflectionService.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.grammaticalinflection; + +import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; + +import android.app.IGrammaticalInflectionManager; +import android.content.Context; +import android.os.IBinder; + +import com.android.server.LocalServices; +import com.android.server.SystemService; +import com.android.server.wm.ActivityTaskManagerInternal; + +/** + * The implementation of IGrammaticalInflectionManager.aidl. + * + * <p>This service is API entry point for storing app-specific grammatical inflection. + */ +public class GrammaticalInflectionService extends SystemService { + + private final ActivityTaskManagerInternal mActivityTaskManagerInternal; + + /** + * Initializes the system service. + * <p> + * Subclasses must define a single argument constructor that accepts the context + * and passes it to super. + * </p> + * + * @param context The system server context. + * + * @hide + */ + public GrammaticalInflectionService(Context context) { + super(context); + mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); + } + + @Override + public void onStart() { + publishBinderService(Context.GRAMMATICAL_INFLECTION_SERVICE, mService); + } + + private final IBinder mService = new IGrammaticalInflectionManager.Stub() { + @Override + public void setRequestedApplicationGrammaticalGender( + String appPackageName, int userId, int gender) { + GrammaticalInflectionService.this.setRequestedApplicationGrammaticalGender( + appPackageName, userId, gender); + } + }; + + private void setRequestedApplicationGrammaticalGender( + String appPackageName, int userId, int gender) { + final ActivityTaskManagerInternal.PackageConfigurationUpdater updater = + mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName, + userId); + + updater.setGrammaticalGender(gender).commit(); + } +} diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index d76da8326335..b8eb901b1b7d 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -385,8 +385,10 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { R.styleable.KeyboardLayout_keyboardLayout, 0); String languageTags = a.getString( - R.styleable.KeyboardLayout_locale); + R.styleable.KeyboardLayout_keyboardLocale); LocaleList locales = getLocalesFromLanguageTags(languageTags); + int layoutType = a.getInt(R.styleable.KeyboardLayout_keyboardLayoutType, + 0); int vid = a.getInt( R.styleable.KeyboardLayout_vendorId, -1); int pid = a.getInt( @@ -403,7 +405,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { if (keyboardName == null || name.equals(keyboardName)) { KeyboardLayout layout = new KeyboardLayout( descriptor, label, collection, priority, - locales, vid, pid); + locales, layoutType, vid, pid); visitor.visitKeyboardLayout( resources, keyboardLayoutResId, layout); } diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 89bc495a67e3..121b7c87223b 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -173,7 +173,6 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Random; import java.util.Set; import java.util.StringJoiner; import java.util.concurrent.CountDownLatch; @@ -234,7 +233,6 @@ public class LockSettingsService extends ILockSettings.Stub { private final SynchronizedStrongAuthTracker mStrongAuthTracker; private final BiometricDeferredQueue mBiometricDeferredQueue; private final LongSparseArray<byte[]> mGatekeeperPasswords; - private final Random mRandom; private final NotificationManager mNotificationManager; protected final UserManager mUserManager; @@ -348,23 +346,17 @@ public class LockSettingsService extends ILockSettings.Stub { } private LockscreenCredential generateRandomProfilePassword() { - byte[] randomLockSeed = new byte[] {}; - try { - randomLockSeed = SecureRandom.getInstance("SHA1PRNG").generateSeed(40); - char[] newPasswordChars = HexEncoding.encode(randomLockSeed); - byte[] newPassword = new byte[newPasswordChars.length]; - for (int i = 0; i < newPasswordChars.length; i++) { - newPassword[i] = (byte) newPasswordChars[i]; - } - LockscreenCredential credential = - LockscreenCredential.createManagedPassword(newPassword); - Arrays.fill(newPasswordChars, '\u0000'); - Arrays.fill(newPassword, (byte) 0); - Arrays.fill(randomLockSeed, (byte) 0); - return credential; - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("Fail to generate profile password", e); - } + byte[] randomLockSeed = SecureRandomUtils.randomBytes(40); + char[] newPasswordChars = HexEncoding.encode(randomLockSeed); + byte[] newPassword = new byte[newPasswordChars.length]; + for (int i = 0; i < newPasswordChars.length; i++) { + newPassword[i] = (byte) newPasswordChars[i]; + } + LockscreenCredential credential = LockscreenCredential.createManagedPassword(newPassword); + Arrays.fill(newPasswordChars, '\u0000'); + Arrays.fill(newPassword, (byte) 0); + Arrays.fill(randomLockSeed, (byte) 0); + return credential; } /** @@ -597,7 +589,6 @@ public class LockSettingsService extends ILockSettings.Stub { mStrongAuthTracker = injector.getStrongAuthTracker(); mStrongAuthTracker.register(mStrongAuth); mGatekeeperPasswords = new LongSparseArray<>(); - mRandom = new SecureRandom(); mSpManager = injector.getSyntheticPasswordManager(mStorage); mManagedProfilePasswordCache = injector.getManagedProfilePasswordCache(mJavaKeyStore); @@ -1752,14 +1743,9 @@ public class LockSettingsService extends ILockSettings.Stub { private String getSalt(int userId) { long salt = getLong(LockPatternUtils.LOCK_PASSWORD_SALT_KEY, 0, userId); if (salt == 0) { - try { - salt = SecureRandom.getInstance("SHA1PRNG").nextLong(); - setLong(LockPatternUtils.LOCK_PASSWORD_SALT_KEY, salt, userId); - Slog.v(TAG, "Initialized lock password salt for user: " + userId); - } catch (NoSuchAlgorithmException e) { - // Throw an exception rather than storing a password we'll never be able to recover - throw new IllegalStateException("Couldn't get SecureRandom number", e); - } + salt = SecureRandomUtils.randomLong(); + setLong(LockPatternUtils.LOCK_PASSWORD_SALT_KEY, salt, userId); + Slog.v(TAG, "Initialized lock password salt for user: " + userId); } return Long.toHexString(salt); } @@ -2644,7 +2630,7 @@ public class LockSettingsService extends ILockSettings.Stub { synchronized (mGatekeeperPasswords) { while (handle == 0L || mGatekeeperPasswords.get(handle) != null) { - handle = mRandom.nextLong(); + handle = SecureRandomUtils.randomLong(); } mGatekeeperPasswords.put(handle, gatekeeperPassword); } diff --git a/services/core/java/com/android/server/locksettings/SecureRandomUtils.java b/services/core/java/com/android/server/locksettings/SecureRandomUtils.java new file mode 100644 index 000000000000..4ba4dd052966 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/SecureRandomUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings; + +import java.security.SecureRandom; + +/** Utilities using a static SecureRandom */ +public class SecureRandomUtils { + private static final SecureRandom RNG = new SecureRandom(); + + /** Use SecureRandom to generate `length` random bytes */ + public static byte[] randomBytes(int length) { + byte[] res = new byte[length]; + RNG.nextBytes(res); + return res; + } + + /** Use SecureRandom to generate a random long */ + public static long randomLong() { + return RNG.nextLong(); + } +} diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index ad2fa222a960..cd972dcd461c 100644 --- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java @@ -59,8 +59,6 @@ import libcore.util.HexEncoding; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -284,8 +282,10 @@ class SyntheticPasswordManager { */ static SyntheticPassword create() { SyntheticPassword result = new SyntheticPassword(SYNTHETIC_PASSWORD_VERSION_V3); - byte[] escrowSplit0 = secureRandom(SYNTHETIC_PASSWORD_SECURITY_STRENGTH); - byte[] escrowSplit1 = secureRandom(SYNTHETIC_PASSWORD_SECURITY_STRENGTH); + byte[] escrowSplit0 = + SecureRandomUtils.randomBytes(SYNTHETIC_PASSWORD_SECURITY_STRENGTH); + byte[] escrowSplit1 = + SecureRandomUtils.randomBytes(SYNTHETIC_PASSWORD_SECURITY_STRENGTH); result.recreate(escrowSplit0, escrowSplit1); byte[] encrypteEscrowSplit0 = SyntheticPasswordCrypto.encrypt(result.mSyntheticPassword, PERSONALIZATION_E0, escrowSplit0); @@ -347,7 +347,7 @@ class SyntheticPasswordManager { result.scryptLogR = PASSWORD_SCRYPT_LOG_R; result.scryptLogP = PASSWORD_SCRYPT_LOG_P; result.credentialType = credentialType; - result.salt = secureRandom(PASSWORD_SALT_LENGTH); + result.salt = SecureRandomUtils.randomBytes(PASSWORD_SALT_LENGTH); return result; } @@ -490,7 +490,7 @@ class SyntheticPasswordManager { android.hardware.weaver.V1_0.IWeaver hidlWeaver = getWeaverHidlService(); if (hidlWeaver != null) { Slog.i(TAG, "Using HIDL weaver service"); - return new WeaverHidlWrapper(hidlWeaver); + return new WeaverHidlAdapter(hidlWeaver); } } catch (RemoteException e) { Slog.w(TAG, "Failed to get HIDL weaver service.", e); @@ -552,7 +552,7 @@ class SyntheticPasswordManager { throw new IllegalArgumentException("Invalid key size for weaver"); } if (value == null) { - value = secureRandom(mWeaverConfig.valueSize); + value = SecureRandomUtils.randomBytes(mWeaverConfig.valueSize); } try { mWeaver.write(slot, key, value); @@ -1039,9 +1039,9 @@ class SyntheticPasswordManager { } TokenData tokenData = new TokenData(); tokenData.mType = type; - final byte[] secdiscardable = secureRandom(SECDISCARDABLE_LENGTH); + final byte[] secdiscardable = SecureRandomUtils.randomBytes(SECDISCARDABLE_LENGTH); if (isWeaverAvailable()) { - tokenData.weaverSecret = secureRandom(mWeaverConfig.valueSize); + tokenData.weaverSecret = SecureRandomUtils.randomBytes(mWeaverConfig.valueSize); tokenData.secdiscardableOnDisk = SyntheticPasswordCrypto.encrypt(tokenData.weaverSecret, PERSONALIZATION_WEAVER_TOKEN, secdiscardable); } else { @@ -1510,7 +1510,7 @@ class SyntheticPasswordManager { * been created. */ private byte[] createSecdiscardable(long protectorId, int userId) { - byte[] data = secureRandom(SECDISCARDABLE_LENGTH); + byte[] data = SecureRandomUtils.randomBytes(SECDISCARDABLE_LENGTH); saveSecdiscardable(protectorId, data, userId); return data; } @@ -1624,12 +1624,12 @@ class SyntheticPasswordManager { } private static long generateProtectorId() { - SecureRandom rng = new SecureRandom(); - long result; - do { - result = rng.nextLong(); - } while (result == NULL_PROTECTOR_ID); - return result; + while (true) { + final long result = SecureRandomUtils.randomLong(); + if (result != NULL_PROTECTOR_ID) { + return result; + } + } } @VisibleForTesting @@ -1637,15 +1637,6 @@ class SyntheticPasswordManager { return 100000 + userId; } - protected static byte[] secureRandom(int length) { - try { - return SecureRandom.getInstance("SHA1PRNG").generateSeed(length); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - return null; - } - } - private String getProtectorKeyAlias(long protectorId) { return TextUtils.formatSimple("%s%x", PROTECTOR_KEY_ALIAS_PREFIX, protectorId); } diff --git a/services/core/java/com/android/server/locksettings/WeaverHidlAdapter.java b/services/core/java/com/android/server/locksettings/WeaverHidlAdapter.java new file mode 100644 index 000000000000..2e9c3fd11461 --- /dev/null +++ b/services/core/java/com/android/server/locksettings/WeaverHidlAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.locksettings; + +import android.hardware.weaver.IWeaver; +import android.hardware.weaver.WeaverConfig; +import android.hardware.weaver.WeaverReadResponse; +import android.hardware.weaver.WeaverReadStatus; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Slog; + +import java.util.ArrayList; + +/** + * Adapt the legacy HIDL interface to present the AIDL interface. + */ +class WeaverHidlAdapter implements IWeaver { + private static final String TAG = "WeaverHidlAdapter"; + private final android.hardware.weaver.V1_0.IWeaver mImpl; + + WeaverHidlAdapter(android.hardware.weaver.V1_0.IWeaver impl) { + mImpl = impl; + } + + @Override + public WeaverConfig getConfig() throws RemoteException { + final WeaverConfig[] res = new WeaverConfig[1]; + mImpl.getConfig((int status, android.hardware.weaver.V1_0.WeaverConfig config) -> { + if (status == android.hardware.weaver.V1_0.WeaverStatus.OK) { + WeaverConfig aidlRes = new WeaverConfig(); + aidlRes.slots = config.slots; + aidlRes.keySize = config.keySize; + aidlRes.valueSize = config.valueSize; + res[0] = aidlRes; + } else { + Slog.e(TAG, + "Failed to get HIDL weaver config. status: " + status + + ", slots: " + config.slots); + } + }); + return res[0]; + } + + @Override + public WeaverReadResponse read(int slotId, byte[] key) + throws RemoteException { + final WeaverReadResponse[] res = new WeaverReadResponse[1]; + mImpl.read( + slotId, toByteArrayList(key), + (int inStatus, android.hardware.weaver.V1_0.WeaverReadResponse readResponse) -> { + WeaverReadResponse aidlRes = + new WeaverReadResponse(); + switch (inStatus) { + case android.hardware.weaver.V1_0.WeaverReadStatus.OK: + aidlRes.status = WeaverReadStatus.OK; + break; + case android.hardware.weaver.V1_0.WeaverReadStatus.THROTTLE: + aidlRes.status = WeaverReadStatus.THROTTLE; + break; + case android.hardware.weaver.V1_0.WeaverReadStatus.INCORRECT_KEY: + aidlRes.status = WeaverReadStatus.INCORRECT_KEY; + break; + case android.hardware.weaver.V1_0.WeaverReadStatus.FAILED: + aidlRes.status = WeaverReadStatus.FAILED; + break; + default: + Slog.e(TAG, "Unexpected status in read: " + inStatus); + aidlRes.status = WeaverReadStatus.FAILED; + break; + } + aidlRes.timeout = readResponse.timeout; + aidlRes.value = fromByteArrayList(readResponse.value); + res[0] = aidlRes; + }); + return res[0]; + } + + @Override + public void write(int slotId, byte[] key, byte[] value) throws RemoteException { + int writeStatus = mImpl.write(slotId, toByteArrayList(key), toByteArrayList(value)); + if (writeStatus != android.hardware.weaver.V1_0.WeaverStatus.OK) { + throw new ServiceSpecificException( + IWeaver.STATUS_FAILED, "Failed IWeaver.write call, status: " + writeStatus); + } + } + + @Override + public String getInterfaceHash() { + // We do not require the interface hash as the client. + throw new UnsupportedOperationException( + "WeaverHidlAdapter does not support getInterfaceHash"); + } + + @Override + public int getInterfaceVersion() { + // Supports only V2 which is at feature parity. + return 2; + } + + @Override + public IBinder asBinder() { + // There is no IHwBinder to IBinder. Not required as the client. + throw new UnsupportedOperationException("WeaverHidlAdapter does not support asBinder"); + } + + private static ArrayList<Byte> toByteArrayList(byte[] data) { + ArrayList<Byte> result = new ArrayList<Byte>(data.length); + for (int i = 0; i < data.length; i++) { + result.add(data[i]); + } + return result; + } + + private static byte[] fromByteArrayList(ArrayList<Byte> data) { + byte[] result = new byte[data.size()]; + for (int i = 0; i < data.size(); i++) { + result[i] = data.get(i); + } + return result; + } +} diff --git a/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java b/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java deleted file mode 100644 index 9d93c3de2f1d..000000000000 --- a/services/core/java/com/android/server/locksettings/WeaverHidlWrapper.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.locksettings; - -import android.hardware.weaver.V1_0.IWeaver; -import android.hardware.weaver.V1_0.WeaverConfig; -import android.hardware.weaver.V1_0.WeaverReadResponse; -import android.hardware.weaver.V1_0.WeaverReadStatus; -import android.hardware.weaver.V1_0.WeaverStatus; -import android.os.RemoteException; -import android.os.ServiceSpecificException; -import android.util.Slog; - -import java.util.ArrayList; - -/** - * Implement the AIDL IWeaver interface wrapping the HIDL implementation - */ -class WeaverHidlWrapper implements android.hardware.weaver.IWeaver { - private static final String TAG = "WeaverHidlWrapper"; - private final IWeaver mImpl; - - WeaverHidlWrapper(IWeaver impl) { - mImpl = impl; - } - - private static ArrayList<Byte> toByteArrayList(byte[] data) { - ArrayList<Byte> result = new ArrayList<Byte>(data.length); - for (int i = 0; i < data.length; i++) { - result.add(data[i]); - } - return result; - } - - private static byte[] fromByteArrayList(ArrayList<Byte> data) { - byte[] result = new byte[data.size()]; - for (int i = 0; i < data.size(); i++) { - result[i] = data.get(i); - } - return result; - } - - @Override - public String getInterfaceHash() { - // We do not require the interface hash as the client. - throw new UnsupportedOperationException( - "WeaverHidlWrapper does not support getInterfaceHash"); - } - @Override - public int getInterfaceVersion() { - // Supports only V2 which is at feature parity. - return 2; - } - @Override - public android.os.IBinder asBinder() { - // There is no IHwBinder to IBinder. Not required as the client. - throw new UnsupportedOperationException("WeaverHidlWrapper does not support asBinder"); - } - - @Override - public android.hardware.weaver.WeaverConfig getConfig() throws RemoteException { - final WeaverConfig[] res = new WeaverConfig[1]; - mImpl.getConfig((int status, WeaverConfig config) -> { - if (status == WeaverStatus.OK && config.slots > 0) { - res[0] = config; - } else { - res[0] = null; - Slog.e(TAG, - "Failed to get HIDL weaver config. status: " + status - + ", slots: " + config.slots); - } - }); - - if (res[0] == null) { - return null; - } - android.hardware.weaver.WeaverConfig config = new android.hardware.weaver.WeaverConfig(); - config.slots = res[0].slots; - config.keySize = res[0].keySize; - config.valueSize = res[0].valueSize; - return config; - } - - @Override - public android.hardware.weaver.WeaverReadResponse read(int slotId, byte[] key) - throws RemoteException { - final WeaverReadResponse[] res = new WeaverReadResponse[1]; - final int[] status = new int[1]; - mImpl.read( - slotId, toByteArrayList(key), (int inStatus, WeaverReadResponse readResponse) -> { - status[0] = inStatus; - res[0] = readResponse; - }); - - android.hardware.weaver.WeaverReadResponse aidlRes = - new android.hardware.weaver.WeaverReadResponse(); - switch (status[0]) { - case WeaverReadStatus.OK: - aidlRes.status = android.hardware.weaver.WeaverReadStatus.OK; - break; - case WeaverReadStatus.THROTTLE: - aidlRes.status = android.hardware.weaver.WeaverReadStatus.THROTTLE; - break; - case WeaverReadStatus.INCORRECT_KEY: - aidlRes.status = android.hardware.weaver.WeaverReadStatus.INCORRECT_KEY; - break; - case WeaverReadStatus.FAILED: - aidlRes.status = android.hardware.weaver.WeaverReadStatus.FAILED; - break; - default: - aidlRes.status = android.hardware.weaver.WeaverReadStatus.FAILED; - break; - } - if (res[0] != null) { - aidlRes.timeout = res[0].timeout; - aidlRes.value = fromByteArrayList(res[0].value); - } - return aidlRes; - } - - @Override - public void write(int slotId, byte[] key, byte[] value) throws RemoteException { - int writeStatus = mImpl.write(slotId, toByteArrayList(key), toByteArrayList(value)); - if (writeStatus != WeaverStatus.OK) { - throw new ServiceSpecificException( - android.hardware.weaver.IWeaver.STATUS_FAILED, "Failed IWeaver.write call"); - } - } -} diff --git a/services/core/java/com/android/server/net/NetworkPolicyLogger.java b/services/core/java/com/android/server/net/NetworkPolicyLogger.java index 4a0a07b9783c..dc8fcb077f61 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyLogger.java +++ b/services/core/java/com/android/server/net/NetworkPolicyLogger.java @@ -94,7 +94,7 @@ public class NetworkPolicyLogger { void networkBlocked(int uid, @Nullable UidBlockedState uidBlockedState) { synchronized (mLock) { if (LOGD || uid == mDebugUid) { - Slog.d(TAG, "Blocked state of " + uid + ": " + uidBlockedState.toString()); + Slog.d(TAG, "Blocked state of " + uid + ": " + uidBlockedState); } if (uidBlockedState == null) { mNetworkBlockedBuffer.networkBlocked(uid, BLOCKED_REASON_NONE, ALLOWED_REASON_NONE, diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 90135ad7684b..b06c4118f9b8 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -240,7 +240,6 @@ import android.service.notification.NotificationRankingUpdate; import android.service.notification.NotificationRecordProto; import android.service.notification.NotificationServiceDumpProto; import android.service.notification.NotificationStats; -import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeProto; @@ -8569,95 +8568,6 @@ public class NotificationManagerService extends SystemService { } } - static class NotificationRecordExtractorData { - // Class that stores any field in a NotificationRecord that can change via an extractor. - // Used to cache previous data used in a sort. - int mPosition; - int mVisibility; - boolean mShowBadge; - boolean mAllowBubble; - boolean mIsBubble; - NotificationChannel mChannel; - String mGroupKey; - ArrayList<String> mOverridePeople; - ArrayList<SnoozeCriterion> mSnoozeCriteria; - Integer mUserSentiment; - Integer mSuppressVisually; - ArrayList<Notification.Action> mSystemSmartActions; - ArrayList<CharSequence> mSmartReplies; - int mImportance; - - // These fields may not trigger a reranking but diffs here may be logged. - float mRankingScore; - boolean mIsConversation; - - NotificationRecordExtractorData(int position, int visibility, boolean showBadge, - boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey, - ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria, - Integer userSentiment, Integer suppressVisually, - ArrayList<Notification.Action> systemSmartActions, - ArrayList<CharSequence> smartReplies, int importance, float rankingScore, - boolean isConversation) { - mPosition = position; - mVisibility = visibility; - mShowBadge = showBadge; - mAllowBubble = allowBubble; - mIsBubble = isBubble; - mChannel = channel; - mGroupKey = groupKey; - mOverridePeople = overridePeople; - mSnoozeCriteria = snoozeCriteria; - mUserSentiment = userSentiment; - mSuppressVisually = suppressVisually; - mSystemSmartActions = systemSmartActions; - mSmartReplies = smartReplies; - mImportance = importance; - mRankingScore = rankingScore; - mIsConversation = isConversation; - } - - // Returns whether the provided NotificationRecord differs from the cached data in any way. - // Should be guarded by mNotificationLock; not annotated here as this class is static. - boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) { - return mPosition != newPosition - || mVisibility != r.getPackageVisibilityOverride() - || mShowBadge != r.canShowBadge() - || mAllowBubble != r.canBubble() - || mIsBubble != r.getNotification().isBubbleNotification() - || !Objects.equals(mChannel, r.getChannel()) - || !Objects.equals(mGroupKey, r.getGroupKey()) - || !Objects.equals(mOverridePeople, r.getPeopleOverride()) - || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) - || !Objects.equals(mUserSentiment, r.getUserSentiment()) - || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects()) - || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) - || !Objects.equals(mSmartReplies, r.getSmartReplies()) - || mImportance != r.getImportance(); - } - - // Returns whether the NotificationRecord has a change from this data for which we should - // log an update. This method specifically targets fields that may be changed via - // adjustments from the assistant. - // - // Fields here are the union of things in NotificationRecordLogger.shouldLogReported - // and NotificationRecord.applyAdjustments. - // - // Should be guarded by mNotificationLock; not annotated here as this class is static. - boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) { - return mPosition != newPosition - || !Objects.equals(mChannel, r.getChannel()) - || !Objects.equals(mGroupKey, r.getGroupKey()) - || !Objects.equals(mOverridePeople, r.getPeopleOverride()) - || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) - || !Objects.equals(mUserSentiment, r.getUserSentiment()) - || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) - || !Objects.equals(mSmartReplies, r.getSmartReplies()) - || mImportance != r.getImportance() - || !r.rankingScoreMatches(mRankingScore) - || mIsConversation != r.isConversation(); - } - } - void handleRankingSort() { if (mRankingHelper == null) return; synchronized (mNotificationLock) { @@ -8683,7 +8593,8 @@ public class NotificationManagerService extends SystemService { r.getSmartReplies(), r.getImportance(), r.getRankingScore(), - r.isConversation()); + r.isConversation(), + r.getProposedImportance()); extractorDataBefore.put(r.getKey(), extractorData); mRankingHelper.extractSignals(r); } @@ -9978,7 +9889,8 @@ public class NotificationManagerService extends SystemService { record.getRankingScore() == 0 ? RANKING_UNCHANGED : (record.getRankingScore() > 0 ? RANKING_PROMOTED : RANKING_DEMOTED), - record.getNotification().isBubbleNotification() + record.getNotification().isBubbleNotification(), + record.getProposedImportance() ); rankings.add(ranking); } diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java index 6dd0daa77ac6..91b5afe1f030 100644 --- a/services/core/java/com/android/server/notification/NotificationRecord.java +++ b/services/core/java/com/android/server/notification/NotificationRecord.java @@ -210,6 +210,7 @@ public final class NotificationRecord { // Whether this notification record should have an update logged the next time notifications // are sorted. private boolean mPendingLogUpdate = false; + private int mProposedImportance = IMPORTANCE_UNSPECIFIED; public NotificationRecord(Context context, StatusBarNotification sbn, NotificationChannel channel) { @@ -499,6 +500,8 @@ public final class NotificationRecord { pw.println(prefix + "mImportance=" + NotificationListenerService.Ranking.importanceToString(mImportance)); pw.println(prefix + "mImportanceExplanation=" + getImportanceExplanation()); + pw.println(prefix + "mProposedImportance=" + + NotificationListenerService.Ranking.importanceToString(mProposedImportance)); pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked); pw.println(prefix + "mIntercept=" + mIntercept); pw.println(prefix + "mHidden==" + mHidden); @@ -738,6 +741,12 @@ public final class NotificationRecord { Adjustment.KEY_NOT_CONVERSATION, Boolean.toString(mIsNotConversationOverride)); } + if (signals.containsKey(Adjustment.KEY_IMPORTANCE_PROPOSAL)) { + mProposedImportance = signals.getInt(Adjustment.KEY_IMPORTANCE_PROPOSAL); + EventLogTags.writeNotificationAdjusted(getKey(), + Adjustment.KEY_IMPORTANCE_PROPOSAL, + Integer.toString(mProposedImportance)); + } if (!signals.isEmpty() && adjustment.getIssuer() != null) { mAdjustmentIssuer = adjustment.getIssuer(); } @@ -870,6 +879,10 @@ public final class NotificationRecord { return stats.naturalImportance; } + public int getProposedImportance() { + return mProposedImportance; + } + public float getRankingScore() { return mRankingScore; } diff --git a/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java new file mode 100644 index 000000000000..6dc9029f8928 --- /dev/null +++ b/services/core/java/com/android/server/notification/NotificationRecordExtractorData.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.notification; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.service.notification.SnoozeCriterion; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * Class that stores any field in a NotificationRecord that can change via an extractor. + * Used to cache previous data used in a sort. + */ +public final class NotificationRecordExtractorData { + private final int mPosition; + private final int mVisibility; + private final boolean mShowBadge; + private final boolean mAllowBubble; + private final boolean mIsBubble; + private final NotificationChannel mChannel; + private final String mGroupKey; + private final ArrayList<String> mOverridePeople; + private final ArrayList<SnoozeCriterion> mSnoozeCriteria; + private final Integer mUserSentiment; + private final Integer mSuppressVisually; + private final ArrayList<Notification.Action> mSystemSmartActions; + private final ArrayList<CharSequence> mSmartReplies; + private final int mImportance; + + // These fields may not trigger a reranking but diffs here may be logged. + private final float mRankingScore; + private final boolean mIsConversation; + private final int mProposedImportance; + + NotificationRecordExtractorData(int position, int visibility, boolean showBadge, + boolean allowBubble, boolean isBubble, NotificationChannel channel, String groupKey, + ArrayList<String> overridePeople, ArrayList<SnoozeCriterion> snoozeCriteria, + Integer userSentiment, Integer suppressVisually, + ArrayList<Notification.Action> systemSmartActions, + ArrayList<CharSequence> smartReplies, int importance, float rankingScore, + boolean isConversation, int proposedImportance) { + mPosition = position; + mVisibility = visibility; + mShowBadge = showBadge; + mAllowBubble = allowBubble; + mIsBubble = isBubble; + mChannel = channel; + mGroupKey = groupKey; + mOverridePeople = overridePeople; + mSnoozeCriteria = snoozeCriteria; + mUserSentiment = userSentiment; + mSuppressVisually = suppressVisually; + mSystemSmartActions = systemSmartActions; + mSmartReplies = smartReplies; + mImportance = importance; + mRankingScore = rankingScore; + mIsConversation = isConversation; + mProposedImportance = proposedImportance; + } + + // Returns whether the provided NotificationRecord differs from the cached data in any way. + // Should be guarded by mNotificationLock; not annotated here as this class is static. + boolean hasDiffForRankingLocked(NotificationRecord r, int newPosition) { + return mPosition != newPosition + || mVisibility != r.getPackageVisibilityOverride() + || mShowBadge != r.canShowBadge() + || mAllowBubble != r.canBubble() + || mIsBubble != r.getNotification().isBubbleNotification() + || !Objects.equals(mChannel, r.getChannel()) + || !Objects.equals(mGroupKey, r.getGroupKey()) + || !Objects.equals(mOverridePeople, r.getPeopleOverride()) + || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) + || !Objects.equals(mUserSentiment, r.getUserSentiment()) + || !Objects.equals(mSuppressVisually, r.getSuppressedVisualEffects()) + || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) + || !Objects.equals(mSmartReplies, r.getSmartReplies()) + || mImportance != r.getImportance() + || mProposedImportance != r.getProposedImportance(); + } + + // Returns whether the NotificationRecord has a change from this data for which we should + // log an update. This method specifically targets fields that may be changed via + // adjustments from the assistant. + // + // Fields here are the union of things in NotificationRecordLogger.shouldLogReported + // and NotificationRecord.applyAdjustments. + // + // Should be guarded by mNotificationLock; not annotated here as this class is static. + boolean hasDiffForLoggingLocked(NotificationRecord r, int newPosition) { + return mPosition != newPosition + || !Objects.equals(mChannel, r.getChannel()) + || !Objects.equals(mGroupKey, r.getGroupKey()) + || !Objects.equals(mOverridePeople, r.getPeopleOverride()) + || !Objects.equals(mSnoozeCriteria, r.getSnoozeCriteria()) + || !Objects.equals(mUserSentiment, r.getUserSentiment()) + || !Objects.equals(mSystemSmartActions, r.getSystemGeneratedSmartActions()) + || !Objects.equals(mSmartReplies, r.getSmartReplies()) + || mImportance != r.getImportance() + || !r.rankingScoreMatches(mRankingScore) + || mIsConversation != r.isConversation() + || mProposedImportance != r.getProposedImportance(); + } +} diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java index 2ef193cacbae..32479ee459ff 100644 --- a/services/core/java/com/android/server/pm/AppStateHelper.java +++ b/services/core/java/com/android/server/pm/AppStateHelper.java @@ -23,6 +23,7 @@ import android.app.ActivityThread; import android.app.usage.NetworkStats; import android.app.usage.NetworkStatsManager; import android.content.Context; +import android.content.pm.PackageManagerInternal; import android.media.AudioManager; import android.media.IAudioService; import android.net.ConnectivityManager; @@ -36,6 +37,7 @@ import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; @@ -157,6 +159,56 @@ public class AppStateHelper { return false; } + private static boolean containsAny(Collection<String> list, Collection<String> which) { + if (list.isEmpty()) { + return false; + } + for (var element : which) { + if (list.contains(element)) { + return true; + } + } + return false; + } + + private void addLibraryDependency(ArraySet<String> results, List<String> libPackageNames) { + var pmInternal = LocalServices.getService(PackageManagerInternal.class); + + var libraryNames = new ArraySet<String>(); + var staticSharedLibraryNames = new ArraySet<String>(); + var sdkLibraryNames = new ArraySet<String>(); + for (var packageName : libPackageNames) { + var pkg = pmInternal.getAndroidPackage(packageName); + if (pkg == null) { + continue; + } + libraryNames.addAll(pkg.getLibraryNames()); + var libraryName = pkg.getStaticSharedLibraryName(); + if (libraryName != null) { + staticSharedLibraryNames.add(libraryName); + } + libraryName = pkg.getSdkLibraryName(); + if (libraryName != null) { + sdkLibraryNames.add(libraryName); + } + } + + if (libraryNames.isEmpty() + && staticSharedLibraryNames.isEmpty() + && sdkLibraryNames.isEmpty()) { + return; + } + + pmInternal.forEachPackage(pkg -> { + if (containsAny(pkg.getUsesLibraries(), libraryNames) + || containsAny(pkg.getUsesOptionalLibraries(), libraryNames) + || containsAny(pkg.getUsesStaticLibraries(), staticSharedLibraryNames) + || containsAny(pkg.getUsesSdkLibraries(), sdkLibraryNames)) { + results.add(pkg.getPackageName()); + } + }); + } + /** * True if any app has sent or received network data over the past * {@link #ACTIVE_NETWORK_DURATION_MILLIS} milliseconds. @@ -225,6 +277,7 @@ public class AppStateHelper { */ public List<String> getDependencyPackages(List<String> packageNames) { var results = new ArraySet<String>(); + // Include packages sharing the same process var am = mContext.getSystemService(ActivityManager.class); for (var info : am.getRunningAppProcesses()) { for (var packageName : packageNames) { @@ -236,10 +289,14 @@ public class AppStateHelper { } } } + // Include packages using bounded services var amInternal = LocalServices.getService(ActivityManagerInternal.class); for (var packageName : packageNames) { results.addAll(amInternal.getClientPackages(packageName)); } + // Include packages using libraries + addLibraryDependency(results, packageNames); + return new ArrayList<>(results); } } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index d87373669d69..0a59c1991684 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -2045,7 +2045,8 @@ public class ComputerEngine implements Computer { } final SharedLibraryInfo libraryInfo = getSharedLibraryInfo( - ps.getPkg().getStaticSharedLibraryName(), ps.getPkg().getStaticSharedLibVersion()); + ps.getPkg().getStaticSharedLibraryName(), + ps.getPkg().getStaticSharedLibraryVersion()); if (libraryInfo == null) { return false; } diff --git a/services/core/java/com/android/server/pm/DeletePackageHelper.java b/services/core/java/com/android/server/pm/DeletePackageHelper.java index 9ea1807ad2b8..3df46a245eb1 100644 --- a/services/core/java/com/android/server/pm/DeletePackageHelper.java +++ b/services/core/java/com/android/server/pm/DeletePackageHelper.java @@ -186,7 +186,7 @@ final class DeletePackageHelper { SharedLibraryInfo libraryInfo = null; if (pkg.getStaticSharedLibraryName() != null) { libraryInfo = computer.getSharedLibraryInfo(pkg.getStaticSharedLibraryName(), - pkg.getStaticSharedLibVersion()); + pkg.getStaticSharedLibraryVersion()); } else if (pkg.getSdkLibraryName() != null) { libraryInfo = computer.getSharedLibraryInfo(pkg.getSdkLibraryName(), pkg.getSdkLibVersionMajor()); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 7049f9a2d584..16bf0fe09b9e 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -297,7 +297,7 @@ final class InstallPackageHelper { SharedUserSetting sharedUserSetting = mPm.mSettings.getSharedUserSettingLPr(pkgSetting); if (sharedUserSetting != null) { sharedUserSetting.addPackage(pkgSetting); - if (parsedPackage.isLeavingSharedUid() + if (parsedPackage.isLeavingSharedUser() && SharedUidMigration.applyStrategy(BEST_EFFORT) && sharedUserSetting.isSingleUser()) { // Attempt the transparent shared UID migration @@ -1552,7 +1552,7 @@ final class InstallPackageHelper { } // APK should not re-join shared UID - if (oldPackage.isLeavingSharedUid() && !parsedPackage.isLeavingSharedUid()) { + if (oldPackage.isLeavingSharedUser() && !parsedPackage.isLeavingSharedUser()) { throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED, "Package " + parsedPackage.getPackageName() + " attempting to rejoin " + newSharedUid); @@ -3801,7 +3801,7 @@ final class InstallPackageHelper { if (installedPkgSetting == null || !installedPkgSetting.hasSharedUser()) { // Directly ignore sharedUserSetting for new installs, or if the app has // already left shared UID - ignoreSharedUserId = parsedPackage.isLeavingSharedUid(); + ignoreSharedUserId = parsedPackage.isLeavingSharedUser(); } if (!ignoreSharedUserId && parsedPackage.getSharedUserId() != null) { @@ -4324,10 +4324,10 @@ final class InstallPackageHelper { SharedLibraryInfo libInfo = versionedLib.valueAt(i); final long libVersionCode = libInfo.getDeclaringPackage() .getLongVersionCode(); - if (libInfo.getLongVersion() < pkg.getStaticSharedLibVersion()) { + if (libInfo.getLongVersion() < pkg.getStaticSharedLibraryVersion()) { minVersionCode = Math.max(minVersionCode, libVersionCode + 1); } else if (libInfo.getLongVersion() - > pkg.getStaticSharedLibVersion()) { + > pkg.getStaticSharedLibraryVersion()) { maxVersionCode = Math.min(maxVersionCode, libVersionCode - 1); } else { minVersionCode = maxVersionCode = libVersionCode; diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 9c607950587d..8c5bab6a55dd 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1211,7 +1211,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements // Take a short detour to confirm with user final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE); intent.setData(Uri.fromParts("package", versionedPackage.getPackageName(), null)); - intent.putExtra(PackageInstaller.EXTRA_CALLBACK, adapter.getBinder().asBinder()); + intent.putExtra(PackageInstaller.EXTRA_CALLBACK, + new PackageManager.UninstallCompleteCallback(adapter.getBinder().asBinder())); adapter.onUserActionRequired(intent); } } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index ed4c849ac267..afcd9d1359a8 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -2579,6 +2579,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { : PackageInstaller.ACTION_CONFIRM_INSTALL); intent.setPackage(mPm.getPackageInstallerPackageName()); intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); + synchronized (mLock) { + intent.putExtra(PackageInstaller.EXTRA_RESOLVED_BASE_PATH, + mResolvedBaseFile != null ? mResolvedBaseFile.getAbsolutePath() : null); + } sendOnUserActionRequired(mContext, target, sessionId, intent); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index a52ed8b1a10a..c1298ffd2d61 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -499,7 +499,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService private static final String PROPERTY_KNOWN_DIGESTERS_LIST = "known_digesters_list"; /** - * Whether of not requesting the approval before committing sessions is available. + * Whether or not requesting the approval before committing sessions is available. * * Flag type: {@code boolean} * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE @@ -508,6 +508,15 @@ public class PackageManagerService implements PackageSender, TestUtilityService "is_preapproval_available"; /** + * Whether or not the update ownership enforcement is available. + * + * Flag type: {@code boolean} + * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE + */ + private static final String PROPERTY_IS_UPDATE_OWNERSHIP_ENFORCEMENT_AVAILABLE = + "is_update_ownership_enforcement_available"; + + /** * The default response for package verification timeout. * * This can be either PackageManager.VERIFICATION_ALLOW or @@ -1105,8 +1114,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService @Deprecated @NonNull public Computer snapshotComputer(boolean allowLiveComputer) { + var isHoldingPackageLock = Thread.holdsLock(mLock); if (allowLiveComputer) { - if (Thread.holdsLock(mLock)) { + if (isHoldingPackageLock) { // If the current thread holds mLock then it may have modified state but not // yet invalidated the snapshot. Always give the thread the live computer. return mLiveComputer; @@ -1120,6 +1130,15 @@ public class PackageManagerService implements PackageSender, TestUtilityService return oldSnapshot.use(); } + if (isHoldingPackageLock) { + // If the current thread holds mLock then it already has exclusive write access to the + // two snapshot fields, and we can just go ahead and rebuild the snapshot. + @SuppressWarnings("GuardedBy") + var newSnapshot = rebuildSnapshot(oldSnapshot, pendingVersion); + sSnapshot.set(newSnapshot); + return newSnapshot.use(); + } + synchronized (mSnapshotLock) { // Re-capture pending version in case a new invalidation occurred since last check var rebuildSnapshot = sSnapshot.get(); @@ -1137,7 +1156,11 @@ public class PackageManagerService implements PackageSender, TestUtilityService // Fetch version one last time to ensure that the rebuilt snapshot matches // the latest invalidation, which could have come in between entering the // SnapshotLock and mLock sync blocks. + rebuildSnapshot = sSnapshot.get(); rebuildVersion = sSnapshotPendingVersion.get(); + if (rebuildSnapshot != null && rebuildSnapshot.getVersion() == rebuildVersion) { + return rebuildSnapshot.use(); + } // Build the snapshot for this version var newSnapshot = rebuildSnapshot(rebuildSnapshot, rebuildVersion); @@ -1147,7 +1170,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } } - @GuardedBy({ "mLock", "mSnapshotLock"}) + @GuardedBy("mLock") private Computer rebuildSnapshot(@Nullable Computer oldSnapshot, int newVersion) { var now = SystemClock.currentTimeMicro(); var hits = oldSnapshot == null ? -1 : oldSnapshot.getUsed(); @@ -2065,6 +2088,14 @@ public class PackageManagerService implements PackageSender, TestUtilityService mInitAppsHelper.initNonSystemApps(packageParser, userIds, startTime); packageParser.close(); + mRequiredVerifierPackages = getRequiredButNotReallyRequiredVerifiersLPr(computer); + mRequiredInstallerPackage = getRequiredInstallerLPr(computer); + mRequiredUninstallerPackage = getRequiredUninstallerLPr(computer); + + // PermissionController hosts default permission granting and role management, so it's a + // critical part of the core system. + mRequiredPermissionControllerPackage = getRequiredPermissionControllerLPr(computer); + // Resolve the storage manager. mStorageManagerPackage = getStorageManagerPackageName(computer); @@ -2210,9 +2241,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_READY, SystemClock.uptimeMillis()); - mRequiredVerifierPackages = getRequiredButNotReallyRequiredVerifiersLPr(computer); - mRequiredInstallerPackage = getRequiredInstallerLPr(computer); - mRequiredUninstallerPackage = getRequiredUninstallerLPr(computer); ComponentName intentFilterVerifierComponent = getIntentFilterVerifierComponentNameLPr(computer); ComponentName domainVerificationAgent = @@ -2230,10 +2258,6 @@ public class PackageManagerService implements PackageSender, TestUtilityService PackageManager.SYSTEM_SHARED_LIBRARY_SHARED, SharedLibraryInfo.VERSION_UNDEFINED); - // PermissionController hosts default permission granting and role management, so it's a - // critical part of the core system. - mRequiredPermissionControllerPackage = getRequiredPermissionControllerLPr(computer); - mSettings.setPermissionControllerVersion( computer.getPackageInfo(mRequiredPermissionControllerPackage, 0, UserHandle.USER_SYSTEM).getLongVersionCode()); @@ -2878,7 +2902,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService static void renameStaticSharedLibraryPackage(ParsedPackage parsedPackage) { // Derive the new package synthetic package name parsedPackage.setPackageName(toStaticSharedLibraryPackageName( - parsedPackage.getPackageName(), parsedPackage.getStaticSharedLibVersion())); + parsedPackage.getPackageName(), parsedPackage.getStaticSharedLibraryVersion())); } private static String toStaticSharedLibraryPackageName( @@ -7060,6 +7084,16 @@ public class PackageManagerService implements PackageSender, TestUtilityService } } + static boolean isUpdateOwnershipEnforcementAvailable() { + final long token = Binder.clearCallingIdentity(); + try { + return DeviceConfig.getBoolean(NAMESPACE_PACKAGE_MANAGER_SERVICE, + PROPERTY_IS_UPDATE_OWNERSHIP_ENFORCEMENT_AVAILABLE, false /* defaultValue */); + } finally { + Binder.restoreCallingIdentity(token); + } + } + /** * Returns the array containing per-uid timeout configuration. * This is derived from DeviceConfig flags. diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index b18179e7b752..433e7a1f12b6 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -1386,6 +1386,8 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return AndroidPackageUtils.getHiddenApiEnforcementPolicy(getAndroidPackage(), this); } + + // Code below generated by codegen v1.0.23. // // DO NOT MODIFY! diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 9b6bfd9bdd49..eb99536437d6 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -234,7 +234,7 @@ final class RemovePackageHelper { } if (pkg.getStaticSharedLibraryName() != null) { if (mSharedLibraries.removeSharedLibrary(pkg.getStaticSharedLibraryName(), - pkg.getStaticSharedLibVersion())) { + pkg.getStaticSharedLibraryVersion())) { if (DEBUG_REMOVE && chatty) { if (r == null) { r = new StringBuilder(256); diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 53be787c3cd1..321c5c64267f 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -89,6 +89,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; +import com.android.internal.security.VerityUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.CollectionUtils; import com.android.internal.util.IndentingPrintWriter; @@ -370,6 +371,8 @@ public final class Settings implements Watchable, Snappable { // Current settings file. private final File mSettingsFilename; + // Reserve copy of the current settings file. + private final File mSettingsReserveCopyFilename; // Previous settings file. // Removed when the current settings file successfully stored. private final File mPreviousSettingsFilename; @@ -640,6 +643,7 @@ public final class Settings implements Watchable, Snappable { mRuntimePermissionsPersistence = null; mPermissionDataProvider = null; mSettingsFilename = null; + mSettingsReserveCopyFilename = null; mPreviousSettingsFilename = null; mPackageListFilename = null; mStoppedPackagesFilename = null; @@ -711,6 +715,7 @@ public final class Settings implements Watchable, Snappable { |FileUtils.S_IROTH|FileUtils.S_IXOTH, -1, -1); mSettingsFilename = new File(mSystemDir, "packages.xml"); + mSettingsReserveCopyFilename = new File(mSystemDir, "packages.xml.reservecopy"); mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml"); mPackageListFilename = new File(mSystemDir, "packages.list"); FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID); @@ -752,6 +757,7 @@ public final class Settings implements Watchable, Snappable { mLock = null; mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence; mSettingsFilename = null; + mSettingsReserveCopyFilename = null; mPreviousSettingsFilename = null; mPackageListFilename = null; mStoppedPackagesFilename = null; @@ -1455,7 +1461,7 @@ public final class Settings implements Watchable, Snappable { void checkAndConvertSharedUserSettingsLPw(SharedUserSetting sharedUser) { if (!sharedUser.isSingleUser()) return; final AndroidPackage pkg = sharedUser.getPackageSettings().valueAt(0).getPkg(); - if (pkg != null && pkg.isLeavingSharedUid() + if (pkg != null && pkg.isLeavingSharedUser() && SharedUidMigration.applyStrategy(BEST_EFFORT)) { convertSharedUserSettingsLPw(sharedUser); } @@ -2681,12 +2687,25 @@ public final class Settings implements Watchable, Snappable { // New settings successfully written, old ones are no longer needed. mPreviousSettingsFilename.delete(); + mSettingsReserveCopyFilename.delete(); FileUtils.setPermissions(mSettingsFilename.toString(), - FileUtils.S_IRUSR|FileUtils.S_IWUSR - |FileUtils.S_IRGRP|FileUtils.S_IWGRP, + FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP, -1, -1); + try { + FileUtils.copy(mSettingsFilename, mSettingsReserveCopyFilename); + } catch (IOException e) { + Slog.e(TAG, "Failed to backup settings", e); + } + + try { + VerityUtils.setUpFsverity(mSettingsFilename.getAbsolutePath()); + VerityUtils.setUpFsverity(mSettingsReserveCopyFilename.getAbsolutePath()); + } catch (IOException e) { + Slog.e(TAG, "Failed to verity-protect settings", e); + } + writeKernelMappingLPr(); writePackageListLPr(); writeAllUsersPackageRestrictionsLPr(sync); @@ -3117,49 +3136,62 @@ public final class Settings implements Watchable, Snappable { } } - boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) { - FileInputStream str = null; - if (mPreviousSettingsFilename.exists()) { - try { - str = new FileInputStream(mPreviousSettingsFilename); - mReadMessages.append("Reading from backup settings file\n"); - PackageManagerService.reportSettingsProblem(Log.INFO, - "Need to read from backup settings file"); - if (mSettingsFilename.exists()) { - // If both the previous and current settings files exist, - // we ignore the current since it might have been corrupted. - Slog.w(PackageManagerService.TAG, "Cleaning up settings file " - + mSettingsFilename); - mSettingsFilename.delete(); - } - } catch (java.io.IOException e) { - // We'll try for the normal settings file. - } - } - + boolean readSettingsLPw(@NonNull Computer computer, @NonNull List<UserInfo> users, + ArrayMap<String, Long> originalFirstInstallTimes) { mPendingPackages.clear(); mPastSignatures.clear(); mKeySetRefs.clear(); mInstallerPackages.clear(); + originalFirstInstallTimes.clear(); - // If any user state doesn't have a first install time, e.g., after an OTA, - // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package - // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost. - final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>(); + File file = null; + FileInputStream str = null; try { + // Check if the previous write was incomplete. + if (mPreviousSettingsFilename.exists()) { + try { + file = mPreviousSettingsFilename; + str = new FileInputStream(file); + mReadMessages.append("Reading from backup settings file\n"); + PackageManagerService.reportSettingsProblem(Log.INFO, + "Need to read from backup settings file"); + if (mSettingsFilename.exists()) { + // If both the previous and current settings files exist, + // we ignore the current since it might have been corrupted. + Slog.w(PackageManagerService.TAG, "Cleaning up settings file " + + mSettingsFilename); + mSettingsFilename.delete(); + } + // Ignore reserve copy as well. + mSettingsReserveCopyFilename.delete(); + } catch (java.io.IOException e) { + // We'll try for the normal settings file. + } + } if (str == null) { - if (!mSettingsFilename.exists()) { - mReadMessages.append("No settings file found\n"); + if (mSettingsFilename.exists()) { + // Using packages.xml. + file = mSettingsFilename; + str = new FileInputStream(file); + } else if (mSettingsReserveCopyFilename.exists()) { + // Using reserve copy. + file = mSettingsReserveCopyFilename; + str = new FileInputStream(file); + mReadMessages.append("Reading from reserve copy settings file\n"); PackageManagerService.reportSettingsProblem(Log.INFO, - "No settings file; creating initial state"); - // It's enough to just touch version details to create them - // with default values - findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent(); - findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent(); - return false; + "Need to read from reserve copy settings file"); } - str = new FileInputStream(mSettingsFilename); + } + if (str == null) { + // No available data sources. + mReadMessages.append("No settings file found\n"); + PackageManagerService.reportSettingsProblem(Log.INFO, + "No settings file; creating initial state"); + // Not necessary, but will avoid wtf-s in the "finally" section. + findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent(); + findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent(); + return false; } final TypedXmlPullParser parser = Xml.resolvePullParser(str); @@ -3280,6 +3312,33 @@ public final class Settings implements Watchable, Snappable { mReadMessages.append("Error reading: " + e.toString()); PackageManagerService.reportSettingsProblem(Log.ERROR, "Error reading settings: " + e); Slog.wtf(PackageManagerService.TAG, "Error reading package manager settings", e); + + // Remove corrupted file and retry. + Slog.e(TAG, + "Error reading package manager settings, removing " + file + " and retrying.", + e); + file.delete(); + + // Ignore the result to not mark this as a "first boot". + readSettingsLPw(computer, users, originalFirstInstallTimes); + } + + return true; + } + + /** + * @return false if settings file is missing (i.e. during first boot), true otherwise + */ + boolean readLPw(@NonNull Computer computer, @NonNull List<UserInfo> users) { + // If any user state doesn't have a first install time, e.g., after an OTA, + // use the pre OTA firstInstallTime timestamp. This is because we migrated from per package + // firstInstallTime to per user-state. Without this, OTA can cause this info to be lost. + final ArrayMap<String, Long> originalFirstInstallTimes = new ArrayMap<>(); + + try { + if (!readSettingsLPw(computer, users, originalFirstInstallTimes)) { + return false; + } } finally { if (!mVersion.containsKey(StorageManager.UUID_PRIVATE_INTERNAL)) { Slog.wtf(PackageManagerService.TAG, @@ -4857,7 +4916,7 @@ public final class Settings implements Watchable, Snappable { pw.print(prefix); pw.println(" static library:"); pw.print(prefix); pw.print(" "); pw.print("name:"); pw.print(pkg.getStaticSharedLibraryName()); - pw.print(" version:"); pw.println(pkg.getStaticSharedLibVersion()); + pw.print(" version:"); pw.println(pkg.getStaticSharedLibraryVersion()); } if (pkg.getSdkLibraryName() != null) { diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index 8c2b212dddd3..d2ce23efd47c 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -408,7 +408,7 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable final int versionCount = versionedLib.size(); for (int i = 0; i < versionCount; i++) { final long libVersion = versionedLib.keyAt(i); - if (libVersion < pkg.getStaticSharedLibVersion()) { + if (libVersion < pkg.getStaticSharedLibraryVersion()) { previousLibVersion = Math.max(previousLibVersion, libVersion); } } @@ -468,7 +468,7 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable } } else if (pkg.getStaticSharedLibraryName() != null) { SharedLibraryInfo definedLibrary = getSharedLibraryInfo( - pkg.getStaticSharedLibraryName(), pkg.getStaticSharedLibVersion()); + pkg.getStaticSharedLibraryName(), pkg.getStaticSharedLibraryVersion()); if (definedLibrary != null) { action.accept(definedLibrary, libInfo); } diff --git a/services/core/java/com/android/server/pm/SharedUserSetting.java b/services/core/java/com/android/server/pm/SharedUserSetting.java index a7a4c4e61dc4..a037ae8ead04 100644 --- a/services/core/java/com/android/server/pm/SharedUserSetting.java +++ b/services/core/java/com/android/server/pm/SharedUserSetting.java @@ -253,7 +253,7 @@ public final class SharedUserSetting extends SettingBase implements SharedUserAp } if (mDisabledPackages.size() == 1) { final AndroidPackage pkg = mDisabledPackages.valueAt(0).getPkg(); - return pkg != null && pkg.isLeavingSharedUid(); + return pkg != null && pkg.isLeavingSharedUser(); } return true; } 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 87805e0d4bd7..ff993eaa81a5 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java +++ b/services/core/java/com/android/server/pm/parsing/PackageInfoUtils.java @@ -131,7 +131,7 @@ public class PackageInfoUtils { info.splitRevisionCodes = pkg.getSplitRevisionCodes(); info.versionName = pkg.getVersionName(); info.sharedUserId = pkg.getSharedUserId(); - info.sharedUserLabel = pkg.getSharedUserLabel(); + info.sharedUserLabel = pkg.getSharedUserLabelRes(); info.applicationInfo = applicationInfo; info.installLocation = pkg.getInstallLocation(); if ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 @@ -218,7 +218,7 @@ public class PackageInfoUtils { } } } - if (pkg.areAttributionsUserVisible()) { + if (pkg.isAttributionsUserVisible()) { info.applicationInfo.privateFlagsExt |= ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE; } else { @@ -869,7 +869,7 @@ public class PackageInfoUtils { public static int appInfoFlags(AndroidPackage pkg, @Nullable PackageStateInternal pkgSetting) { // @formatter:off int pkgWithoutStateFlags = flag(pkg.isExternalStorage(), ApplicationInfo.FLAG_EXTERNAL_STORAGE) - | flag(pkg.isBaseHardwareAccelerated(), ApplicationInfo.FLAG_HARDWARE_ACCELERATED) + | flag(pkg.isHardwareAccelerated(), ApplicationInfo.FLAG_HARDWARE_ACCELERATED) | flag(pkg.isAllowBackup(), ApplicationInfo.FLAG_ALLOW_BACKUP) | flag(pkg.isKillAfterRestore(), ApplicationInfo.FLAG_KILL_AFTER_RESTORE) | flag(pkg.isRestoreAnyVersion(), ApplicationInfo.FLAG_RESTORE_ANY_VERSION) @@ -972,7 +972,7 @@ public class PackageInfoUtils { // @formatter:off int pkgWithoutStateFlags = flag(pkg.isProfileable(), ApplicationInfo.PRIVATE_FLAG_EXT_PROFILEABLE) | flag(pkg.hasRequestForegroundServiceExemption(), ApplicationInfo.PRIVATE_FLAG_EXT_REQUEST_FOREGROUND_SERVICE_EXEMPTION) - | flag(pkg.areAttributionsUserVisible(), ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE) + | flag(pkg.isAttributionsUserVisible(), ApplicationInfo.PRIVATE_FLAG_EXT_ATTRIBUTIONS_ARE_USER_VISIBLE) | flag(pkg.isOnBackInvokedCallbackEnabled(), ApplicationInfo.PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK) | flag(isAllowlistedForHiddenApis, ApplicationInfo.PRIVATE_FLAG_EXT_ALLOWLISTED_FOR_HIDDEN_APIS); return appInfoPrivateFlagsExt(pkgWithoutStateFlags, pkgSetting); diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java index 4fee84f8962b..f3ee53178b60 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageUtils.java @@ -103,7 +103,7 @@ public class AndroidPackageUtils { return new SharedLibraryInfo(null, pkg.getPackageName(), AndroidPackageUtils.getAllCodePaths(pkg), pkg.getStaticSharedLibraryName(), - pkg.getStaticSharedLibVersion(), + pkg.getStaticSharedLibraryVersion(), SharedLibraryInfo.TYPE_STATIC, new VersionedPackage(pkg.getManifestPackageName(), pkg.getLongVersionCode()), diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java index ba36ab7a1f02..e361c9332972 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java @@ -717,7 +717,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public boolean areAttributionsUserVisible() { + public boolean isAttributionsUserVisible() { return getBoolean(Booleans.ATTRIBUTIONS_ARE_USER_VISIBLE); } @@ -869,7 +869,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public int getBanner() { + public int getBannerRes() { return banner; } @@ -897,7 +897,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @Nullable @Override - public String getClassName() { + public String getApplicationClassName() { return className; } @@ -924,7 +924,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public int getDataExtractionRules() { + public int getDataExtractionRulesRes() { return dataExtractionRules; } @@ -940,7 +940,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public int getFullBackupContent() { + public int getFullBackupContentRes() { return fullBackupContent; } @@ -1006,7 +1006,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public int getLogo() { + public int getLogoRes() { return logo; } @@ -1277,7 +1277,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public int getSharedUserLabel() { + public int getSharedUserLabelRes() { return sharedUserLabel; } @@ -1330,7 +1330,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public long getStaticSharedLibVersion() { + public long getStaticSharedLibraryVersion() { return staticSharedLibVersion; } @@ -1356,7 +1356,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public int getTheme() { + public int getThemeRes() { return theme; } @@ -1519,8 +1519,8 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public boolean isBaseHardwareAccelerated() { - return getBoolean(Booleans.BASE_HARDWARE_ACCELERATED); + public boolean isHardwareAccelerated() { + return getBoolean(Booleans.HARDWARE_ACCELERATED); } @Override @@ -1609,7 +1609,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public boolean isLeavingSharedUid() { + public boolean isLeavingSharedUser() { return getBoolean(Booleans.LEAVING_SHARED_UID); } @@ -1840,14 +1840,14 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setBanner(int value) { + public PackageImpl setBannerRes(int value) { banner = value; return this; } @Override - public PackageImpl setBaseHardwareAccelerated(boolean value) { - return setBoolean(Booleans.BASE_HARDWARE_ACCELERATED, value); + public PackageImpl setHardwareAccelerated(boolean value) { + return setBoolean(Booleans.HARDWARE_ACCELERATED, value); } @Override @@ -1874,7 +1874,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setClassName(@Nullable String className) { + public PackageImpl setApplicationClassName(@Nullable String className) { this.className = className == null ? null : className.trim(); return this; } @@ -1903,7 +1903,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setDataExtractionRules(int value) { + public PackageImpl setDataExtractionRulesRes(int value) { dataExtractionRules = value; return this; } @@ -1940,7 +1940,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setFullBackupContent(int value) { + public PackageImpl setFullBackupContentRes(int value) { fullBackupContent = value; return this; } @@ -2022,7 +2022,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setLeavingSharedUid(boolean value) { + public PackageImpl setLeavingSharedUser(boolean value) { return setBoolean(Booleans.LEAVING_SHARED_UID, value); } @@ -2033,7 +2033,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setLogo(int value) { + public PackageImpl setLogoRes(int value) { logo = value; return this; } @@ -2292,7 +2292,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setSharedUserLabel(int value) { + public PackageImpl setSharedUserLabelRes(int value) { sharedUserLabel = value; return this; } @@ -2318,7 +2318,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setStaticSharedLibVersion(long value) { + public PackageImpl setStaticSharedLibraryVersion(long value) { staticSharedLibVersion = value; return this; } @@ -2397,7 +2397,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override - public PackageImpl setTheme(int value) { + public PackageImpl setThemeRes(int value) { theme = value; return this; } @@ -3529,7 +3529,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, private static class Booleans { @LongDef({ EXTERNAL_STORAGE, - BASE_HARDWARE_ACCELERATED, + HARDWARE_ACCELERATED, ALLOW_BACKUP, KILL_AFTER_RESTORE, RESTORE_ANY_VERSION, @@ -3593,7 +3593,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, public @interface Flags {} private static final long EXTERNAL_STORAGE = 1L; - private static final long BASE_HARDWARE_ACCELERATED = 1L << 1; + private static final long HARDWARE_ACCELERATED = 1L << 1; private static final long ALLOW_BACKUP = 1L << 2; private static final long KILL_AFTER_RESTORE = 1L << 3; private static final long RESTORE_ANY_VERSION = 1L << 4; diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index 83e17a5a9075..58f88c324354 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -1800,6 +1800,15 @@ final class DefaultPermissionGrantPolicy { PermissionState permState; if (permIdx >= 0) { permState = uidState.valueAt(permIdx); + // Quick and dirty fix for shared UID packages - we should grant permission with the + // correct package even if a previous checkPermission() used a package that isn't + // requesting the permission. Ideally we should use package manager snapshot and get + // rid of this entire inner class. + if (!ArrayUtils.contains(permState.mPkgRequestingPerm.requestedPermissions, + permission) && ArrayUtils.contains(pkg.requestedPermissions, + permission)) { + permState.mPkgRequestingPerm = pkg; + } } else { permState = new PermissionState(permission, pkg, user); uidState.put(permission, permState); @@ -1887,7 +1896,7 @@ final class DefaultPermissionGrantPolicy { */ private class PermissionState { private final @NonNull String mPermission; - private final @NonNull PackageInfo mPkgRequestingPerm; + private @NonNull PackageInfo mPkgRequestingPerm; private final @NonNull UserHandle mUser; /** Permission flags when the state was created */ diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index a949f7545f23..268a36f39a60 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -119,7 +119,7 @@ public class PermissionManagerService extends IPermissionManager.Stub { private final AppOpsManager mAppOpsManager; private final Context mContext; - private final PermissionManagerServiceImpl mPermissionManagerServiceImpl; + private final PermissionManagerServiceInterface mPermissionManagerServiceImpl; @NonNull private final AttributionSourceRegistry mAttributionSourceRegistry; @@ -152,6 +152,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { mPermissionManagerServiceImpl = new PermissionManagerServiceImpl(context, availableFeatures); + //mPermissionManagerServiceImpl = new PermissionManagerServiceLoggingDecorator( + // LocalServices.getService(PermissionManagerServiceInterface.class)); } /** diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java new file mode 100644 index 000000000000..bfe0008bc9e3 --- /dev/null +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceLoggingDecorator.java @@ -0,0 +1,433 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm.permission; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.pm.PermissionGroupInfo; +import android.content.pm.PermissionInfo; +import android.content.pm.permission.SplitPermissionInfoParcelable; +import android.permission.IOnPermissionsChangeListener; +import android.util.Log; + +import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.PackageState; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Logging decorator for {@link PermissionManagerServiceInterface}. + */ +public class PermissionManagerServiceLoggingDecorator implements PermissionManagerServiceInterface { + private static final String LOG_TAG = + PermissionManagerServiceLoggingDecorator.class.getSimpleName(); + + @NonNull + private final PermissionManagerServiceInterface mService; + + public PermissionManagerServiceLoggingDecorator( + @NonNull PermissionManagerServiceInterface service + ) { + mService = service; + } + + @Nullable + @Override + public byte[] backupRuntimePermissions(int userId) { + Log.i(LOG_TAG, "backupRuntimePermissions(userId = " + userId + ")"); + return mService.backupRuntimePermissions(userId); + } + + @Override + @SuppressWarnings("ArrayToString") + public void restoreRuntimePermissions(@NonNull byte[] backup, int userId) { + Log.i(LOG_TAG, "restoreRuntimePermissions(backup = " + backup + ", userId = " + userId + + ")"); + mService.restoreRuntimePermissions(backup, userId); + } + + @Override + public void restoreDelayedRuntimePermissions(@NonNull String packageName, int userId) { + Log.i(LOG_TAG, "restoreDelayedRuntimePermissions(packageName = " + packageName + + ", userId = " + userId + ")"); + mService.restoreDelayedRuntimePermissions(packageName, userId); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + Log.i(LOG_TAG, "dump(fd = " + fd + ", pw = " + pw + ", args = " + Arrays.toString(args) + + ")"); + mService.dump(fd, pw, args); + } + + @Override + public List<PermissionGroupInfo> getAllPermissionGroups(int flags) { + Log.i(LOG_TAG, "getAllPermissionGroups(flags = " + flags + ")"); + return mService.getAllPermissionGroups(flags); + } + + @Override + public PermissionGroupInfo getPermissionGroupInfo(String groupName, int flags) { + Log.i(LOG_TAG, "getPermissionGroupInfo(groupName = " + groupName + ", flags = " + flags + + ")"); + return mService.getPermissionGroupInfo(groupName, flags); + } + + @Override + public PermissionInfo getPermissionInfo(@NonNull String permName, int flags, + @NonNull String opPackageName) { + Log.i(LOG_TAG, "getPermissionInfo(permName = " + permName + ", flags = " + flags + + ", opPackageName = " + opPackageName + ")"); + return mService.getPermissionInfo(permName, flags, opPackageName); + } + + @Override + public List<PermissionInfo> queryPermissionsByGroup(String groupName, int flags) { + Log.i(LOG_TAG, "queryPermissionsByGroup(groupName = " + groupName + ", flags = " + flags + + ")"); + return mService.queryPermissionsByGroup(groupName, flags); + } + + @Override + public boolean addPermission(PermissionInfo info, boolean async) { + Log.i(LOG_TAG, "addPermission(info = " + info + ", async = " + async + ")"); + return mService.addPermission(info, async); + } + + @Override + public void removePermission(String permName) { + Log.i(LOG_TAG, "removePermission(permName = " + permName + ")"); + mService.removePermission(permName); + } + + @Override + public int getPermissionFlags(String packageName, String permName, int userId) { + Log.i(LOG_TAG, "getPermissionFlags(packageName = " + packageName + ", permName = " + + permName + ", userId = " + userId + ")"); + return mService.getPermissionFlags(packageName, permName, userId); + } + + @Override + public void updatePermissionFlags(String packageName, String permName, int flagMask, + int flagValues, boolean checkAdjustPolicyFlagPermission, int userId) { + Log.i(LOG_TAG, "updatePermissionFlags(packageName = " + packageName + ", permName = " + + permName + ", flagMask = " + flagMask + ", flagValues = " + flagValues + + ", checkAdjustPolicyFlagPermission = " + checkAdjustPolicyFlagPermission + + ", userId = " + userId + ")"); + mService.updatePermissionFlags(packageName, permName, flagMask, flagValues, + checkAdjustPolicyFlagPermission, userId); + } + + @Override + public void updatePermissionFlagsForAllApps(int flagMask, int flagValues, int userId) { + Log.i(LOG_TAG, "updatePermissionFlagsForAllApps(flagMask = " + flagMask + ", flagValues = " + + flagValues + ", userId = " + userId + ")"); + mService.updatePermissionFlagsForAllApps(flagMask, flagValues, userId); + } + + @Override + public void addOnPermissionsChangeListener(IOnPermissionsChangeListener listener) { + Log.i(LOG_TAG, "addOnPermissionsChangeListener(listener = " + listener + ")"); + mService.addOnPermissionsChangeListener(listener); + } + + @Override + public void removeOnPermissionsChangeListener( + IOnPermissionsChangeListener listener) { + Log.i(LOG_TAG, "removeOnPermissionsChangeListener(listener = " + listener + ")"); + mService.removeOnPermissionsChangeListener(listener); + } + + @Override + public boolean addAllowlistedRestrictedPermission(@NonNull String packageName, + @NonNull String permName, int flags, int userId) { + Log.i(LOG_TAG, "addAllowlistedRestrictedPermission(packageName = " + packageName + + ", permName = " + permName + ", flags = " + flags + ", userId = " + userId + ")"); + return mService.addAllowlistedRestrictedPermission(packageName, permName, flags, userId); + } + + @Override + public List<String> getAllowlistedRestrictedPermissions(@NonNull String packageName, int flags, + int userId) { + Log.i(LOG_TAG, "getAllowlistedRestrictedPermissions(packageName = " + packageName + + ", flags = " + flags + ", userId = " + userId + ")"); + return mService.getAllowlistedRestrictedPermissions(packageName, flags, userId); + } + + @Override + public boolean removeAllowlistedRestrictedPermission(@NonNull String packageName, + @NonNull String permName, int flags, int userId) { + Log.i(LOG_TAG, "removeAllowlistedRestrictedPermission(packageName = " + packageName + + ", permName = " + permName + ", flags = " + flags + ", userId = " + userId + ")"); + return mService.removeAllowlistedRestrictedPermission(packageName, permName, flags, userId); + } + + @Override + public void grantRuntimePermission(String packageName, String permName, int userId) { + Log.i(LOG_TAG, "grantRuntimePermission(packageName = " + packageName + ", permName = " + + permName + ", userId = " + userId + ")"); + mService.grantRuntimePermission(packageName, permName, userId); + } + + @Override + public void revokeRuntimePermission(String packageName, String permName, int userId, + String reason) { + Log.i(LOG_TAG, "revokeRuntimePermission(packageName = " + packageName + ", permName = " + + permName + ", userId = " + userId + ", reason = " + reason + ")"); + mService.revokeRuntimePermission(packageName, permName, userId, reason); + } + + @Override + public void revokePostNotificationPermissionWithoutKillForTest(String packageName, int userId) { + Log.i(LOG_TAG, "revokePostNotificationPermissionWithoutKillForTest(packageName = " + + packageName + ", userId = " + userId + ")"); + mService.revokePostNotificationPermissionWithoutKillForTest(packageName, userId); + } + + @Override + public boolean shouldShowRequestPermissionRationale(String packageName, String permName, + int userId) { + Log.i(LOG_TAG, "shouldShowRequestPermissionRationale(packageName = " + packageName + + ", permName = " + permName + ", userId = " + userId + ")"); + return mService.shouldShowRequestPermissionRationale(packageName, permName, userId); + } + + @Override + public boolean isPermissionRevokedByPolicy(String packageName, String permName, int userId) { + Log.i(LOG_TAG, "isPermissionRevokedByPolicy(packageName = " + packageName + ", permName = " + + permName + ", userId = " + userId + ")"); + return mService.isPermissionRevokedByPolicy(packageName, permName, userId); + } + + @Override + public List<SplitPermissionInfoParcelable> getSplitPermissions() { + Log.i(LOG_TAG, "getSplitPermissions()"); + return mService.getSplitPermissions(); + } + + @Override + public int checkPermission(String pkgName, String permName, int userId) { + Log.i(LOG_TAG, "checkPermission(pkgName = " + pkgName + ", permName = " + permName + + ", userId = " + userId + ")"); + return mService.checkPermission(pkgName, permName, userId); + } + + @Override + public int checkUidPermission(int uid, String permName) { + Log.i(LOG_TAG, "checkUidPermission(uid = " + uid + ", permName = " + permName + ")"); + return mService.checkUidPermission(uid, permName); + } + + @Override + public void addOnRuntimePermissionStateChangedListener( + @NonNull PermissionManagerServiceInternal + .OnRuntimePermissionStateChangedListener listener) { + Log.i(LOG_TAG, "addOnRuntimePermissionStateChangedListener(listener = " + listener + ")"); + mService.addOnRuntimePermissionStateChangedListener(listener); + } + + @Override + public void removeOnRuntimePermissionStateChangedListener( + @NonNull PermissionManagerServiceInternal + .OnRuntimePermissionStateChangedListener listener) { + Log.i(LOG_TAG, "removeOnRuntimePermissionStateChangedListener(listener = " + listener + + ")"); + mService.removeOnRuntimePermissionStateChangedListener(listener); + } + + @Override + public Map<String, Set<String>> getAllAppOpPermissionPackages() { + Log.i(LOG_TAG, "getAllAppOpPermissionPackages()"); + return mService.getAllAppOpPermissionPackages(); + } + + @Override + public boolean isPermissionsReviewRequired(@NonNull String packageName, int userId) { + Log.i(LOG_TAG, "isPermissionsReviewRequired(packageName = " + packageName + ", userId = " + + userId + ")"); + return mService.isPermissionsReviewRequired(packageName, userId); + } + + @Override + public void resetRuntimePermissions(@NonNull AndroidPackage pkg, int userId) { + Log.i(LOG_TAG, "resetRuntimePermissions(pkg = " + pkg + ", userId = " + userId + ")"); + mService.resetRuntimePermissions(pkg, userId); + } + + @Override + public void resetRuntimePermissionsForUser(int userId) { + Log.i(LOG_TAG, "resetRuntimePermissionsForUser(userId = " + userId + ")"); + mService.resetRuntimePermissionsForUser(userId); + } + + @Override + public void readLegacyPermissionStateTEMP() { + Log.i(LOG_TAG, "readLegacyPermissionStateTEMP()"); + mService.readLegacyPermissionStateTEMP(); + } + + @Override + public void writeLegacyPermissionStateTEMP() { + Log.i(LOG_TAG, "writeLegacyPermissionStateTEMP()"); + mService.writeLegacyPermissionStateTEMP(); + } + + @NonNull + @Override + public Set<String> getGrantedPermissions(@NonNull String packageName, int userId) { + Log.i(LOG_TAG, "getGrantedPermissions(packageName = " + packageName + ", userId = " + + userId + ")"); + return mService.getGrantedPermissions(packageName, userId); + } + + @NonNull + @Override + public int[] getPermissionGids(@NonNull String permissionName, int userId) { + Log.i(LOG_TAG, "getPermissionGids(permissionName = " + permissionName + ", userId = " + + userId + ")"); + return mService.getPermissionGids(permissionName, userId); + } + + @NonNull + @Override + public String[] getAppOpPermissionPackages(@NonNull String permissionName) { + Log.i(LOG_TAG, "getAppOpPermissionPackages(permissionName = " + permissionName + ")"); + return mService.getAppOpPermissionPackages(permissionName); + } + + @Nullable + @Override + public Permission getPermissionTEMP(@NonNull String permName) { + Log.i(LOG_TAG, "getPermissionTEMP(permName = " + permName + ")"); + return mService.getPermissionTEMP(permName); + } + + @NonNull + @Override + public List<PermissionInfo> getAllPermissionsWithProtection(int protection) { + Log.i(LOG_TAG, "getAllPermissionsWithProtection(protection = " + protection + ")"); + return mService.getAllPermissionsWithProtection(protection); + } + + @NonNull + @Override + public List<PermissionInfo> getAllPermissionsWithProtectionFlags(int protectionFlags) { + Log.i(LOG_TAG, "getAllPermissionsWithProtectionFlags(protectionFlags = " + protectionFlags + + ")"); + return mService.getAllPermissionsWithProtectionFlags(protectionFlags); + } + + @NonNull + @Override + public List<LegacyPermission> getLegacyPermissions() { + Log.i(LOG_TAG, "getLegacyPermissions()"); + return mService.getLegacyPermissions(); + } + + @NonNull + @Override + public LegacyPermissionState getLegacyPermissionState(int appId) { + Log.i(LOG_TAG, "getLegacyPermissionState(appId = " + appId + ")"); + return mService.getLegacyPermissionState(appId); + } + + @Override + public void readLegacyPermissionsTEMP( + @NonNull LegacyPermissionSettings legacyPermissionSettings) { + Log.i(LOG_TAG, "readLegacyPermissionsTEMP(legacyPermissionSettings = " + + legacyPermissionSettings + ")"); + mService.readLegacyPermissionsTEMP(legacyPermissionSettings); + } + + @Override + public void writeLegacyPermissionsTEMP( + @NonNull LegacyPermissionSettings legacyPermissionSettings) { + Log.i(LOG_TAG, "writeLegacyPermissionsTEMP(legacyPermissionSettings = " + + legacyPermissionSettings + ")"); + mService.writeLegacyPermissionsTEMP(legacyPermissionSettings); + } + + @Override + public void onSystemReady() { + Log.i(LOG_TAG, "onSystemReady()"); + mService.onSystemReady(); + } + + @Override + public void onStorageVolumeMounted(@NonNull String volumeUuid, boolean fingerprintChanged) { + Log.i(LOG_TAG, "onStorageVolumeMounted(volumeUuid = " + volumeUuid + + ", fingerprintChanged = " + fingerprintChanged + ")"); + mService.onStorageVolumeMounted(volumeUuid, fingerprintChanged); + } + + @NonNull + @Override + public int[] getGidsForUid(int uid) { + Log.i(LOG_TAG, "getGidsForUid(uid = " + uid + ")"); + return mService.getGidsForUid(uid); + } + + @Override + public void onUserCreated(int userId) { + Log.i(LOG_TAG, "onUserCreated(userId = " + userId + ")"); + mService.onUserCreated(userId); + } + + @Override + public void onUserRemoved(int userId) { + Log.i(LOG_TAG, "onUserRemoved(userId = " + userId + ")"); + mService.onUserRemoved(userId); + } + + @Override + public void onPackageAdded(@NonNull PackageState packageState, boolean isInstantApp, + @Nullable AndroidPackage oldPkg) { + Log.i(LOG_TAG, "onPackageAdded(packageState = " + packageState + ", isInstantApp = " + + isInstantApp + ", oldPkg = " + oldPkg + ")"); + mService.onPackageAdded(packageState, isInstantApp, oldPkg); + } + + @Override + public void onPackageInstalled(@NonNull AndroidPackage pkg, int previousAppId, + @NonNull PermissionManagerServiceInternal.PackageInstalledParams params, int userId) { + Log.i(LOG_TAG, "onPackageInstalled(pkg = " + pkg + ", previousAppId = " + previousAppId + + ", params = " + params + ", userId = " + userId + ")"); + mService.onPackageInstalled(pkg, previousAppId, params, userId); + } + + @Override + public void onPackageRemoved(@NonNull AndroidPackage pkg) { + Log.i(LOG_TAG, "onPackageRemoved(pkg = " + pkg + ")"); + mService.onPackageRemoved(pkg); + } + + @Override + public void onPackageUninstalled(@NonNull String packageName, int appId, + @NonNull PackageState packageState, @NonNull AndroidPackage pkg, + @NonNull List<AndroidPackage> sharedUserPkgs, int userId) { + Log.i(LOG_TAG, "onPackageUninstalled(packageName = " + packageName + ", appId = " + appId + + ", packageState = " + packageState + ", pkg = " + pkg + ", sharedUserPkgs = " + + sharedUserPkgs + ", userId = " + userId + ")"); + mService.onPackageUninstalled(packageName, appId, packageState, pkg, sharedUserPkgs, + userId); + } +} diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java index 075173dd3acd..49f85e9e910a 100644 --- a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -16,9 +16,14 @@ package com.android.server.pm.pkg; +import android.annotation.Dimension; +import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.StringRes; +import android.annotation.StyleRes; import android.annotation.SystemApi; +import android.annotation.XmlRes; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -86,6 +91,109 @@ import java.util.UUID; public interface AndroidPackage { /** + * @see ApplicationInfo#className + * @see R.styleable#AndroidManifestApplication_name + */ + @Nullable + String getApplicationClassName(); + + /** + * @see ApplicationInfo#appComponentFactory + * @see R.styleable#AndroidManifestApplication_appComponentFactory + */ + @Nullable + String getAppComponentFactory(); + + /** + * @see ApplicationInfo#backupAgentName + * @see R.styleable#AndroidManifestApplication_backupAgent + */ + @Nullable + String getBackupAgentName(); + + /** + * @see ApplicationInfo#banner + * @see R.styleable#AndroidManifestApplication_banner + */ + @DrawableRes + int getBannerRes(); + + /** + * @see PackageInfo#baseRevisionCode + * @see R.styleable#AndroidManifest_revisionCode + */ + int getBaseRevisionCode(); + + /** + * @see ApplicationInfo#category + * @see R.styleable#AndroidManifestApplication_appCategory + */ + int getCategory(); + + /** + * @see ApplicationInfo#classLoaderName + * @see R.styleable#AndroidManifestApplication_classLoader + */ + @Nullable + String getClassLoaderName(); + + /** + * @see ApplicationInfo#compatibleWidthLimitDp + * @see R.styleable#AndroidManifestSupportsScreens_compatibleWidthLimitDp + */ + @Dimension(unit = Dimension.DP) + int getCompatibleWidthLimitDp(); + + /** + * @see ApplicationInfo#dataExtractionRulesRes + * @see R.styleable#AndroidManifestApplication_dataExtractionRules + */ + @XmlRes + int getDataExtractionRulesRes(); + + /** + * @see ApplicationInfo#descriptionRes + * @see R.styleable#AndroidManifestApplication_description + */ + @StringRes // This is actually format="reference" + int getDescriptionRes(); + + /** + * @see ApplicationInfo#fullBackupContent + * @see R.styleable#AndroidManifestApplication_fullBackupContent + */ + @XmlRes + int getFullBackupContentRes(); + + /** + * @see ApplicationInfo#getGwpAsanMode() + * @see R.styleable#AndroidManifestApplication_gwpAsanMode + */ + @ApplicationInfo.GwpAsanMode + int getGwpAsanMode(); + + /** + * @see ApplicationInfo#iconRes + * @see R.styleable#AndroidManifestApplication_icon + */ + @DrawableRes + int getIconRes(); + + /** + * @see ApplicationInfo#labelRes + * @see R.styleable#AndroidManifestApplication_label + */ + @StringRes + int getLabelRes(); + + /** + * @see ApplicationInfo#largestWidthLimitDp + * @see R.styleable#AndroidManifestSupportsScreens_largestWidthLimitDp + */ + @Dimension(unit = Dimension.DP) + int getLargestWidthLimitDp(); + + /** * Library names this package is declared as, for use by other packages with "uses-library". * * @see R.styleable#AndroidManifestLibrary @@ -94,12 +202,104 @@ public interface AndroidPackage { List<String> getLibraryNames(); /** + * @see ApplicationInfo#logo + * @see R.styleable#AndroidManifestApplication_logo + */ + @DrawableRes + int getLogoRes(); + + /** + * The resource ID used to provide the application's locales configuration. + * + * @see R.styleable#AndroidManifestApplication_localeConfig + */ + @XmlRes + int getLocaleConfigRes(); + + /** + * @see PackageInfo#getLongVersionCode() + * @see R.styleable#AndroidManifest_versionCode + * @see R.styleable#AndroidManifest_versionCodeMajor + */ + long getLongVersionCode(); + + /** + * @see ApplicationInfo#maxAspectRatio + * @see R.styleable#AndroidManifestApplication_maxAspectRatio + */ + float getMaxAspectRatio(); + + /** + * @see ApplicationInfo#minAspectRatio + * @see R.styleable#AndroidManifestApplication_minAspectRatio + */ + float getMinAspectRatio(); + + /** + * @see ApplicationInfo#getNativeHeapZeroInitialized() + * @see R.styleable#AndroidManifestApplication_nativeHeapZeroInitialized + */ + @ApplicationInfo.NativeHeapZeroInitialized + int getNativeHeapZeroInitialized(); + + /** + * @see ApplicationInfo#networkSecurityConfigRes + * @see R.styleable#AndroidManifestApplication_networkSecurityConfig + */ + @XmlRes + int getNetworkSecurityConfigRes(); + + /** + * @see PackageInfo#requiredAccountType + * @see R.styleable#AndroidManifestApplication_requiredAccountType + */ + @Nullable + String getRequiredAccountType(); + + /** + * @see ApplicationInfo#requiresSmallestWidthDp + * @see R.styleable#AndroidManifestSupportsScreens_requiresSmallestWidthDp + */ + @Dimension(unit = Dimension.DP) + int getRequiresSmallestWidthDp(); + + /** + * The restricted account authenticator type that is used by this application. + * + * @see PackageInfo#restrictedAccountType + * @see R.styleable#AndroidManifestApplication_restrictedAccountType + */ + @Nullable + String getRestrictedAccountType(); + + /** + * @see ApplicationInfo#roundIconRes + * @see R.styleable#AndroidManifestApplication_roundIcon + */ + @DrawableRes + int getRoundIconRes(); + + /** * @see R.styleable#AndroidManifestSdkLibrary_name */ @Nullable String getSdkLibraryName(); /** + * @see PackageInfo#sharedUserId + * @see R.styleable#AndroidManifest_sharedUserId + */ + @Nullable + String getSharedUserId(); + + /** + * @see PackageInfo#sharedUserLabel + * @see R.styleable#AndroidManifest_sharedUserLabel + */ + @StringRes + int getSharedUserLabelRes(); + + /** * @return List of all splits for a package. Note that base.apk is considered a * split and will be provided as index 0 of the list. */ @@ -113,6 +313,12 @@ public interface AndroidPackage { String getStaticSharedLibraryName(); /** + * @see R.styleable#AndroidManifestStaticLibrary_version + * @hide + */ + long getStaticSharedLibraryVersion(); + + /** * @return The {@link UUID} for use with {@link StorageManager} APIs identifying where this * package was installed. */ @@ -126,23 +332,319 @@ public interface AndroidPackage { int getTargetSdkVersion(); /** + * @see ApplicationInfo#theme + * @see R.styleable#AndroidManifestApplication_theme + */ + @StyleRes + int getThemeRes(); + + /** + * @see ApplicationInfo#uiOptions + * @see R.styleable#AndroidManifestApplication_uiOptions + */ + int getUiOptions(); + + /** + * @see PackageInfo#versionName + */ + @Nullable + String getVersionName(); + + /** + * @see ApplicationInfo#zygotePreloadName + * @see R.styleable#AndroidManifestApplication_zygotePreloadName + */ + @Nullable + String getZygotePreloadName(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE + * @see R.styleable#AndroidManifestApplication_allowAudioPlaybackCapture + */ + boolean isAllowAudioPlaybackCapture(); + + /** + * @see ApplicationInfo#FLAG_ALLOW_BACKUP + * @see R.styleable#AndroidManifestApplication_allowBackup + */ + boolean isAllowBackup(); + + /** + * @see ApplicationInfo#FLAG_ALLOW_CLEAR_USER_DATA + * @see R.styleable#AndroidManifestApplication_allowClearUserData + */ + boolean isAllowClearUserData(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE + * @see R.styleable#AndroidManifestApplication_allowClearUserDataOnFailedRestore + */ + boolean isAllowClearUserDataOnFailedRestore(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING + * @see R.styleable#AndroidManifestApplication_allowNativeHeapPointerTagging + */ + boolean isAllowNativeHeapPointerTagging(); + + /** + * @see ApplicationInfo#FLAG_ALLOW_TASK_REPARENTING + * @see R.styleable#AndroidManifestApplication_allowTaskReparenting + */ + boolean isAllowTaskReparenting(); + + /** + * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link + * android.os.Build.VERSION_CODES#DONUT}. + * + * @see R.styleable#AndroidManifestSupportsScreens_anyDensity + * @see ApplicationInfo#FLAG_SUPPORTS_SCREEN_DENSITIES + */ + boolean isAnyDensity(); + + /** + * @see ApplicationInfo#areAttributionsUserVisible() + * @see R.styleable#AndroidManifestApplication_attributionsAreUserVisible + */ + boolean isAttributionsUserVisible(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_BACKUP_IN_FOREGROUND + * @see R.styleable#AndroidManifestApplication_backupInForeground + */ + boolean isBackupInForeground(); + + /** + * @see ApplicationInfo#FLAG_HARDWARE_ACCELERATED + * @see R.styleable#AndroidManifestApplication_hardwareAccelerated + */ + boolean isHardwareAccelerated(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_CANT_SAVE_STATE + * @see R.styleable#AndroidManifestApplication_cantSaveState + */ + boolean isCantSaveState(); + + /** + * @see PackageInfo#coreApp + */ + boolean isCoreApp(); + + /** + * @see ApplicationInfo#crossProfile + * @see R.styleable#AndroidManifestApplication_crossProfile + */ + boolean isCrossProfile(); + + /** * @see ApplicationInfo#FLAG_DEBUGGABLE * @see R.styleable#AndroidManifestApplication_debuggable */ boolean isDebuggable(); /** + * @see ApplicationInfo#PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE + * @see R.styleable#AndroidManifestApplication_defaultToDeviceProtectedStorage + */ + boolean isDefaultToDeviceProtectedStorage(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_DIRECT_BOOT_AWARE + * @see R.styleable#AndroidManifestApplication_directBootAware + */ + boolean isDirectBootAware(); + + /** + * @see ApplicationInfo#FLAG_EXTRACT_NATIVE_LIBS + * @see R.styleable#AndroidManifestApplication_extractNativeLibs + */ + boolean isExtractNativeLibs(); + + /** + * @see ApplicationInfo#FLAG_FACTORY_TEST + */ + boolean isFactoryTest(); + + /** + * @see R.styleable#AndroidManifestApplication_forceQueryable + */ + boolean isForceQueryable(); + + /** + * @see ApplicationInfo#FLAG_FULL_BACKUP_ONLY + * @see R.styleable#AndroidManifestApplication_fullBackupOnly + */ + boolean isFullBackupOnly(); + + /** + * @see ApplicationInfo#FLAG_HAS_CODE + * @see R.styleable#AndroidManifestApplication_hasCode + */ + boolean isHasCode(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_HAS_FRAGILE_USER_DATA + * @see R.styleable#AndroidManifestApplication_hasFragileUserData + */ + boolean isHasFragileUserData(); + + /** * @see ApplicationInfo#PRIVATE_FLAG_ISOLATED_SPLIT_LOADING * @see R.styleable#AndroidManifest_isolatedSplits */ boolean isIsolatedSplitLoading(); /** + * @see ApplicationInfo#FLAG_KILL_AFTER_RESTORE + * @see R.styleable#AndroidManifestApplication_killAfterRestore + */ + boolean isKillAfterRestore(); + + /** + * @see ApplicationInfo#FLAG_LARGE_HEAP + * @see R.styleable#AndroidManifestApplication_largeHeap + */ + boolean isLargeHeap(); + + /** + * Returns true if R.styleable#AndroidManifest_sharedUserMaxSdkVersion is set to a value + * smaller than the current SDK version, indicating the package wants to leave its declared + * {@link #getSharedUserId()}. This only occurs on new installs, pretending the app never + * declared one. + * + * @see R.styleable#AndroidManifest_sharedUserMaxSdkVersion + */ + boolean isLeavingSharedUser(); + + /** + * @see ApplicationInfo#FLAG_MULTIARCH + * @see R.styleable#AndroidManifestApplication_multiArch + */ + boolean isMultiArch(); + + /** + * @see ApplicationInfo#nativeLibraryRootRequiresIsa + */ + boolean isNativeLibraryRootRequiresIsa(); + + /** + * @see R.styleable#AndroidManifestApplication_enableOnBackInvokedCallback + */ + boolean isOnBackInvokedCallbackEnabled(); + + /** + * @see ApplicationInfo#FLAG_PERSISTENT + * @see R.styleable#AndroidManifestApplication_persistent + */ + boolean isPersistent(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_EXT_PROFILEABLE + * @see R.styleable#AndroidManifestProfileable + */ + boolean isProfileable(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_PROFILEABLE_BY_SHELL + * @see R.styleable#AndroidManifestProfileable_shell + */ + boolean isProfileableByShell(); + + /** + * @see ApplicationInfo#PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE + * @see R.styleable#AndroidManifestApplication_requestLegacyExternalStorage + */ + boolean isRequestLegacyExternalStorage(); + + /** + * @see PackageInfo#requiredForAllUsers + * @see R.styleable#AndroidManifestApplication_requiredForAllUsers + */ + boolean isRequiredForAllUsers(); + + /** + * Whether the enabled settings of components in the application should be reset to the default, + * when the application's user data is cleared. + * + * @see R.styleable#AndroidManifestApplication_resetEnabledSettingsOnAppDataCleared + */ + boolean isResetEnabledSettingsOnAppDataCleared(); + + /** + * @see ApplicationInfo#FLAG_RESTORE_ANY_VERSION + * @see R.styleable#AndroidManifestApplication_restoreAnyVersion + */ + boolean isRestoreAnyVersion(); + + /** * @see ApplicationInfo#PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY */ boolean isSignedWithPlatformKey(); /** + * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link + * android.os.Build.VERSION_CODES#GINGERBREAD}. + * + * @see R.styleable#AndroidManifestSupportsScreens_xlargeScreens + * @see ApplicationInfo#FLAG_SUPPORTS_XLARGE_SCREENS + */ + boolean isSupportsExtraLargeScreens(); + + /** + * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link + * android.os.Build.VERSION_CODES#DONUT}. + * + * @see R.styleable#AndroidManifestSupportsScreens_largeScreens + * @see ApplicationInfo#FLAG_SUPPORTS_LARGE_SCREENS + */ + boolean isSupportsLargeScreens(); + + /** + * If omitted from manifest, returns true. + * + * @see R.styleable#AndroidManifestSupportsScreens_normalScreens + * @see ApplicationInfo#FLAG_SUPPORTS_NORMAL_SCREENS + */ + boolean isSupportsNormalScreens(); + + /** + * @see ApplicationInfo#FLAG_SUPPORTS_RTL + * @see R.styleable#AndroidManifestApplication_supportsRtl + */ + boolean isSupportsRtl(); + + /** + * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link + * android.os.Build.VERSION_CODES#DONUT}. + * + * @see R.styleable#AndroidManifestSupportsScreens_smallScreens + * @see ApplicationInfo#FLAG_SUPPORTS_SMALL_SCREENS + */ + boolean isSupportsSmallScreens(); + + /** + * @see ApplicationInfo#FLAG_TEST_ONLY + * @see R.styleable#AndroidManifestApplication_testOnly + */ + boolean isTestOnly(); + + /** + * The install time abi override to choose 32bit abi's when multiple abi's are present. This is + * only meaningful for multiarch applications. The use32bitAbi attribute is ignored if + * cpuAbiOverride is also set. + * + * @see R.attr#use32bitAbi + */ + boolean isUse32BitAbi(); + + /** + * @see ApplicationInfo#FLAG_USES_CLEARTEXT_TRAFFIC + * @see R.styleable#AndroidManifestApplication_usesCleartextTraffic + */ + boolean isUsesCleartextTraffic(); + + /** * @see ApplicationInfo#PRIVATE_FLAG_USE_EMBEDDED_DEX * @see R.styleable#AndroidManifestApplication_useEmbeddedDex */ @@ -163,14 +665,6 @@ public interface AndroidPackage { // Methods below this comment are not yet exposed as API /** - * @see ApplicationInfo#areAttributionsUserVisible() - * @see R.styleable#AndroidManifestApplication_attributionsAreUserVisible - * @hide - */ - @Nullable - boolean areAttributionsUserVisible(); - - /** * Set of Activities parsed from the manifest. * <p> * This contains minimal system state and does not @@ -207,14 +701,6 @@ public interface AndroidPackage { List<ParsedApexSystemService> getApexSystemServices(); /** - * @see ApplicationInfo#appComponentFactory - * @see R.styleable#AndroidManifestApplication_appComponentFactory - * @hide - */ - @Nullable - String getAppComponentFactory(); - - /** * @see R.styleable#AndroidManifestAttribution * @hide */ @@ -232,21 +718,6 @@ public interface AndroidPackage { int getAutoRevokePermissions(); /** - * @see ApplicationInfo#backupAgentName - * @see R.styleable#AndroidManifestApplication_backupAgent - * @hide - */ - @Nullable - String getBackupAgentName(); - - /** - * @see ApplicationInfo#banner - * @see R.styleable#AndroidManifestApplication_banner - * @hide - */ - int getBanner(); - - /** * @see ApplicationInfo#sourceDir * @see ApplicationInfo#getBaseCodePath * @@ -259,43 +730,6 @@ public interface AndroidPackage { String getBaseApkPath(); /** - * @see PackageInfo#baseRevisionCode - * @see R.styleable#AndroidManifest_revisionCode - * @hide - */ - int getBaseRevisionCode(); - - /** - * @see ApplicationInfo#category - * @see R.styleable#AndroidManifestApplication_appCategory - * @hide - */ - int getCategory(); - - /** - * @see ApplicationInfo#classLoaderName - * @see R.styleable#AndroidManifestApplication_classLoader - * @hide - */ - @Nullable - String getClassLoaderName(); - - /** - * @see ApplicationInfo#className - * @see R.styleable#AndroidManifestApplication_name - * @hide - */ - @Nullable - String getClassName(); - - /** - * @see ApplicationInfo#compatibleWidthLimitDp - * @see R.styleable#AndroidManifestSupportsScreens_compatibleWidthLimitDp - * @hide - */ - int getCompatibleWidthLimitDp(); - - /** * @see ApplicationInfo#compileSdkVersion * @see R.styleable#AndroidManifest_compileSdkVersion * @hide @@ -320,20 +754,6 @@ public interface AndroidPackage { List<ConfigurationInfo> getConfigPreferences(); /** - * @see ApplicationInfo#dataExtractionRulesRes - * @see R.styleable#AndroidManifestApplication_dataExtractionRules - * @hide - */ - int getDataExtractionRules(); - - /** - * @see ApplicationInfo#descriptionRes - * @see R.styleable#AndroidManifestApplication_description - * @hide - */ - int getDescriptionRes(); - - /** * @see PackageInfo#featureGroups * @see R.styleable#AndroidManifestUsesFeature * @hide @@ -343,28 +763,6 @@ public interface AndroidPackage { List<FeatureGroupInfo> getFeatureGroups(); /** - * @see ApplicationInfo#fullBackupContent - * @see R.styleable#AndroidManifestApplication_fullBackupContent - * @hide - */ - int getFullBackupContent(); - - /** - * @see ApplicationInfo#getGwpAsanMode() - * @see R.styleable#AndroidManifestApplication_gwpAsanMode - * @hide - */ - @ApplicationInfo.GwpAsanMode - int getGwpAsanMode(); - - /** - * @see ApplicationInfo#iconRes - * @see R.styleable#AndroidManifestApplication_icon - * @hide - */ - int getIconRes(); - - /** * Permissions requested but not in the manifest. These may have been split or migrated from * previous versions/definitions. * @hide @@ -411,43 +809,6 @@ public interface AndroidPackage { Set<String> getKnownActivityEmbeddingCerts(); /** - * @see ApplicationInfo#labelRes - * @see R.styleable#AndroidManifestApplication_label - * @hide - */ - int getLabelRes(); - - /** - * @see ApplicationInfo#largestWidthLimitDp - * @see R.styleable#AndroidManifestSupportsScreens_largestWidthLimitDp - * @hide - */ - int getLargestWidthLimitDp(); - - /** - * The resource ID used to provide the application's locales configuration. - * - * @see R.styleable#AndroidManifestApplication_localeConfig - * @hide - */ - int getLocaleConfigRes(); - - /** - * @see ApplicationInfo#logo - * @see R.styleable#AndroidManifestApplication_logo - * @hide - */ - int getLogo(); - - /** - * @see PackageInfo#getLongVersionCode() - * @see R.styleable#AndroidManifest_versionCode - * @see R.styleable#AndroidManifest_versionCodeMajor - * @hide - */ - long getLongVersionCode(); - - /** * @see ApplicationInfo#manageSpaceActivityName * @see R.styleable#AndroidManifestApplication_manageSpaceActivity * @hide @@ -464,13 +825,6 @@ public interface AndroidPackage { String getManifestPackageName(); /** - * @see ApplicationInfo#maxAspectRatio - * @see R.styleable#AndroidManifestApplication_maxAspectRatio - * @hide - */ - float getMaxAspectRatio(); - - /** * @see R.styleable#AndroidManifestUsesSdk_maxSdkVersion * @hide */ @@ -501,13 +855,6 @@ public interface AndroidPackage { Set<String> getMimeGroups(); /** - * @see ApplicationInfo#minAspectRatio - * @see R.styleable#AndroidManifestApplication_minAspectRatio - * @hide - */ - float getMinAspectRatio(); - - /** * @see R.styleable#AndroidManifestExtensionSdk * @hide */ @@ -523,14 +870,6 @@ public interface AndroidPackage { int getMinSdkVersion(); /** - * @see ApplicationInfo#getNativeHeapZeroInitialized() - * @see R.styleable#AndroidManifestApplication_nativeHeapZeroInitialized - * @hide - */ - @ApplicationInfo.NativeHeapZeroInitialized - int getNativeHeapZeroInitialized(); - - /** * @see ApplicationInfo#nativeLibraryDir * @hide */ @@ -545,13 +884,6 @@ public interface AndroidPackage { String getNativeLibraryRootDir(); /** - * @see ApplicationInfo#networkSecurityConfigRes - * @see R.styleable#AndroidManifestApplication_networkSecurityConfig - * @hide - */ - int getNetworkSecurityConfigRes(); - - /** * If {@link R.styleable#AndroidManifestApplication_label} is a string literal, this is it. * Otherwise, it's stored as {@link #getLabelRes()}. * @@ -787,21 +1119,6 @@ public interface AndroidPackage { List<String> getRequestedPermissions(); /** - * @see PackageInfo#requiredAccountType - * @see R.styleable#AndroidManifestApplication_requiredAccountType - * @hide - */ - @Nullable - String getRequiredAccountType(); - - /** - * @see ApplicationInfo#requiresSmallestWidthDp - * @see R.styleable#AndroidManifestSupportsScreens_requiresSmallestWidthDp - * @hide - */ - int getRequiresSmallestWidthDp(); - - /** * Whether or not the app requested explicitly resizeable Activities. Null value means nothing * was explicitly requested. * @@ -824,23 +1141,6 @@ public interface AndroidPackage { byte[] getRestrictUpdateHash(); /** - * The restricted account authenticator type that is used by this application. - * - * @see PackageInfo#restrictedAccountType - * @see R.styleable#AndroidManifestApplication_restrictedAccountType - * @hide - */ - @Nullable - String getRestrictedAccountType(); - - /** - * @see ApplicationInfo#roundIconRes - * @see R.styleable#AndroidManifestApplication_roundIcon - * @hide - */ - int getRoundIconRes(); - - /** * @see R.styleable#AndroidManifestSdkLibrary_versionMajor * @hide */ @@ -872,21 +1172,6 @@ public interface AndroidPackage { List<ParsedService> getServices(); /** - * @see PackageInfo#sharedUserId - * @see R.styleable#AndroidManifest_sharedUserId - * @hide - */ - @Nullable - String getSharedUserId(); - - /** - * @see PackageInfo#sharedUserLabel - * @see R.styleable#AndroidManifest_sharedUserLabel - * @hide - */ - int getSharedUserLabel(); - - /** * The signature data of all APKs in this package, which must be exactly the same across the * base and splits. * @hide @@ -949,12 +1234,6 @@ public interface AndroidPackage { int[] getSplitRevisionCodes(); /** - * @see R.styleable#AndroidManifestStaticLibrary_version - * @hide - */ - long getStaticSharedLibVersion(); - - /** * @see ApplicationInfo#targetSandboxVersion * @see R.styleable#AndroidManifest_targetSandboxVersion * @hide @@ -970,20 +1249,6 @@ public interface AndroidPackage { String getTaskAffinity(); /** - * @see ApplicationInfo#theme - * @see R.styleable#AndroidManifestApplication_theme - * @hide - */ - int getTheme(); - - /** - * @see ApplicationInfo#uiOptions - * @see R.styleable#AndroidManifestApplication_uiOptions - * @hide - */ - int getUiOptions(); - - /** * This is an appId, the {@link ApplicationInfo#uid} if the user ID is * {@link android.os.UserHandle#SYSTEM}. * @@ -1095,27 +1360,12 @@ public interface AndroidPackage { long[] getUsesStaticLibrariesVersions(); /** - * @see PackageInfo#versionName - * @hide - */ - @Nullable - String getVersionName(); - - /** * @see ApplicationInfo#volumeUuid * @hide */ @Nullable String getVolumeUuid(); - /** - * @see ApplicationInfo#zygotePreloadName - * @see R.styleable#AndroidManifestApplication_zygotePreloadName - * @hide - */ - @Nullable - String getZygotePreloadName(); - /** @hide */ boolean hasPreserveLegacyExternalStorage(); @@ -1133,110 +1383,10 @@ public interface AndroidPackage { */ Boolean hasRequestRawExternalStorageAccess(); - /** - * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_AUDIO_PLAYBACK_CAPTURE - * @see R.styleable#AndroidManifestApplication_allowAudioPlaybackCapture - * @hide - */ - boolean isAllowAudioPlaybackCapture(); - - /** - * @see ApplicationInfo#FLAG_ALLOW_BACKUP - * @see R.styleable#AndroidManifestApplication_allowBackup - * @hide - */ - boolean isAllowBackup(); - - /** - * @see ApplicationInfo#FLAG_ALLOW_CLEAR_USER_DATA - * @see R.styleable#AndroidManifestApplication_allowClearUserData - * @hide - */ - boolean isAllowClearUserData(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_CLEAR_USER_DATA_ON_FAILED_RESTORE - * @see R.styleable#AndroidManifestApplication_allowClearUserDataOnFailedRestore - * @hide - */ - boolean isAllowClearUserDataOnFailedRestore(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_ALLOW_NATIVE_HEAP_POINTER_TAGGING - * @see R.styleable#AndroidManifestApplication_allowNativeHeapPointerTagging - * @hide - */ - boolean isAllowNativeHeapPointerTagging(); - - /** - * @see ApplicationInfo#FLAG_ALLOW_TASK_REPARENTING - * @see R.styleable#AndroidManifestApplication_allowTaskReparenting - * @hide - */ - boolean isAllowTaskReparenting(); - - /** - * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link - * android.os.Build.VERSION_CODES#DONUT}. - * - * @see R.styleable#AndroidManifestSupportsScreens_anyDensity - * @see ApplicationInfo#FLAG_SUPPORTS_SCREEN_DENSITIES - * @hide - */ - boolean isAnyDensity(); - /** @hide */ boolean isApex(); /** - * @see ApplicationInfo#PRIVATE_FLAG_BACKUP_IN_FOREGROUND - * @see R.styleable#AndroidManifestApplication_backupInForeground - * @hide - */ - boolean isBackupInForeground(); - - /** - * @see ApplicationInfo#FLAG_HARDWARE_ACCELERATED - * @see R.styleable#AndroidManifestApplication_hardwareAccelerated - * @hide - */ - boolean isBaseHardwareAccelerated(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_CANT_SAVE_STATE - * @see R.styleable#AndroidManifestApplication_cantSaveState - * @hide - */ - boolean isCantSaveState(); - - /** - * @see PackageInfo#coreApp - * @hide - */ - boolean isCoreApp(); - - /** - * @see ApplicationInfo#crossProfile - * @see R.styleable#AndroidManifestApplication_crossProfile - * @hide - */ - boolean isCrossProfile(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE - * @see R.styleable#AndroidManifestApplication_defaultToDeviceProtectedStorage - * @hide - */ - boolean isDefaultToDeviceProtectedStorage(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_DIRECT_BOOT_AWARE - * @see R.styleable#AndroidManifestApplication_directBootAware - * @hide - */ - boolean isDirectBootAware(); - - /** * @see ApplicationInfo#enabled * @see R.styleable#AndroidManifestApplication_enabled * @hide @@ -1250,32 +1400,6 @@ public interface AndroidPackage { boolean isExternalStorage(); /** - * @see ApplicationInfo#FLAG_EXTRACT_NATIVE_LIBS - * @see R.styleable#AndroidManifestApplication_extractNativeLibs - * @hide - */ - boolean isExtractNativeLibs(); - - /** - * @see ApplicationInfo#FLAG_FACTORY_TEST - * @hide - */ - boolean isFactoryTest(); - - /** - * @see R.styleable#AndroidManifestApplication_forceQueryable - * @hide - */ - boolean isForceQueryable(); - - /** - * @see ApplicationInfo#FLAG_FULL_BACKUP_ONLY - * @see R.styleable#AndroidManifestApplication_fullBackupOnly - * @hide - */ - boolean isFullBackupOnly(); - - /** * @see ApplicationInfo#FLAG_IS_GAME * @see R.styleable#AndroidManifestApplication_isGame * @hide @@ -1284,13 +1408,6 @@ public interface AndroidPackage { boolean isGame(); /** - * @see ApplicationInfo#FLAG_HAS_CODE - * @see R.styleable#AndroidManifestApplication_hasCode - * @hide - */ - boolean isHasCode(); - - /** * @see ApplicationInfo#PRIVATE_FLAG_HAS_DOMAIN_URLS * @see R.styleable#AndroidManifestIntentFilter * @hide @@ -1298,55 +1415,6 @@ public interface AndroidPackage { boolean isHasDomainUrls(); /** - * @see ApplicationInfo#PRIVATE_FLAG_HAS_FRAGILE_USER_DATA - * @see R.styleable#AndroidManifestApplication_hasFragileUserData - * @hide - */ - boolean isHasFragileUserData(); - - /** - * @see ApplicationInfo#FLAG_KILL_AFTER_RESTORE - * @see R.styleable#AndroidManifestApplication_killAfterRestore - * @hide - */ - boolean isKillAfterRestore(); - - /** - * @see ApplicationInfo#FLAG_LARGE_HEAP - * @see R.styleable#AndroidManifestApplication_largeHeap - * @hide - */ - boolean isLargeHeap(); - - /** - * Returns true if R.styleable#AndroidManifest_sharedUserMaxSdkVersion is set to a value - * smaller than the current SDK version. - * - * @see R.styleable#AndroidManifest_sharedUserMaxSdkVersion - * @hide - */ - boolean isLeavingSharedUid(); - - /** - * @see ApplicationInfo#FLAG_MULTIARCH - * @see R.styleable#AndroidManifestApplication_multiArch - * @hide - */ - boolean isMultiArch(); - - /** - * @see ApplicationInfo#nativeLibraryRootRequiresIsa - * @hide - */ - boolean isNativeLibraryRootRequiresIsa(); - - /** - * @see R.styleable#AndroidManifestApplication_enableOnBackInvokedCallback - * @hide - */ - boolean isOnBackInvokedCallbackEnabled(); - - /** * @see ApplicationInfo#PRIVATE_FLAG_IS_RESOURCE_OVERLAY * @see ApplicationInfo#isResourceOverlay() * @see R.styleable#AndroidManifestResourceOverlay @@ -1371,50 +1439,6 @@ public interface AndroidPackage { boolean isPartiallyDirectBootAware(); /** - * @see ApplicationInfo#FLAG_PERSISTENT - * @see R.styleable#AndroidManifestApplication_persistent - * @hide - */ - boolean isPersistent(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_EXT_PROFILEABLE - * @see R.styleable#AndroidManifestProfileable - * @hide - */ - boolean isProfileable(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_PROFILEABLE_BY_SHELL - * @see R.styleable#AndroidManifestProfileable_shell - * @hide - */ - boolean isProfileableByShell(); - - /** - * @see ApplicationInfo#PRIVATE_FLAG_REQUEST_LEGACY_EXTERNAL_STORAGE - * @see R.styleable#AndroidManifestApplication_requestLegacyExternalStorage - * @hide - */ - boolean isRequestLegacyExternalStorage(); - - /** - * @see PackageInfo#requiredForAllUsers - * @see R.styleable#AndroidManifestApplication_requiredForAllUsers - * @hide - */ - boolean isRequiredForAllUsers(); - - /** - * Whether the enabled settings of components in the application should be reset to the default, - * when the application's user data is cleared. - * - * @see R.styleable#AndroidManifestApplication_resetEnabledSettingsOnAppDataCleared - * @hide - */ - boolean isResetEnabledSettingsOnAppDataCleared(); - - /** * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link * android.os.Build.VERSION_CODES#DONUT}. * @@ -1432,13 +1456,6 @@ public interface AndroidPackage { boolean isResizeableActivityViaSdkVersion(); /** - * @see ApplicationInfo#FLAG_RESTORE_ANY_VERSION - * @see R.styleable#AndroidManifestApplication_restoreAnyVersion - * @hide - */ - boolean isRestoreAnyVersion(); - - /** * True means that this package/app contains an SDK library. * @see R.styleable#AndroidManifestSdkLibrary * @hide @@ -1459,76 +1476,6 @@ public interface AndroidPackage { boolean isStub(); /** - * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link - * android.os.Build.VERSION_CODES#GINGERBREAD}. - * - * @see R.styleable#AndroidManifestSupportsScreens_xlargeScreens - * @see ApplicationInfo#FLAG_SUPPORTS_XLARGE_SCREENS - * @hide - */ - boolean isSupportsExtraLargeScreens(); - - /** - * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link - * android.os.Build.VERSION_CODES#DONUT}. - * - * @see R.styleable#AndroidManifestSupportsScreens_largeScreens - * @see ApplicationInfo#FLAG_SUPPORTS_LARGE_SCREENS - * @hide - */ - boolean isSupportsLargeScreens(); - - /** - * If omitted from manifest, returns true. - * - * @see R.styleable#AndroidManifestSupportsScreens_normalScreens - * @see ApplicationInfo#FLAG_SUPPORTS_NORMAL_SCREENS - * @hide - */ - boolean isSupportsNormalScreens(); - - /** - * @see ApplicationInfo#FLAG_SUPPORTS_RTL - * @see R.styleable#AndroidManifestApplication_supportsRtl - * @hide - */ - boolean isSupportsRtl(); - - /** - * If omitted from manifest, returns true if {@link #getTargetSdkVersion()} >= {@link - * android.os.Build.VERSION_CODES#DONUT}. - * - * @see R.styleable#AndroidManifestSupportsScreens_smallScreens - * @see ApplicationInfo#FLAG_SUPPORTS_SMALL_SCREENS - * @hide - */ - boolean isSupportsSmallScreens(); - - /** - * @see ApplicationInfo#FLAG_TEST_ONLY - * @see R.styleable#AndroidManifestApplication_testOnly - * @hide - */ - boolean isTestOnly(); - - /** - * The install time abi override to choose 32bit abi's when multiple abi's are present. This is - * only meaningful for multiarch applications. The use32bitAbi attribute is ignored if - * cpuAbiOverride is also set. - * - * @see R.attr#use32bitAbi - * @hide - */ - boolean isUse32BitAbi(); - - /** - * @see ApplicationInfo#FLAG_USES_CLEARTEXT_TRAFFIC - * @see R.styleable#AndroidManifestApplication_usesCleartextTraffic - * @hide - */ - boolean isUsesCleartextTraffic(); - - /** * Set if the any of components are visible to instant applications. * * @see R.styleable#AndroidManifestActivity_visibleToInstantApps diff --git a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java index ea791e1f29db..3bd0e0dab800 100644 --- a/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java +++ b/services/core/java/com/android/server/pm/pkg/component/ParsedActivityUtils.java @@ -144,7 +144,7 @@ public class ParsedActivityUtils { | flag(ActivityInfo.FLAG_SYSTEM_USER_ONLY, R.styleable.AndroidManifestActivity_systemUserOnly, sa))); if (!receiver) { - activity.setFlags(activity.getFlags() | (flag(ActivityInfo.FLAG_HARDWARE_ACCELERATED, R.styleable.AndroidManifestActivity_hardwareAccelerated, pkg.isBaseHardwareAccelerated(), sa) + activity.setFlags(activity.getFlags() | (flag(ActivityInfo.FLAG_HARDWARE_ACCELERATED, R.styleable.AndroidManifestActivity_hardwareAccelerated, pkg.isHardwareAccelerated(), sa) | flag(ActivityInfo.FLAG_ALLOW_EMBEDDED, R.styleable.AndroidManifestActivity_allowEmbedded, sa) | flag(ActivityInfo.FLAG_ALWAYS_FOCUSABLE, R.styleable.AndroidManifestActivity_alwaysFocusable, sa) | flag(ActivityInfo.FLAG_AUTO_REMOVE_FROM_RECENTS, R.styleable.AndroidManifestActivity_autoRemoveFromRecents, sa) diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java index 16f5d1617a8f..12dfef4f5af3 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java @@ -155,7 +155,7 @@ public interface ParsingPackage { ParsingPackage setUiOptions(int uiOptions); - ParsingPackage setBaseHardwareAccelerated(boolean baseHardwareAccelerated); + ParsingPackage setHardwareAccelerated(boolean hardwareAccelerated); ParsingPackage setResizeableActivity(Boolean resizeable); @@ -255,13 +255,13 @@ public interface ParsingPackage { ParsingPackage setBackupAgentName(String backupAgentName); - ParsingPackage setBanner(int banner); + ParsingPackage setBannerRes(int banner); ParsingPackage setCategory(int category); ParsingPackage setClassLoaderName(String classLoaderName); - ParsingPackage setClassName(String className); + ParsingPackage setApplicationClassName(String className); ParsingPackage setCompatibleWidthLimitDp(int compatibleWidthLimitDp); @@ -281,9 +281,9 @@ public interface ParsingPackage { ParsingPackage setCrossProfile(boolean crossProfile); - ParsingPackage setFullBackupContent(int fullBackupContent); + ParsingPackage setFullBackupContentRes(int fullBackupContentRes); - ParsingPackage setDataExtractionRules(int dataExtractionRules); + ParsingPackage setDataExtractionRulesRes(int dataExtractionRulesRes); ParsingPackage setHasDomainUrls(boolean hasDomainUrls); @@ -292,13 +292,13 @@ public interface ParsingPackage { ParsingPackage setInstallLocation(int installLocation); /** @see R#styleable.AndroidManifest_sharedUserMaxSdkVersion */ - ParsingPackage setLeavingSharedUid(boolean leavingSharedUid); + ParsingPackage setLeavingSharedUser(boolean leavingSharedUser); ParsingPackage setLabelRes(int labelRes); ParsingPackage setLargestWidthLimitDp(int largestWidthLimitDp); - ParsingPackage setLogo(int logo); + ParsingPackage setLogoRes(int logo); ParsingPackage setManageSpaceActivityName(String manageSpaceActivityName); @@ -336,13 +336,13 @@ public interface ParsingPackage { ParsingPackage setRoundIconRes(int roundIconRes); - ParsingPackage setSharedUserLabel(int sharedUserLabel); + ParsingPackage setSharedUserLabelRes(int sharedUserLabelRes); ParsingPackage setSigningDetails(@NonNull SigningDetails signingDetails); ParsingPackage setSplitClassLoaderName(int splitIndex, String classLoaderName); - ParsingPackage setStaticSharedLibVersion(long staticSharedLibVersion); + ParsingPackage setStaticSharedLibraryVersion(long staticSharedLibraryVersion); ParsingPackage setSupportsLargeScreens(int supportsLargeScreens); @@ -354,7 +354,7 @@ public interface ParsingPackage { ParsingPackage setTargetSandboxVersion(int targetSandboxVersion); - ParsingPackage setTheme(int theme); + ParsingPackage setThemeRes(int theme); ParsingPackage setRequestForegroundServiceExemption(boolean requestForegroundServiceExemption); @@ -512,7 +512,7 @@ public interface ParsingPackage { boolean isAnyDensity(); - boolean isBaseHardwareAccelerated(); + boolean isHardwareAccelerated(); boolean isCantSaveState(); diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index c6e1793c0ebb..2a2640d13f20 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -1044,9 +1044,9 @@ public class ParsingPackageUtils { } return input.success(pkg - .setLeavingSharedUid(leaving) + .setLeavingSharedUser(leaving) .setSharedUserId(str.intern()) - .setSharedUserLabel(resId(R.styleable.AndroidManifest_sharedUserLabel, sa))); + .setSharedUserLabelRes(resId(R.styleable.AndroidManifest_sharedUserLabel, sa))); } private static ParseResult<ParsingPackage> parseKeySets(ParseInput input, @@ -1869,7 +1869,7 @@ public class ParsingPackageUtils { return input.error("Empty class name in package " + packageName); } - pkg.setClassName(outInfoName); + pkg.setApplicationClassName(outInfoName); } TypedValue labelValue = sa.peekValue(R.styleable.AndroidManifestApplication_label); @@ -1939,7 +1939,7 @@ public class ParsingPackageUtils { fullBackupContent = v.data == 0 ? -1 : 0; } - pkg.setFullBackupContent(fullBackupContent); + pkg.setFullBackupContentRes(fullBackupContent); } if (DEBUG_BACKUP) { Slog.v(TAG, "fullBackupContent=" + fullBackupContent + " for " + pkgName); @@ -2247,7 +2247,7 @@ public class ParsingPackageUtils { .setOnBackInvokedCallbackEnabled(bool(false, R.styleable.AndroidManifestApplication_enableOnBackInvokedCallback, sa)) // targetSdkVersion gated .setAllowAudioPlaybackCapture(bool(targetSdk >= Build.VERSION_CODES.Q, R.styleable.AndroidManifestApplication_allowAudioPlaybackCapture, sa)) - .setBaseHardwareAccelerated(bool(targetSdk >= Build.VERSION_CODES.ICE_CREAM_SANDWICH, R.styleable.AndroidManifestApplication_hardwareAccelerated, sa)) + .setHardwareAccelerated(bool(targetSdk >= Build.VERSION_CODES.ICE_CREAM_SANDWICH, R.styleable.AndroidManifestApplication_hardwareAccelerated, sa)) .setRequestLegacyExternalStorage(bool(targetSdk < Build.VERSION_CODES.Q, R.styleable.AndroidManifestApplication_requestLegacyExternalStorage, sa)) .setUsesCleartextTraffic(bool(targetSdk < Build.VERSION_CODES.P, R.styleable.AndroidManifestApplication_usesCleartextTraffic, sa)) // Ints Default 0 @@ -2258,14 +2258,14 @@ public class ParsingPackageUtils { .setMaxAspectRatio(aFloat(R.styleable.AndroidManifestApplication_maxAspectRatio, sa)) .setMinAspectRatio(aFloat(R.styleable.AndroidManifestApplication_minAspectRatio, sa)) // Resource ID - .setBanner(resId(R.styleable.AndroidManifestApplication_banner, sa)) + .setBannerRes(resId(R.styleable.AndroidManifestApplication_banner, sa)) .setDescriptionRes(resId(R.styleable.AndroidManifestApplication_description, sa)) .setIconRes(resId(R.styleable.AndroidManifestApplication_icon, sa)) - .setLogo(resId(R.styleable.AndroidManifestApplication_logo, sa)) + .setLogoRes(resId(R.styleable.AndroidManifestApplication_logo, sa)) .setNetworkSecurityConfigRes(resId(R.styleable.AndroidManifestApplication_networkSecurityConfig, sa)) .setRoundIconRes(resId(R.styleable.AndroidManifestApplication_roundIcon, sa)) - .setTheme(resId(R.styleable.AndroidManifestApplication_theme, sa)) - .setDataExtractionRules( + .setThemeRes(resId(R.styleable.AndroidManifestApplication_theme, sa)) + .setDataExtractionRulesRes( resId(R.styleable.AndroidManifestApplication_dataExtractionRules, sa)) .setLocaleConfigRes(resId(R.styleable.AndroidManifestApplication_localeConfig, sa)) // Strings @@ -2399,7 +2399,7 @@ public class ParsingPackageUtils { } return input.success(pkg.setStaticSharedLibraryName(lname.intern()) - .setStaticSharedLibVersion( + .setStaticSharedLibraryVersion( PackageInfo.composeLongVersionCode(versionMajor, version)) .setStaticSharedLibrary(true)); } finally { @@ -2694,7 +2694,7 @@ public class ParsingPackageUtils { // Build custom App Details activity info instead of parsing it from xml return input.success(ParsedActivity.makeAppDetailsActivity(packageName, pkg.getProcessName(), pkg.getUiOptions(), taskAffinity, - pkg.isBaseHardwareAccelerated())); + pkg.isHardwareAccelerated())); } /** diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index f173f7ab8b3d..966e88354b91 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -34,6 +34,7 @@ import android.media.tv.AdRequest; import android.media.tv.AdResponse; import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; +import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.media.tv.interactive.AppLinkInfo; import android.media.tv.interactive.ITvInteractiveAppClient; @@ -1369,6 +1370,58 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void sendTvRecordingInfo(IBinder sessionToken, TvRecordingInfo recordingInfo, + int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendTvRecordingInfo(recordingInfo=%s)", recordingInfo); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendTvRecordingInfo"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).sendTvRecordingInfo(recordingInfo); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendTvRecordingInfo", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void sendTvRecordingInfoList(IBinder sessionToken, + List<TvRecordingInfo> recordingInfoList, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendTvRecordingInfoList(type=%s)", recordingInfoList); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendTvRecordingInfoList"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).sendTvRecordingInfoList(recordingInfoList); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendTvRecordingInfoList", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void sendSigningResult( IBinder sessionToken, String signingId, byte[] result, int userId) { if (DEBUG) { @@ -2315,6 +2368,59 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onSetTvRecordingInfo(String recordingId, TvRecordingInfo recordingInfo) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onSetTvRecordingInfo"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onSetTvRecordingInfo(recordingId, recordingInfo, + mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onSetTvRecordingInfo", e); + } + } + } + + @Override + public void onRequestTvRecordingInfo(String recordingId) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestTvRecordingInfo"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestTvRecordingInfo(recordingId, mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestTvRecordingInfo", e); + } + } + } + + @Override + public void onRequestTvRecordingInfoList(int type) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestTvRecordingInfoList"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestTvRecordingInfoList(type, mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestTvRecordingInfoList", e); + } + } + } + + + @Override public void onRequestSigning(String id, String algorithm, String alias, byte[] data) { synchronized (mLock) { if (DEBUG) { diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 3fa5efcaf0bd..a16e65961013 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -100,7 +100,6 @@ import com.android.internal.app.AssistUtils; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.protolog.ProtoLogGroup; import com.android.internal.protolog.common.ProtoLog; -import com.android.internal.util.FrameworkStatsLog; import com.android.server.LocalServices; import com.android.server.Watchdog; import com.android.server.pm.KnownPackages; @@ -491,46 +490,13 @@ class ActivityClientController extends IActivityClientController.Stub { finishTask == Activity.FINISH_TASK_WITH_ROOT_ACTIVITY; if (finishTask == Activity.FINISH_TASK_WITH_ACTIVITY || (finishWithRootActivity && r == rootR)) { - ActivityRecord topActivity = - r.getTask().getTopNonFinishingActivity(); - boolean passesAsmChecks = topActivity != null - && topActivity.getUid() == r.getUid(); - if (!passesAsmChecks) { - Slog.i(TAG, "Finishing task from background. r: " + r); - FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, - /* caller_uid */ - r.getUid(), - /* caller_activity_class_name */ - r.info.name, - /* target_task_top_activity_uid */ - topActivity == null ? -1 : topActivity.getUid(), - /* target_task_top_activity_class_name */ - topActivity == null ? null : topActivity.info.name, - /* target_task_is_different */ - false, - /* target_activity_uid */ - -1, - /* target_activity_class_name */ - null, - /* target_intent_action */ - null, - /* target_intent_flags */ - 0, - /* action */ - FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__FINISH_TASK, - /* version */ - 1, - /* multi_window */ - false - ); - } // If requested, remove the task that is associated to this activity only if it // was the root activity in the task. The result code and data is ignored // because we don't support returning them across task boundaries. Also, to // keep backwards compatibility we remove the task from recents when finishing // task with root activity. mTaskSupervisor.removeTask(tr, false /*killProcess*/, - finishWithRootActivity, "finish-activity"); + finishWithRootActivity, "finish-activity", r.getUid(), r.info.name); res = true; // Explicitly dismissing the activity so reset its relaunch flag. r.mRelaunchReason = RELAUNCH_REASON_NONE; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index bf4e25c3b14c..c1a68de430a3 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -9359,7 +9359,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A preserveWindow); final ActivityLifecycleItem lifecycleItem; if (andResume) { - lifecycleItem = ResumeActivityItem.obtain(isTransitionForward()); + lifecycleItem = ResumeActivityItem.obtain(isTransitionForward(), + shouldSendCompatFakeFocus()); } else { lifecycleItem = PauseActivityItem.obtain(); } @@ -10120,6 +10121,18 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + /** + * Whether we should send fake focus when the activity is resumed. This is done because some + * game engines wait to get focus before drawing the content of the app. + */ + // TODO(b/263593361): Explore enabling compat fake focus for freeform. + // TODO(b/263592337): Explore enabling compat fake focus for fullscreen, e.g. for when + // covered with bubbles. + boolean shouldSendCompatFakeFocus() { + return mWmService.mLetterboxConfiguration.isCompatFakeFocusEnabled() && inMultiWindowMode() + && !inPinnedWindowingMode() && !inFreeformWindowingMode(); + } + static class Builder { private final ActivityTaskManagerService mAtmService; private WindowProcessController mCallerApp; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index ff50fd4eb9b7..b4af69e14780 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -74,6 +74,8 @@ import static com.android.server.wm.ActivityTaskManagerService.ANIMATE; import static com.android.server.wm.ActivityTaskSupervisor.DEFER_RESUME; import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP; import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_DEFAULT; +import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY; import static com.android.server.wm.Task.REPARENT_MOVE_ROOT_TASK_TO_FRONT; @@ -130,6 +132,7 @@ import com.android.server.power.ShutdownCheckPoints; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.uri.NeededUriGrants; import com.android.server.wm.ActivityMetricsLogger.LaunchingState; +import com.android.server.wm.BackgroundActivityStartController.BalCode; import com.android.server.wm.LaunchParamsController.LaunchParams; import com.android.server.wm.TaskFragment.EmbeddingCheckResult; @@ -171,9 +174,10 @@ class ActivityStarter { private int mCallingUid; private ActivityOptions mOptions; - // If it is true, background activity can only be started in an existing task that contains + // If it is BAL_BLOCK, background activity can only be started in an existing task that contains // an activity with same uid, or if activity starts are enabled in developer options. - private boolean mRestrictedBgActivity; + @BalCode + private int mBalCode; private int mLaunchMode; private boolean mLaunchTaskBehind; @@ -590,7 +594,7 @@ class ActivityStarter { mIntent = starter.mIntent; mCallingUid = starter.mCallingUid; mOptions = starter.mOptions; - mRestrictedBgActivity = starter.mRestrictedBgActivity; + mBalCode = starter.mBalCode; mLaunchTaskBehind = starter.mLaunchTaskBehind; mLaunchFlags = starter.mLaunchFlags; @@ -1024,15 +1028,15 @@ class ActivityStarter { ActivityOptions checkedOptions = options != null ? options.getOptions(intent, aInfo, callerApp, mSupervisor) : null; - boolean restrictedBgActivity = false; + @BalCode int balCode = BAL_ALLOW_DEFAULT; if (!abort) { try { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "shouldAbortBackgroundActivityStart"); BackgroundActivityStartController balController = mController.getBackgroundActivityLaunchController(); - restrictedBgActivity = - balController.shouldAbortBackgroundActivityStart( + balCode = + balController.checkBackgroundActivityStart( callingUid, callingPid, callingPackage, @@ -1221,13 +1225,13 @@ class ActivityStarter { WindowProcessController homeProcess = mService.mHomeProcess; boolean isHomeProcess = homeProcess != null && aInfo.applicationInfo.uid == homeProcess.mUid; - if (!restrictedBgActivity && !isHomeProcess) { + if (balCode != BAL_BLOCK && !isHomeProcess) { mService.resumeAppSwitches(); } mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession, request.voiceInteractor, startFlags, checkedOptions, - inTask, inTaskFragment, restrictedBgActivity, intentGrants); + inTask, inTaskFragment, balCode, intentGrants); if (request.outActivity != null) { request.outActivity[0] = mLastStartActivityRecord; @@ -1377,7 +1381,7 @@ class ActivityStarter { private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags, ActivityOptions options, Task inTask, - TaskFragment inTaskFragment, boolean restrictedBgActivity, + TaskFragment inTaskFragment, @BalCode int balCode, NeededUriGrants intentGrants) { int result = START_CANCELED; final Task startedActivityRootTask; @@ -1397,7 +1401,7 @@ class ActivityStarter { try { Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "startActivityInner"); result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor, - startFlags, options, inTask, inTaskFragment, restrictedBgActivity, + startFlags, options, inTask, inTaskFragment, balCode, intentGrants); } finally { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); @@ -1544,10 +1548,10 @@ class ActivityStarter { int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags, ActivityOptions options, Task inTask, - TaskFragment inTaskFragment, boolean restrictedBgActivity, + TaskFragment inTaskFragment, @BalCode int balCode, NeededUriGrants intentGrants) { setInitialState(r, options, inTask, inTaskFragment, startFlags, sourceRecord, - voiceSession, voiceInteractor, restrictedBgActivity); + voiceSession, voiceInteractor, balCode); computeLaunchingTaskFlags(); mIntent.setFlags(mLaunchFlags); @@ -1800,7 +1804,8 @@ class ActivityStarter { || !targetTask.isUidPresent(mCallingUid) || (LAUNCH_SINGLE_INSTANCE == mLaunchMode && targetTask.inPinnedWindowingMode())); - if (mRestrictedBgActivity && blockBalInTask && handleBackgroundActivityAbort(r)) { + if (mBalCode == BAL_BLOCK && blockBalInTask + && handleBackgroundActivityAbort(r)) { Slog.e(TAG, "Abort background activity starts from " + mCallingUid); return START_ABORTED; } @@ -2209,7 +2214,7 @@ class ActivityStarter { mIntent = null; mCallingUid = -1; mOptions = null; - mRestrictedBgActivity = false; + mBalCode = BAL_ALLOW_DEFAULT; mLaunchTaskBehind = false; mLaunchFlags = 0; @@ -2254,7 +2259,7 @@ class ActivityStarter { private void setInitialState(ActivityRecord r, ActivityOptions options, Task inTask, TaskFragment inTaskFragment, int startFlags, ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession, - IVoiceInteractor voiceInteractor, boolean restrictedBgActivity) { + IVoiceInteractor voiceInteractor, @BalCode int balCode) { reset(false /* clearRequest */); mStartActivity = r; @@ -2265,7 +2270,7 @@ class ActivityStarter { mSourceRootTask = mSourceRecord != null ? mSourceRecord.getRootTask() : null; mVoiceSession = voiceSession; mVoiceInteractor = voiceInteractor; - mRestrictedBgActivity = restrictedBgActivity; + mBalCode = balCode; mLaunchParams.reset(); @@ -2418,7 +2423,7 @@ class ActivityStarter { mNoAnimation = (mLaunchFlags & FLAG_ACTIVITY_NO_ANIMATION) != 0; - if (mRestrictedBgActivity && !mService.isBackgroundActivityStartsEnabled()) { + if (mBalCode == BAL_BLOCK && !mService.isBackgroundActivityStartsEnabled()) { mAvoidMoveToFront = true; mDoResume = false; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index c63bd52900e8..ef126a97e7a4 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -29,6 +29,7 @@ import android.content.IIntentSender; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; import android.os.Bundle; import android.os.IBinder; import android.os.LocaleList; @@ -622,10 +623,19 @@ public abstract class ActivityTaskManagerInternal { @Nullable public final LocaleList mLocales; + /** + * Gender for the application, null if app-specific grammatical gender is not set. + */ + @Nullable + public final @Configuration.GrammaticalGender + Integer mGrammaticalGender; + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) - public PackageConfig(Integer nightMode, LocaleList locales) { + public PackageConfig(Integer nightMode, LocaleList locales, + @Configuration.GrammaticalGender Integer grammaticalGender) { mNightMode = nightMode; mLocales = locales; + mGrammaticalGender = grammaticalGender; } /** @@ -660,6 +670,13 @@ public abstract class ActivityTaskManagerInternal { PackageConfigurationUpdater setLocales(LocaleList locales); /** + * Sets the gender for the current application. This setting is persisted and will + * override the system configuration for this application. + */ + PackageConfigurationUpdater setGrammaticalGender( + @Configuration.GrammaticalGender int gender); + + /** * Commit changes. * @return true if the configuration changes were persisted, * false if there were no changes, or if erroneous inputs were provided, such as: diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 1180df779c11..f4d76c2603a2 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -65,6 +65,7 @@ import static android.provider.Settings.Global.HIDE_ERROR_DIALOGS; import static android.provider.Settings.System.FONT_SCALE; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; @@ -2843,7 +2844,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public boolean resizeTask(int taskId, Rect bounds, int resizeMode) { + public void resizeTask(int taskId, Rect bounds, int resizeMode) { enforceTaskPermission("resizeTask()"); final long ident = Binder.clearCallingIdentity(); try { @@ -2852,19 +2853,48 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { MATCH_ATTACHED_TASK_ONLY); if (task == null) { Slog.w(TAG, "resizeTask: taskId=" + taskId + " not found"); - return false; + return; } if (!task.getWindowConfiguration().canResizeTask()) { Slog.w(TAG, "resizeTask not allowed on task=" + task); - return false; + return; } // Reparent the task to the right root task if necessary boolean preserveWindow = (resizeMode & RESIZE_MODE_PRESERVE_WINDOW) != 0; - // After reparenting (which only resizes the task to the root task bounds), - // resize the task to the actual bounds provided - return task.resize(bounds, resizeMode, preserveWindow); + if (!getTransitionController().isShellTransitionsEnabled()) { + // After reparenting (which only resizes the task to the root task bounds), + // resize the task to the actual bounds provided + task.resize(bounds, resizeMode, preserveWindow); + return; + } + + final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */, + getTransitionController(), mWindowManager.mSyncEngine); + if (mWindowManager.mSyncEngine.hasActiveSync()) { + mWindowManager.mSyncEngine.queueSyncSet( + () -> getTransitionController().moveToCollecting(transition), + () -> { + if (!task.getWindowConfiguration().canResizeTask()) { + Slog.w(TAG, "resizeTask not allowed on task=" + task); + transition.abort(); + return; + } + getTransitionController().requestStartTransition(transition, task, + null /* remoteTransition */, null /* displayChange */); + getTransitionController().collect(task); + task.resize(bounds, resizeMode, preserveWindow); + transition.setReady(task, true); + }); + } else { + getTransitionController().moveToCollecting(transition); + getTransitionController().requestStartTransition(transition, task, + null /* remoteTransition */, null /* displayChange */); + getTransitionController().collect(task); + task.resize(bounds, resizeMode, preserveWindow); + transition.setReady(task, true); + } } } finally { Binder.restoreCallingIdentity(ident); diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 42da2a5e17a8..473a6e5ac0c5 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -46,6 +46,7 @@ import static android.os.Process.INVALID_UID; import static android.os.Process.SYSTEM_UID; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; @@ -98,6 +99,7 @@ import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.LaunchActivityItem; import android.app.servertransaction.PauseActivityItem; import android.app.servertransaction.ResumeActivityItem; +import android.companion.virtual.VirtualDeviceManager; import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -140,6 +142,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.ReferrerIntent; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.ArrayUtils; +import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.LocalServices; import com.android.server.am.ActivityManagerService; @@ -252,6 +255,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { private WindowManagerService mWindowManager; private AppOpsManager mAppOpsManager; + private VirtualDeviceManager mVirtualDeviceManager; /** Common synchronization logic used to save things to disks. */ PersisterQueue mPersisterQueue; @@ -895,12 +899,14 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { final boolean isTransitionForward = r.isTransitionForward(); final IBinder fragmentToken = r.getTaskFragment().getFragmentToken(); + + final int deviceId = getDeviceIdForDisplayId(r.getDisplayId()); clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent), System.identityHashCode(r), r.info, // TODO: Have this take the merged configuration instead of separate global // and override configs. mergedConfiguration.getGlobalConfiguration(), - mergedConfiguration.getOverrideConfiguration(), + mergedConfiguration.getOverrideConfiguration(), deviceId, r.getFilteredReferrer(r.launchedFromPackage), task.voiceInteractor, proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(), results, newIntents, r.takeOptions(), isTransitionForward, @@ -910,7 +916,8 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // Set desired final state. final ActivityLifecycleItem lifecycleItem; if (andResume) { - lifecycleItem = ResumeActivityItem.obtain(isTransitionForward); + lifecycleItem = ResumeActivityItem.obtain(isTransitionForward, + r.shouldSendCompatFakeFocus()); } else { lifecycleItem = PauseActivityItem.obtain(); } @@ -1216,6 +1223,17 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } } + int getDeviceIdForDisplayId(int displayId) { + if (displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) { + return VirtualDeviceManager.DEVICE_ID_DEFAULT; + } + if (mVirtualDeviceManager == null) { + mVirtualDeviceManager = + mService.mContext.getSystemService(VirtualDeviceManager.class); + } + return mVirtualDeviceManager.getDeviceIdForDisplayId(displayId); + } + private AppOpsManager getAppOpsManager() { if (mAppOpsManager == null) { mAppOpsManager = mService.mContext.getSystemService(AppOpsManager.class); @@ -1590,11 +1608,11 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { * @return Returns true if the given task was found and removed. */ boolean removeTaskById(int taskId, boolean killProcess, boolean removeFromRecents, - String reason) { + String reason, int callingUid) { final Task task = mRootWindowContainer.anyTaskForId(taskId, MATCH_ATTACHED_TASK_OR_RECENT_TASKS); if (task != null) { - removeTask(task, killProcess, removeFromRecents, reason); + removeTask(task, killProcess, removeFromRecents, reason, callingUid, null); return true; } Slog.w(TAG, "Request to remove task ignored for non-existent task " + taskId); @@ -1602,10 +1620,52 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } void removeTask(Task task, boolean killProcess, boolean removeFromRecents, String reason) { + removeTask(task, killProcess, removeFromRecents, reason, SYSTEM_UID, null); + } + + void removeTask(Task task, boolean killProcess, boolean removeFromRecents, String reason, + int callingUid, String callerActivityClassName) { if (task.mInRemoveTask) { // Prevent recursion. return; } + // We may have already checked that the callingUid has additional clearTask privileges, and + // cleared the calling identify. If so, we infer we do not need further restrictions here. + // TODO(b/263368846) Move to live with the rest of the ASM logic. + if (callingUid != SYSTEM_UID) { + ActivityRecord topActivity = task.getTopNonFinishingActivity(); + boolean passesAsmChecks = topActivity != null + && topActivity.getUid() == callingUid; + if (!passesAsmChecks) { + Slog.i(TAG, "Finishing task from background. t: " + task); + FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, + /* caller_uid */ + callingUid, + /* caller_activity_class_name */ + callerActivityClassName, + /* target_task_top_activity_uid */ + topActivity == null ? -1 : topActivity.getUid(), + /* target_task_top_activity_class_name */ + topActivity == null ? null : topActivity.info.name, + /* target_task_is_different */ + false, + /* target_activity_uid */ + -1, + /* target_activity_class_name */ + null, + /* target_intent_action */ + null, + /* target_intent_flags */ + 0, + /* action */ + FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__FINISH_TASK, + /* version */ + 1, + /* multi_window */ + false + ); + } + } task.mTransitionController.requestCloseTransitionIfNeeded(task); task.mInRemoveTask = true; try { @@ -1728,7 +1788,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // Task was trimmed from the recent tasks list -- remove the active task record as well // since the user won't really be able to go back to it removeTaskById(task.mTaskId, killProcess, false /* removeFromRecents */, - "recent-task-trimmed"); + "recent-task-trimmed", SYSTEM_UID); } task.removedFromRecents(); } diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index b160af6a3e11..7bd8c538351d 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -72,15 +72,16 @@ class AppTaskImpl extends IAppTask.Stub { checkCallerOrSystemOrRoot(); synchronized (mService.mGlobalLock) { - final long origId = Binder.clearCallingIdentity(); + int origCallingUid = Binder.getCallingUid(); + final long callingIdentity = Binder.clearCallingIdentity(); try { // We remove the task from recents to preserve backwards if (!mService.mTaskSupervisor.removeTaskById(mTaskId, false, - REMOVE_FROM_RECENTS, "finish-and-remove-task")) { + REMOVE_FROM_RECENTS, "finish-and-remove-task", origCallingUid)) { throw new IllegalArgumentException("Unable to find task ID " + mTaskId); } } finally { - Binder.restoreCallingIdentity(origId); + Binder.restoreCallingIdentity(callingIdentity); } } } diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index d515a277e692..2315795a003b 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -25,6 +25,9 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -39,6 +42,8 @@ import android.util.Slog; import com.android.server.am.PendingIntentRecord; +import java.lang.annotation.Retention; + /** * Helper class to check permissions for starting Activities. * @@ -52,6 +57,56 @@ public class BackgroundActivityStartController { private final ActivityTaskManagerService mService; private final ActivityTaskSupervisor mSupervisor; + // TODO(b/263368846) Rename when ASM logic is moved in + @Retention(SOURCE) + @IntDef({BAL_BLOCK, + BAL_ALLOW_DEFAULT, + BAL_ALLOW_ALLOWLISTED_UID, + BAL_ALLOW_ALLOWLISTED_COMPONENT, + BAL_ALLOW_VISIBLE_WINDOW, + BAL_ALLOW_PENDING_INTENT, + BAL_ALLOW_BAL_PERMISSION, + BAL_ALLOW_SAW_PERMISSION, + BAL_ALLOW_GRACE_PERIOD, + BAL_ALLOW_FOREGROUND, + BAL_ALLOW_SDK_SANDBOX + }) + public @interface BalCode {} + + static final int BAL_BLOCK = 0; + + static final int BAL_ALLOW_DEFAULT = 1; + + // Following codes are in order of precedence + + /** Important UIDs which should be always allowed to launch activities */ + static final int BAL_ALLOW_ALLOWLISTED_UID = 2; + + /** Apps that fulfill a certain role that can can always launch new tasks */ + static final int BAL_ALLOW_ALLOWLISTED_COMPONENT = 3; + + /** Apps which currently have a visible window */ + static final int BAL_ALLOW_VISIBLE_WINDOW = 4; + + /** Allowed due to the PendingIntent sender */ + static final int BAL_ALLOW_PENDING_INTENT = 5; + + /** App has START_ACTIVITIES_FROM_BACKGROUND permission or BAL instrumentation privileges + * granted to it */ + static final int BAL_ALLOW_BAL_PERMISSION = 6; + + /** Process has SYSTEM_ALERT_WINDOW permission granted to it */ + static final int BAL_ALLOW_SAW_PERMISSION = 7; + + /** App is in grace period after an activity was started or finished */ + static final int BAL_ALLOW_GRACE_PERIOD = 8; + + /** App is in a foreground task or bound to a foreground service (but not itself visible) */ + static final int BAL_ALLOW_FOREGROUND = 9; + + /** Process belongs to a SDK sandbox */ + static final int BAL_ALLOW_SDK_SANDBOX = 10; + BackgroundActivityStartController( final ActivityTaskManagerService service, final ActivityTaskSupervisor supervisor) { mService = service; @@ -83,6 +138,27 @@ public class BackgroundActivityStartController { boolean allowBackgroundActivityStart, Intent intent, ActivityOptions checkedOptions) { + return checkBackgroundActivityStart(callingUid, callingPid, callingPackage, + realCallingUid, realCallingPid, callerApp, originatingPendingIntent, + allowBackgroundActivityStart, intent, checkedOptions) == BAL_BLOCK; + } + + /** + * @return A code denoting which BAL rule allows an activity to be started, + * or {@link BAL_BLOCK} if the launch should be blocked + */ + @BalCode + int checkBackgroundActivityStart( + int callingUid, + int callingPid, + final String callingPackage, + int realCallingUid, + int realCallingPid, + WindowProcessController callerApp, + PendingIntentRecord originatingPendingIntent, + boolean allowBackgroundActivityStart, + Intent intent, + ActivityOptions checkedOptions) { // don't abort for the most important UIDs final int callingAppId = UserHandle.getAppId(callingUid); final boolean useCallingUidState = @@ -93,32 +169,22 @@ public class BackgroundActivityStartController { if (callingUid == Process.ROOT_UID || callingAppId == Process.SYSTEM_UID || callingAppId == Process.NFC_UID) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed for important callingUid (" + callingUid + ")"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, callingUid, + BAL_ALLOW_ALLOWLISTED_UID, "Important callingUid"); } // Always allow home application to start activities. if (isHomeApp(callingUid, callingPackage)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed for home app callingUid (" + callingUid + ")"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, callingUid, + BAL_ALLOW_ALLOWLISTED_COMPONENT, "Home app"); } // IME should always be allowed to start activity, like IME settings. final WindowState imeWindow = mService.mRootWindowContainer.getCurrentInputMethodWindow(); if (imeWindow != null && callingAppId == imeWindow.mOwnerUid) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d(TAG, "Activity start allowed for active ime (" + callingUid + ")"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, callingUid, + BAL_ALLOW_ALLOWLISTED_COMPONENT, "Active ime"); } } @@ -145,15 +211,12 @@ public class BackgroundActivityStartController { && callingUidHasAnyVisibleWindow) || isCallingUidPersistentSystemProcess; if (useCallingUidState && allowCallingUidStartActivity) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed: callingUidHasAnyVisibleWindow = " - + callingUid - + ", isCallingUidPersistentSystemProcess = " - + isCallingUidPersistentSystemProcess); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, + BAL_ALLOW_VISIBLE_WINDOW, + "callingUidHasAnyVisibleWindow = " + + callingUid + + ", isCallingUidPersistentSystemProcess = " + + isCallingUidPersistentSystemProcess); } // take realCallingUid into consideration final int realCallingUidProcState = @@ -184,14 +247,9 @@ public class BackgroundActivityStartController { Process.getAppUidForSdkSandboxUid(UserHandle.getAppId(realCallingUid)); if (mService.hasActiveVisibleWindow(realCallingSdkSandboxUidToAppUid)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed: uid in SDK sandbox (" - + realCallingUid - + ") has visible (non-toast) window."); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, realCallingUid, + BAL_ALLOW_SDK_SANDBOX, + "uid in SDK sandbox has visible (non-toast) window"); } } @@ -209,100 +267,60 @@ public class BackgroundActivityStartController { -1, true) == PackageManager.PERMISSION_GRANTED) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed: realCallingUid (" - + realCallingUid - + ") has BAL permission."); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, callingUid, + BAL_ALLOW_PENDING_INTENT, + "realCallingUid has BAL permission. realCallingUid: " + realCallingUid); } // don't abort if the realCallingUid has a visible window // TODO(b/171459802): We should check appSwitchAllowed also if (realCallingUidHasAnyVisibleWindow) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed: realCallingUid (" - + realCallingUid - + ") has visible (non-toast) window"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, + callingUid, BAL_ALLOW_PENDING_INTENT, + "realCallingUid has visible (non-toast) window. realCallingUid: " + + realCallingUid); } // if the realCallingUid is a persistent system process, abort if the IntentSender // wasn't allowed to start an activity if (isRealCallingUidPersistentSystemProcess && allowBackgroundActivityStart) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed: realCallingUid (" - + realCallingUid - + ") is persistent system process AND intent sender allowed " - + "(allowBackgroundActivityStart = true)"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, + callingUid, + BAL_ALLOW_PENDING_INTENT, + "realCallingUid is persistent system process AND intent " + + "sender allowed (allowBackgroundActivityStart = true). " + + "realCallingUid: " + realCallingUid); } // don't abort if the realCallingUid is an associated companion app if (mService.isAssociatedCompanionApp( UserHandle.getUserId(realCallingUid), realCallingUid)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Activity start allowed: realCallingUid (" - + realCallingUid - + ") is companion app"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ false, callingUid, + BAL_ALLOW_PENDING_INTENT, "realCallingUid is a companion app. " + + "realCallingUid: " + realCallingUid); } } if (useCallingUidState) { // don't abort if the callingUid has START_ACTIVITIES_FROM_BACKGROUND permission - if (mService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, callingPid, callingUid) - == PERMISSION_GRANTED) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Background activity start allowed: START_ACTIVITIES_FROM_BACKGROUND " - + "permission granted for uid " - + callingUid); - } - return false; + if (ActivityTaskManagerService.checkPermission(START_ACTIVITIES_FROM_BACKGROUND, + callingPid, callingUid) == PERMISSION_GRANTED) { + return logStartAllowedAndReturnCode(/*background*/ true, callingUid, + BAL_ALLOW_BAL_PERMISSION, + "START_ACTIVITIES_FROM_BACKGROUND permission granted"); } // don't abort if the caller has the same uid as the recents component if (mSupervisor.mRecentTasks.isCallerRecents(callingUid)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Background activity start allowed: callingUid (" - + callingUid - + ") is recents"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ true, callingUid, + BAL_ALLOW_ALLOWLISTED_COMPONENT, "Recents Component"); } // don't abort if the callingUid is the device owner if (mService.isDeviceOwner(callingUid)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Background activity start allowed: callingUid (" - + callingUid - + ") is device owner"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ true, callingUid, + BAL_ALLOW_ALLOWLISTED_COMPONENT, "Device Owner"); } // don't abort if the callingUid has companion device final int callingUserId = UserHandle.getUserId(callingUid); if (mService.isAssociatedCompanionApp(callingUserId, callingUid)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Background activity start allowed: callingUid (" - + callingUid - + ") is companion app"); - } - return false; + return logStartAllowedAndReturnCode(/*background*/ true, callingUid, + BAL_ALLOW_ALLOWLISTED_COMPONENT, "Companion App"); } // don't abort if the callingUid has SYSTEM_ALERT_WINDOW permission if (mService.hasSystemAlertWindowPermission(callingUid, callingPid, callingPackage)) { @@ -311,7 +329,8 @@ public class BackgroundActivityStartController { "Background activity start for " + callingPackage + " allowed because SYSTEM_ALERT_WINDOW permission is granted."); - return false; + return logStartAllowedAndReturnCode(/*background*/ true, callingUid, + BAL_ALLOW_SAW_PERMISSION, "SYSTEM_ALERT_WINDOW permission is granted"); } } // If we don't have callerApp at this point, no caller was provided to startActivity(). @@ -326,17 +345,12 @@ public class BackgroundActivityStartController { // don't abort if the callerApp or other processes of that uid are allowed in any way if (callerApp != null && useCallingUidState) { // first check the original calling process - if (callerApp.areBackgroundActivityStartsAllowed(appSwitchState)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Background activity start allowed: callerApp process (pid = " - + callerApp.getPid() - + ", uid = " - + callerAppUid - + ") is allowed"); - } - return false; + @BalCode int balAllowedForCaller = callerApp + .areBackgroundActivityStartsAllowed(appSwitchState); + if (balAllowedForCaller != BAL_BLOCK) { + return logStartAllowedAndReturnCode(/*background*/ true, balAllowedForCaller, + "callerApp process (pid = " + callerApp.getPid() + + ", uid = " + callerAppUid + ") is allowed"); } // only if that one wasn't allowed, check the other ones final ArraySet<WindowProcessController> uidProcesses = @@ -344,18 +358,12 @@ public class BackgroundActivityStartController { if (uidProcesses != null) { for (int i = uidProcesses.size() - 1; i >= 0; i--) { final WindowProcessController proc = uidProcesses.valueAt(i); + int balAllowedForUid = proc.areBackgroundActivityStartsAllowed(appSwitchState); if (proc != callerApp - && proc.areBackgroundActivityStartsAllowed(appSwitchState)) { - if (DEBUG_ACTIVITY_STARTS) { - Slog.d( - TAG, - "Background activity start allowed: process " - + proc.getPid() - + " from uid " - + callerAppUid - + " is allowed"); - } - return false; + && balAllowedForUid != BAL_BLOCK) { + return logStartAllowedAndReturnCode(/*background*/ true, balAllowedForUid, + "process" + proc.getPid() + + " from uid " + callerAppUid + " is allowed"); } } } @@ -416,6 +424,34 @@ public class BackgroundActivityStartController { realCallingUidHasAnyVisibleWindow, (originatingPendingIntent != null)); } - return true; + return BAL_BLOCK; + } + + private int logStartAllowedAndReturnCode(boolean background, int callingUid, int code, + String msg) { + if (DEBUG_ACTIVITY_STARTS) { + return logStartAllowedAndReturnCode(background, code, + msg, "callingUid: " + callingUid); + } + return code; + } + + private int logStartAllowedAndReturnCode(boolean background, int code, + String... msg) { + if (DEBUG_ACTIVITY_STARTS) { + StringBuilder builder = new StringBuilder(); + if (background) { + builder.append("Background "); + } + builder.append("Activity start allowed: "); + for (int i = 0; i < msg.length; i++) { + builder.append(msg[i]); + builder.append(". "); + } + builder.append("BAL Code: "); + builder.append(code); + Slog.d(TAG, builder.toString()); + } + return code; } } diff --git a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java index 0afd87282783..020e9c582ebe 100644 --- a/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java +++ b/services/core/java/com/android/server/wm/BackgroundLaunchProcessController.java @@ -22,6 +22,10 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.ActivityTaskManagerService.ACTIVITY_BG_START_GRACE_PERIOD_MS; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_BAL_PERMISSION; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_FOREGROUND; +import static com.android.server.wm.BackgroundActivityStartController.BAL_ALLOW_GRACE_PERIOD; +import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK; import android.annotation.NonNull; import android.annotation.Nullable; @@ -71,7 +75,8 @@ class BackgroundLaunchProcessController { mBackgroundActivityStartCallback = callback; } - boolean areBackgroundActivityStartsAllowed(int pid, int uid, String packageName, + @BackgroundActivityStartController.BalCode + int areBackgroundActivityStartsAllowed(int pid, int uid, String packageName, int appSwitchState, boolean isCheckingForFgsStart, boolean hasActivityInVisibleTask, boolean hasBackgroundActivityStartPrivileges, long lastStopAppSwitchesTime, long lastActivityLaunchTime, @@ -93,7 +98,7 @@ class BackgroundLaunchProcessController { + ")] Activity start allowed: within " + ACTIVITY_BG_START_GRACE_PERIOD_MS + "ms grace period"); } - return true; + return BAL_ALLOW_GRACE_PERIOD; } if (DEBUG_ACTIVITY_STARTS) { Slog.d(TAG, "[Process(" + pid + ")] Activity start within " @@ -110,7 +115,7 @@ class BackgroundLaunchProcessController { + ")] Activity start allowed: process instrumenting with background " + "activity starts privileges"); } - return true; + return BAL_ALLOW_BAL_PERMISSION; } // Allow if the caller has an activity in any foreground task. if (hasActivityInVisibleTask @@ -119,7 +124,7 @@ class BackgroundLaunchProcessController { Slog.d(TAG, "[Process(" + pid + ")] Activity start allowed: process has activity in foreground task"); } - return true; + return BAL_ALLOW_FOREGROUND; } // Allow if the caller is bound by a UID that's currently foreground. if (isBoundByForegroundUid()) { @@ -127,7 +132,7 @@ class BackgroundLaunchProcessController { Slog.d(TAG, "[Process(" + pid + ")] Activity start allowed: process bound by foreground uid"); } - return true; + return BAL_ALLOW_FOREGROUND; } // Allow if the flag was explicitly set. if (isBackgroundStartAllowedByToken(uid, packageName, isCheckingForFgsStart)) { @@ -135,9 +140,9 @@ class BackgroundLaunchProcessController { Slog.d(TAG, "[Process(" + pid + ")] Activity start allowed: process allowed by token"); } - return true; + return BAL_ALLOW_BAL_PERMISSION; } - return false; + return BAL_BLOCK; } /** diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java index 0c6cea82e00c..58d4e82961f6 100644 --- a/services/core/java/com/android/server/wm/ConfigurationContainer.java +++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java @@ -536,16 +536,19 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { * Applies app-specific nightMode and {@link LocaleList} on requested configuration. * @return true if any of the requested configuration has been updated. */ - public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales) { + public boolean applyAppSpecificConfig(Integer nightMode, LocaleList locales, + @Configuration.GrammaticalGender Integer gender) { mRequestsTmpConfig.setTo(getRequestedOverrideConfiguration()); boolean newNightModeSet = (nightMode != null) && setOverrideNightMode(mRequestsTmpConfig, nightMode); boolean newLocalesSet = (locales != null) && setOverrideLocales(mRequestsTmpConfig, locales); - if (newNightModeSet || newLocalesSet) { + boolean newGenderSet = (gender != null) && setOverrideGender(mRequestsTmpConfig, + gender); + if (newNightModeSet || newLocalesSet || newGenderSet) { onRequestedOverrideConfigurationChanged(mRequestsTmpConfig); } - return newNightModeSet || newLocalesSet; + return newNightModeSet || newLocalesSet || newGenderSet; } /** @@ -578,6 +581,21 @@ public abstract class ConfigurationContainer<E extends ConfigurationContainer> { return true; } + /** + * Overrides the gender to this ConfigurationContainer. + * + * @return true if the grammatical gender has been changed. + */ + private boolean setOverrideGender(Configuration requestsTmpConfig, + @Configuration.GrammaticalGender int gender) { + if (mRequestedOverrideConfiguration.getGrammaticalGender() == gender) { + return false; + } else { + requestsTmpConfig.setGrammaticalGender(gender); + return true; + } + } + public boolean isActivityTypeDream() { return getActivityType() == ACTIVITY_TYPE_DREAM; } diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index 18c5c3b82b19..7266d2194779 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -214,7 +214,8 @@ final class DisplayRotationCompatPolicy { activity.app.getThread(), activity.token); transaction.addCallback( RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE)); - transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false)); + transaction.setLifecycleStateRequest(ResumeActivityItem.obtain( + /* isForward */ false, /* shouldSendCompatFakeFocus */ false)); activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction); mHandler.postDelayed( () -> onActivityRefreshed(activity), diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index a7bf595fa673..03c558924430 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -191,6 +191,11 @@ final class LetterboxConfiguration { // Allows to enable letterboxing strategy for translucent activities ignoring flags. private boolean mTranslucentLetterboxingOverrideEnabled; + // Whether sending compat fake focus is enabled for unfocused apps in splitscreen. Some game + // engines wait to get focus before drawing the content of the app so this needs to be used + // otherwise the apps get blacked out when they are resumed and do not have focus yet. + private boolean mIsCompatFakeFocusEnabled; + // Whether camera compatibility treatment is enabled. // See DisplayRotationCompatPolicy for context. private final boolean mIsCameraCompatTreatmentEnabled; @@ -259,6 +264,8 @@ final class LetterboxConfiguration { R.bool.config_isWindowManagerCameraCompatTreatmentEnabled); mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); + mIsCompatFakeFocusEnabled = mContext.getResources() + .getBoolean(R.bool.config_isCompatFakeFocusEnabled); } /** @@ -970,6 +977,13 @@ final class LetterboxConfiguration { "enable_translucent_activity_letterbox", false); } + // TODO(b/262866240): Add listener to check for device config property + /** Whether fake sending focus is enabled for unfocused apps in splitscreen */ + boolean isCompatFakeFocusEnabled() { + return mIsCompatFakeFocusEnabled && DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_WINDOW_MANAGER, "enable_compat_fake_focus", true); + } + /** Whether camera compatibility treatment is enabled. */ boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) { return mIsCameraCompatTreatmentEnabled diff --git a/services/core/java/com/android/server/wm/PackageConfigPersister.java b/services/core/java/com/android/server/wm/PackageConfigPersister.java index 18a7d2e760fc..23127ac1ba09 100644 --- a/services/core/java/com/android/server/wm/PackageConfigPersister.java +++ b/services/core/java/com/android/server/wm/PackageConfigPersister.java @@ -16,6 +16,8 @@ package com.android.server.wm; +import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; + import android.annotation.NonNull; import android.content.res.Configuration; import android.os.Environment; @@ -165,7 +167,8 @@ public class PackageConfigPersister { if (modifiedRecord != null) { container.applyAppSpecificConfig(modifiedRecord.mNightMode, LocaleOverlayHelper.combineLocalesIfOverlayExists( - modifiedRecord.mLocales, mAtm.getGlobalConfiguration().getLocales())); + modifiedRecord.mLocales, mAtm.getGlobalConfiguration().getLocales()), + modifiedRecord.mGrammaticalGender); } } } @@ -188,16 +191,19 @@ public class PackageConfigPersister { } boolean isNightModeChanged = updateNightMode(impl.getNightMode(), record); boolean isLocalesChanged = updateLocales(impl.getLocales(), record); + boolean isGenderChanged = updateGender(impl.getGrammaticalGender(), record); if ((record.mNightMode == null || record.isResetNightMode()) - && (record.mLocales == null || record.mLocales.isEmpty())) { + && (record.mLocales == null || record.mLocales.isEmpty()) + && (record.mGrammaticalGender == null + || record.mGrammaticalGender == GRAMMATICAL_GENDER_NOT_SPECIFIED)) { // if all values default to system settings, we can remove the package. removePackage(packageName, userId); // if there was a pre-existing record for the package that was deleted, // we return true (since it was successfully deleted), else false (since there was // no change to the previous state). return isRecordPresent; - } else if (!isNightModeChanged && !isLocalesChanged) { + } else if (!isNightModeChanged && !isLocalesChanged && !isGenderChanged) { return false; } else { final PackageConfigRecord pendingRecord = @@ -211,7 +217,8 @@ public class PackageConfigPersister { } if (!updateNightMode(record.mNightMode, writeRecord) - && !updateLocales(record.mLocales, writeRecord)) { + && !updateLocales(record.mLocales, writeRecord) + && !updateGender(record.mGrammaticalGender, writeRecord)) { return false; } @@ -240,6 +247,15 @@ public class PackageConfigPersister { return true; } + private boolean updateGender(@Configuration.GrammaticalGender Integer requestedGender, + PackageConfigRecord record) { + if (requestedGender == null || requestedGender.equals(record.mGrammaticalGender)) { + return false; + } + record.mGrammaticalGender = requestedGender; + return true; + } + @GuardedBy("mLock") void removeUser(int userId) { synchronized (mLock) { @@ -305,7 +321,9 @@ public class PackageConfigPersister { return null; } return new ActivityTaskManagerInternal.PackageConfig( - packageConfigRecord.mNightMode, packageConfigRecord.mLocales); + packageConfigRecord.mNightMode, + packageConfigRecord.mLocales, + packageConfigRecord.mGrammaticalGender); } } @@ -336,6 +354,8 @@ public class PackageConfigPersister { final int mUserId; Integer mNightMode; LocaleList mLocales; + @Configuration.GrammaticalGender + Integer mGrammaticalGender; PackageConfigRecord(String name, int userId) { mName = name; diff --git a/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java b/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java index f3be66c5e6da..2cf8a4a7c146 100644 --- a/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java +++ b/services/core/java/com/android/server/wm/PackageConfigurationUpdaterImpl.java @@ -17,6 +17,7 @@ package com.android.server.wm; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.os.Binder; import android.os.LocaleList; import android.util.ArraySet; @@ -33,6 +34,8 @@ final class PackageConfigurationUpdaterImpl implements private final Optional<Integer> mPid; private Integer mNightMode; private LocaleList mLocales; + private @Configuration.GrammaticalGender + int mGrammaticalGender; private String mPackageName; private int mUserId; private ActivityTaskManagerService mAtm; @@ -68,6 +71,15 @@ final class PackageConfigurationUpdaterImpl implements } @Override + public ActivityTaskManagerInternal.PackageConfigurationUpdater setGrammaticalGender( + @Configuration.GrammaticalGender int gender) { + synchronized (this) { + mGrammaticalGender = gender; + } + return this; + } + + @Override public boolean commit() { synchronized (this) { synchronized (mAtm.mGlobalLock) { @@ -112,12 +124,12 @@ final class PackageConfigurationUpdaterImpl implements for (int i = processes.size() - 1; i >= 0; i--) { final WindowProcessController wpc = processes.valueAt(i); if (wpc.mInfo.packageName.equals(packageName)) { - wpc.applyAppSpecificConfig(mNightMode, localesOverride); + wpc.applyAppSpecificConfig(mNightMode, localesOverride, mGrammaticalGender); } // Always inform individual activities about the update, since activities from other // packages may be sharing this process wpc.updateAppSpecificSettingsForAllActivitiesInPackage(packageName, mNightMode, - localesOverride); + localesOverride, mGrammaticalGender); } } @@ -128,4 +140,9 @@ final class PackageConfigurationUpdaterImpl implements LocaleList getLocales() { return mLocales; } + + @Configuration.GrammaticalGender + Integer getGrammaticalGender() { + return mGrammaticalGender; + } } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index ae3b2f2bf8e6..b8878618b21c 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1444,7 +1444,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { next.abortAndClearOptionsAnimation(); transaction.setLifecycleStateRequest( ResumeActivityItem.obtain(next.app.getReportedProcState(), - dc.isNextTransitionForward())); + dc.isNextTransitionForward(), next.shouldSendCompatFakeFocus())); mAtmService.getLifecycleManager().scheduleTransaction(transaction); ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Resumed %s", next); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 13de4df9e39c..b83f4231bd78 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -6738,12 +6738,12 @@ public class WindowManagerService extends IWindowManager.Stub pw.print(" mDisplayFrozen="); pw.print(mDisplayFrozen); pw.print(" windows="); pw.print(mWindowsFreezingScreen); pw.print(" client="); pw.print(mClientFreezingScreen); - pw.print(" apps="); pw.println(mAppsFreezingScreen); + pw.print(" apps="); pw.print(mAppsFreezingScreen); final DisplayContent defaultDisplayContent = getDefaultDisplayContentLocked(); - pw.print(" mRotation="); pw.println(defaultDisplayContent.getRotation()); + pw.print(" mRotation="); pw.print(defaultDisplayContent.getRotation()); pw.print(" mLastOrientation="); pw.println(defaultDisplayContent.getLastOrientation()); - pw.print(" mWaitingForConfig="); + pw.print(" waitingForConfig="); pw.println(defaultDisplayContent.mWaitingForConfig); pw.print(" Animation settings: disabled="); pw.print(mAnimationsDisabled); diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index dcd30bb247db..3e1dc7ee5cfd 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -42,6 +42,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.ActivityTaskManagerService.INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT_MILLIS; import static com.android.server.wm.ActivityTaskManagerService.RELAUNCH_REASON_NONE; +import static com.android.server.wm.BackgroundActivityStartController.BAL_BLOCK; import static com.android.server.wm.WindowManagerService.MY_PID; import android.Manifest; @@ -52,6 +53,7 @@ import android.app.ActivityThread; import android.app.IApplicationThread; import android.app.ProfilerInfo; import android.app.servertransaction.ConfigurationChangeItem; +import android.companion.virtual.VirtualDeviceManager; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -206,6 +208,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio /** Whether {@link #mLastReportedConfiguration} is deferred by the cached state. */ private volatile boolean mHasCachedConfiguration; + private int mTopActivityDeviceId = VirtualDeviceManager.DEVICE_ID_DEFAULT; /** * Registered {@link DisplayArea} as a listener to override config changes. {@code null} if not * registered. @@ -559,15 +562,17 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio @HotPath(caller = HotPath.START_SERVICE) public boolean areBackgroundFgsStartsAllowed() { return areBackgroundActivityStartsAllowed(mAtm.getBalAppSwitchesState(), - true /* isCheckingForFgsStart */); + true /* isCheckingForFgsStart */) != BAL_BLOCK; } - boolean areBackgroundActivityStartsAllowed(int appSwitchState) { + @BackgroundActivityStartController.BalCode + int areBackgroundActivityStartsAllowed(int appSwitchState) { return areBackgroundActivityStartsAllowed(appSwitchState, false /* isCheckingForFgsStart */); } - private boolean areBackgroundActivityStartsAllowed(int appSwitchState, + @BackgroundActivityStartController.BalCode + private int areBackgroundActivityStartsAllowed(int appSwitchState, boolean isCheckingForFgsStart) { return mBgLaunchController.areBackgroundActivityStartsAllowed(mPid, mUid, mInfo.packageName, appSwitchState, isCheckingForFgsStart, hasActivityInVisibleTask(), @@ -868,13 +873,13 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio // TODO(b/199277729): Consider whether we need to add special casing for edge cases like // activity-embeddings etc. void updateAppSpecificSettingsForAllActivitiesInPackage(String packageName, Integer nightMode, - LocaleList localesOverride) { + LocaleList localesOverride, @Configuration.GrammaticalGender int gender) { for (int i = mActivities.size() - 1; i >= 0; --i) { final ActivityRecord r = mActivities.get(i); // Activities from other packages could be sharing this process. Only propagate updates // to those activities that are part of the package whose app-specific settings changed if (packageName.equals(r.packageName) - && r.applyAppSpecificConfig(nightMode, localesOverride) + && r.applyAppSpecificConfig(nightMode, localesOverride, gender) && r.isVisibleRequested()) { r.ensureActivityConfiguration(0 /* globalChanges */, true /* preserveWindow */); } @@ -1381,8 +1386,16 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio @Override public void onConfigurationChanged(Configuration newGlobalConfig) { super.onConfigurationChanged(newGlobalConfig); + + // If deviceId for the top-activity changed, schedule passing it to the app process. + boolean topActivityDeviceChanged = false; + int deviceId = getTopActivityDeviceId(); + if (deviceId != mTopActivityDeviceId) { + topActivityDeviceChanged = true; + } + final Configuration config = getConfiguration(); - if (mLastReportedConfiguration.equals(config)) { + if (mLastReportedConfiguration.equals(config) & !topActivityDeviceChanged) { // Nothing changed. if (Build.IS_DEBUGGABLE && mHasImeService) { // TODO (b/135719017): Temporary log for debugging IME service. @@ -1396,7 +1409,34 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio mHasPendingConfigurationChange = true; return; } - dispatchConfiguration(config); + + // TODO(b/263402938): Add tests that capture the deviceId dispatch to the client. + mTopActivityDeviceId = deviceId; + dispatchConfiguration(config, topActivityDeviceChanged ? mTopActivityDeviceId + : VirtualDeviceManager.DEVICE_ID_INVALID); + } + + private int getTopActivityDeviceId() { + ActivityRecord topActivity = getTopNonFinishingActivity(); + int updatedDeviceId = mTopActivityDeviceId; + if (topActivity != null && topActivity.mDisplayContent != null) { + updatedDeviceId = mAtm.mTaskSupervisor.getDeviceIdForDisplayId( + topActivity.mDisplayContent.mDisplayId); + } + return updatedDeviceId; + } + + @Nullable + private ActivityRecord getTopNonFinishingActivity() { + if (mActivities.isEmpty()) { + return null; + } + for (int i = mActivities.size() - 1; i >= 0; i--) { + if (!mActivities.get(i).finishing) { + return mActivities.get(i); + } + } + return null; } @Override @@ -1423,6 +1463,10 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } void dispatchConfiguration(Configuration config) { + dispatchConfiguration(config, getTopActivityDeviceId()); + } + + void dispatchConfiguration(Configuration config, int deviceId) { mHasPendingConfigurationChange = false; if (mThread == null) { if (Build.IS_DEBUGGABLE && mHasImeService) { @@ -1449,10 +1493,16 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio } } - scheduleConfigurationChange(mThread, config); + scheduleConfigurationChange(mThread, config, deviceId); } private void scheduleConfigurationChange(IApplicationThread thread, Configuration config) { + // By default send invalid deviceId as no-op signal so it's not updated on the client side. + scheduleConfigurationChange(thread, config, VirtualDeviceManager.DEVICE_ID_INVALID); + } + + private void scheduleConfigurationChange(IApplicationThread thread, Configuration config, + int deviceId) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending to proc %s new config %s", mName, config); if (Build.IS_DEBUGGABLE && mHasImeService) { @@ -1462,7 +1512,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio mHasCachedConfiguration = false; try { mAtm.getLifecycleManager().scheduleTransaction(thread, - ConfigurationChangeItem.obtain(config)); + ConfigurationChangeItem.obtain(config, deviceId)); } catch (Exception e) { Slog.e(TAG_CONFIGURATION, "Failed to schedule configuration change: " + mOwner, e); } diff --git a/services/core/jni/gnss/GnssMeasurementCallback.cpp b/services/core/jni/gnss/GnssMeasurementCallback.cpp index d37f3bd16ee8..a1c570812682 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.cpp +++ b/services/core/jni/gnss/GnssMeasurementCallback.cpp @@ -58,6 +58,7 @@ jmethodID method_gnssAgcBuilderSetCarrierFrequencyHz; jmethodID method_gnssAgcBuilderBuild; jmethodID method_gnssMeasurementsEventBuilderCtor; jmethodID method_gnssMeasurementsEventBuilderSetClock; +jmethodID method_gnssMeasurementsEventBuilderSetFullTracking; jmethodID method_gnssMeasurementsEventBuilderSetMeasurements; jmethodID method_gnssMeasurementsEventBuilderSetGnssAutomaticGainControls; jmethodID method_gnssMeasurementsEventBuilderBuild; @@ -109,6 +110,10 @@ void GnssMeasurement_class_init_once(JNIEnv* env, jclass& clazz) { env->GetMethodID(class_gnssMeasurementsEventBuilder, "setGnssAutomaticGainControls", "([Landroid/location/GnssAutomaticGainControl;)" "Landroid/location/GnssMeasurementsEvent$Builder;"); + method_gnssMeasurementsEventBuilderSetFullTracking = + env->GetMethodID(class_gnssMeasurementsEventBuilder, "setFullTracking", + "(Z)" + "Landroid/location/GnssMeasurementsEvent$Builder;"); method_gnssMeasurementsEventBuilderBuild = env->GetMethodID(class_gnssMeasurementsEventBuilder, "build", "()Landroid/location/GnssMeasurementsEvent;"); @@ -228,7 +233,8 @@ void GnssMeasurement_class_init_once(JNIEnv* env, jclass& clazz) { } void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock, - jobjectArray measurementArray, jobjectArray gnssAgcArray) { + jobjectArray measurementArray, jobjectArray gnssAgcArray, + bool hasFullTracking, jboolean isFullTracking) { jobject gnssMeasurementsEventBuilderObject = env->NewObject(class_gnssMeasurementsEventBuilder, method_gnssMeasurementsEventBuilderCtor); @@ -240,6 +246,11 @@ void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock, callObjectMethodIgnoringResult(env, gnssMeasurementsEventBuilderObject, method_gnssMeasurementsEventBuilderSetGnssAutomaticGainControls, gnssAgcArray); + if (hasFullTracking) { + callObjectMethodIgnoringResult(env, gnssMeasurementsEventBuilderObject, + method_gnssMeasurementsEventBuilderSetFullTracking, + isFullTracking); + } jobject gnssMeasurementsEventObject = env->CallObjectMethod(gnssMeasurementsEventBuilderObject, method_gnssMeasurementsEventBuilderBuild); @@ -381,7 +392,14 @@ void GnssMeasurementCallbackAidl::translateAndSetGnssData(const GnssData& data) jobjectArray gnssAgcArray = nullptr; gnssAgcArray = translateAllGnssAgcs(env, data.gnssAgcs); - setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray); + if (this->getInterfaceVersion() >= 3) { + setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray, + /*hasFullTracking=*/true, data.isFullTracking); + } else { + setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray, + /*hasFullTracking=*/false, + /*isFullTracking=*/JNI_FALSE); + } env->DeleteLocalRef(clock); env->DeleteLocalRef(measurementArray); diff --git a/services/core/jni/gnss/GnssMeasurementCallback.h b/services/core/jni/gnss/GnssMeasurementCallback.h index c8f1803800b6..fde56881d854 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.h +++ b/services/core/jni/gnss/GnssMeasurementCallback.h @@ -48,7 +48,8 @@ extern jmethodID method_reportMeasurementData; void GnssMeasurement_class_init_once(JNIEnv* env, jclass& clazz); void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock, - jobjectArray measurementArray, jobjectArray gnssAgcArray); + jobjectArray measurementArray, jobjectArray gnssAgcArray, + bool hasFullTracking, jboolean isFullTracking); class GnssMeasurementCallbackAidl : public hardware::gnss::BnGnssMeasurementCallback { public: @@ -140,7 +141,9 @@ void GnssMeasurementCallbackHidl::translateAndSetGnssData(const T& data) { size_t count = getMeasurementCount(data); jobjectArray measurementArray = translateAllGnssMeasurements(env, data.measurements.data(), count); - setMeasurementData(env, mCallbacksObj, clock, measurementArray, nullptr); + setMeasurementData(env, mCallbacksObj, clock, measurementArray, /*gnssAgcArray=*/nullptr, + /*hasFullTracking=*/false, + /*isFullTracking=*/JNI_FALSE); env->DeleteLocalRef(clock); env->DeleteLocalRef(measurementArray); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index e0880c030bfe..5b9460a226bb 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -132,6 +132,7 @@ import com.android.server.display.DisplayManagerService; import com.android.server.display.color.ColorDisplayService; import com.android.server.dreams.DreamManagerService; import com.android.server.emergency.EmergencyAffordanceService; +import com.android.server.grammaticalinflection.GrammaticalInflectionService; import com.android.server.gpu.GpuService; import com.android.server.graphics.fonts.FontManagerService; import com.android.server.hdmi.HdmiControlService; @@ -1051,6 +1052,17 @@ public final class SystemServer implements Dumpable { private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) { t.traceBegin("startBootstrapServices"); + t.traceBegin("ArtModuleServiceInitializer"); + // This needs to happen before DexUseManagerLocal init. We do it here to avoid colliding + // with a GC. ArtModuleServiceInitializer is a class from a separate dex file + // "service-art.jar", so referencing it involves the class linker. The class linker and the + // GC are mutually exclusive (b/263486535). Therefore, we do this here to force trigger the + // class linker earlier. If we did this later, especially after PackageManagerService init, + // the class linker would be consistently blocked by a GC because PackageManagerService + // allocates a lot of memory and almost certainly triggers a GC. + ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager()); + t.traceEnd(); + // Start the watchdog as early as possible so we can crash the system server // if we deadlock during early boot t.traceBegin("StartWatchdog"); @@ -1237,8 +1249,6 @@ public final class SystemServer implements Dumpable { t.traceBegin("DexUseManagerLocal"); // DexUseManagerLocal needs to be loaded after PackageManagerLocal has been registered, but // before PackageManagerService starts processing binder calls to notifyDexLoad. - // DexUseManagerLocal may also call artd, so ensure ArtModuleServiceManager is instantiated. - ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager()); LocalManagerRegistry.addManager( DexUseManagerLocal.class, DexUseManagerLocal.createInstance()); t.traceEnd(); @@ -1769,6 +1779,14 @@ public final class SystemServer implements Dumpable { } t.traceEnd(); + t.traceBegin("StartGrammarInflectionService"); + try { + mSystemServiceManager.startService(GrammaticalInflectionService.class); + } catch (Throwable e) { + reportWtf("starting GrammarInflectionService service", e); + } + t.traceEnd(); + t.traceBegin("UpdatePackagesIfNeeded"); try { Watchdog.getInstance().pauseWatchingCurrentThread("dexopt"); diff --git a/services/people/java/com/android/server/people/data/ConversationInfo.java b/services/people/java/com/android/server/people/data/ConversationInfo.java index 6ead44a8cd1b..a539fddc9381 100644 --- a/services/people/java/com/android/server/people/data/ConversationInfo.java +++ b/services/people/java/com/android/server/people/data/ConversationInfo.java @@ -424,6 +424,7 @@ public class ConversationInfo { case (int) ConversationInfoProto.CREATION_TIMESTAMP: builder.setCreationTimestamp(protoInputStream.readLong( ConversationInfoProto.CREATION_TIMESTAMP)); + break; case (int) ConversationInfoProto.SHORTCUT_FLAGS: builder.setShortcutFlags(protoInputStream.readInt( ConversationInfoProto.SHORTCUT_FLAGS)); diff --git a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt index 3b277f8fbb8a..f549797d42f8 100644 --- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt +++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt @@ -88,6 +88,7 @@ class AccessCheckingService(context: Context) : SystemService(context) { configPermissions, privilegedPermissionAllowlistPackages, permissionAllowlist, implicitToSourcePermissions ) + persistence.initialize() persistence.read(state) this.state = state @@ -99,43 +100,6 @@ class AccessCheckingService(context: Context) : SystemService(context) { permissionService.initialize() } - private val PackageManagerInternal.knownPackages: IntMap<Array<String>> - get() = IntMap<Array<String>>().apply { - this[KnownPackages.PACKAGE_INSTALLER] = getKnownPackageNames( - KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_PERMISSION_CONTROLLER] = getKnownPackageNames( - KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_VERIFIER] = getKnownPackageNames( - KnownPackages.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_SETUP_WIZARD] = getKnownPackageNames( - KnownPackages.PACKAGE_SETUP_WIZARD, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER] = getKnownPackageNames( - KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_CONFIGURATOR] = getKnownPackageNames( - KnownPackages.PACKAGE_CONFIGURATOR, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER] = getKnownPackageNames( - KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_APP_PREDICTOR] = getKnownPackageNames( - KnownPackages.PACKAGE_APP_PREDICTOR, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_COMPANION] = getKnownPackageNames( - KnownPackages.PACKAGE_COMPANION, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_RETAIL_DEMO] = getKnownPackageNames( - KnownPackages.PACKAGE_RETAIL_DEMO, UserHandle.USER_SYSTEM - ) - this[KnownPackages.PACKAGE_RECENTS] = getKnownPackageNames( - KnownPackages.PACKAGE_RECENTS, UserHandle.USER_SYSTEM - ) - } - private val SystemConfig.isLeanback: Boolean get() = PackageManager.FEATURE_LEANBACK in availableFeatures @@ -187,10 +151,12 @@ class AccessCheckingService(context: Context) : SystemService(context) { internal fun onStorageVolumeMounted(volumeUuid: String?, isSystemUpdated: Boolean) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { onStorageVolumeMounted( - packageStates, disabledSystemPackageStates, volumeUuid, isSystemUpdated + packageStates, disabledSystemPackageStates, knownPackages, volumeUuid, + isSystemUpdated ) } } @@ -198,35 +164,48 @@ class AccessCheckingService(context: Context) : SystemService(context) { internal fun onPackageAdded(packageName: String) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates + val knownPackages = packageManagerInternal.knownPackages mutateState { - with(policy) { onPackageAdded(packageStates, disabledSystemPackageStates, packageName) } + with(policy) { + onPackageAdded( + packageStates, disabledSystemPackageStates, knownPackages, packageName + ) + } } } internal fun onPackageRemoved(packageName: String, appId: Int) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { - onPackageRemoved(packageStates, disabledSystemPackageStates, packageName, appId) + onPackageRemoved( + packageStates, disabledSystemPackageStates, knownPackages, packageName, appId + ) } } } internal fun onPackageInstalled(packageName: String, userId: Int) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { - onPackageInstalled(packageStates, disabledSystemPackageStates, packageName, userId) + onPackageInstalled( + packageStates, disabledSystemPackageStates, knownPackages, packageName, userId + ) } } } internal fun onPackageUninstalled(packageName: String, appId: Int, userId: Int) { val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates + val knownPackages = packageManagerInternal.knownPackages mutateState { with(policy) { onPackageUninstalled( - packageStates, disabledSystemPackageStates, packageName, appId, userId + packageStates, disabledSystemPackageStates, knownPackages, packageName, appId, + userId ) } } @@ -236,6 +215,43 @@ class AccessCheckingService(context: Context) : SystemService(context) { Pair<Map<String, PackageState>, Map<String, PackageState>> get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates } + private val PackageManagerInternal.knownPackages: IntMap<Array<String>> + get() = IntMap<Array<String>>().apply { + this[KnownPackages.PACKAGE_INSTALLER] = getKnownPackageNames( + KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_PERMISSION_CONTROLLER] = getKnownPackageNames( + KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_VERIFIER] = getKnownPackageNames( + KnownPackages.PACKAGE_VERIFIER, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_SETUP_WIZARD] = getKnownPackageNames( + KnownPackages.PACKAGE_SETUP_WIZARD, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER] = getKnownPackageNames( + KnownPackages.PACKAGE_SYSTEM_TEXT_CLASSIFIER, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_CONFIGURATOR] = getKnownPackageNames( + KnownPackages.PACKAGE_CONFIGURATOR, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER] = getKnownPackageNames( + KnownPackages.PACKAGE_INCIDENT_REPORT_APPROVER, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_APP_PREDICTOR] = getKnownPackageNames( + KnownPackages.PACKAGE_APP_PREDICTOR, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_COMPANION] = getKnownPackageNames( + KnownPackages.PACKAGE_COMPANION, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_RETAIL_DEMO] = getKnownPackageNames( + KnownPackages.PACKAGE_RETAIL_DEMO, UserHandle.USER_SYSTEM + ) + this[KnownPackages.PACKAGE_RECENTS] = getKnownPackageNames( + KnownPackages.PACKAGE_RECENTS, UserHandle.USER_SYSTEM + ) + } + @OptIn(ExperimentalContracts::class) internal inline fun <T> getState(action: GetStateScope.() -> T): T { contract { callsInPlace(action, InvocationKind.EXACTLY_ONCE) } diff --git a/services/permission/java/com/android/server/permission/access/AccessPersistence.kt b/services/permission/java/com/android/server/permission/access/AccessPersistence.kt index 91239c692505..a25b72072442 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPersistence.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPersistence.kt @@ -16,8 +16,15 @@ package com.android.server.permission.access +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.SystemClock +import android.os.UserHandle import android.util.AtomicFile import android.util.Log +import com.android.internal.annotations.GuardedBy +import com.android.internal.os.BackgroundThread import com.android.modules.utils.BinaryXmlPullParser import com.android.modules.utils.BinaryXmlSerializer import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports @@ -32,6 +39,20 @@ import java.io.FileNotFoundException class AccessPersistence( private val policy: AccessPolicy ) { + private val scheduleLock = Any() + @GuardedBy("scheduleLock") + private val pendingMutationTimesMillis = IntLongMap() + @GuardedBy("scheduleLock") + private val pendingStates = IntMap<AccessState>() + @GuardedBy("scheduleLock") + private lateinit var writeHandler: WriteHandler + + private val writeLock = Any() + + fun initialize() { + writeHandler = WriteHandler(BackgroundThread.getHandler().looper) + } + fun read(state: AccessState) { readSystemState(state) state.systemState.userIds.forEachIndexed { _, userId -> @@ -64,21 +85,61 @@ class AccessPersistence( } fun write(state: AccessState) { - writeState(state.systemState) { writeSystemState(state) } + state.systemState.write(state, UserHandle.USER_ALL) state.userStates.forEachIndexed { _, userId, userState -> - writeState(userState) { writeUserState(state, userId) } + userState.write(state, userId) } } - private inline fun <T : WritableState> writeState(state: T, write: () -> Unit) { - when (val writeMode = state.writeMode) { + private fun WritableState.write(state: AccessState, userId: Int) { + when (val writeMode = writeMode) { WriteMode.NONE -> {} - WriteMode.SYNC -> write() - WriteMode.ASYNC -> TODO() + WriteMode.SYNC -> { + synchronized(scheduleLock) { pendingStates[userId] = state } + writePendingState(userId) + } + WriteMode.ASYNC -> { + synchronized(scheduleLock) { + writeHandler.removeMessages(userId) + pendingStates[userId] = state + // SystemClock.uptimeMillis() is used in Handler.sendMessageDelayed(). + val currentTimeMillis = SystemClock.uptimeMillis() + val pendingMutationTimeMillis = + pendingMutationTimesMillis.getOrPut(userId) { currentTimeMillis } + val currentDelayMillis = currentTimeMillis - pendingMutationTimeMillis + val message = writeHandler.obtainMessage(userId) + if (currentDelayMillis > MAX_WRITE_DELAY_MILLIS) { + message.sendToTarget() + } else { + val newDelayMillis = WRITE_DELAY_TIME_MILLIS + .coerceAtMost(MAX_WRITE_DELAY_MILLIS - currentDelayMillis) + writeHandler.sendMessageDelayed(message, newDelayMillis) + } + } + } else -> error(writeMode) } } + private fun writePendingState(userId: Int) { + synchronized(writeLock) { + val state: AccessState? + synchronized(scheduleLock) { + pendingMutationTimesMillis -= userId + state = pendingStates.removeReturnOld(userId) + writeHandler.removeMessages(userId) + } + if (state == null) { + return + } + if (userId == UserHandle.USER_ALL) { + writeSystemState(state) + } else { + writeUserState(state, userId) + } + } + } + private fun writeSystemState(state: AccessState) { systemFile.serialize { with(policy) { serializeSystemState(state) } @@ -109,5 +170,25 @@ class AccessPersistence( private val LOG_TAG = AccessPersistence::class.java.simpleName private const val FILE_NAME = "access.abx" + + private const val WRITE_DELAY_TIME_MILLIS = 1000L + private const val MAX_WRITE_DELAY_MILLIS = 2000L + } + + private inner class WriteHandler(looper: Looper) : Handler(looper) { + fun writeAtTime(userId: Int, timeMillis: Long) { + removeMessages(userId) + val message = obtainMessage(userId) + sendMessageDelayed(message, timeMillis) + } + + fun cancelWrite(userId: Int) { + removeMessages(userId) + } + + override fun handleMessage(message: Message) { + val userId = message.what + writePendingState(userId) + } } } diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt index 2d83bfd5a526..e0f94c7707a6 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt @@ -82,6 +82,11 @@ class AccessPolicy private constructor( this.permissionAllowlist = permissionAllowlist this.implicitToSourcePermissions = implicitToSourcePermissions } + state.userStates.apply { + userIds.forEachIndexed { _, userId -> + this[userId] = UserState() + } + } } fun GetStateScope.onStateMutated() { @@ -115,12 +120,29 @@ class AccessPolicy private constructor( fun MutateStateScope.onStorageVolumeMounted( packageStates: Map<String, PackageState>, disabledSystemPackageStates: Map<String, PackageState>, + knownPackages: IntMap<Array<String>>, volumeUuid: String?, isSystemUpdated: Boolean ) { + val addedAppIds = IntSet() newState.systemState.apply { this.packageStates = packageStates this.disabledSystemPackageStates = disabledSystemPackageStates + packageStates.forEach { (packageName, packageState) -> + if (packageState.volumeUuid == volumeUuid) { + val appId = packageState.appId + appIds.getOrPut(appId) { + addedAppIds += appId + IndexedListSet() + } += packageName + } + } + this.knownPackages = knownPackages + } + addedAppIds.forEachIndexed { _, appId -> + forEachSchemePolicy { + with(it) { onAppIdAdded(appId) } + } } forEachSchemePolicy { with(it) { onStorageVolumeMounted(volumeUuid, isSystemUpdated) } @@ -130,6 +152,7 @@ class AccessPolicy private constructor( fun MutateStateScope.onPackageAdded( packageStates: Map<String, PackageState>, disabledSystemPackageStates: Map<String, PackageState>, + knownPackages: IntMap<Array<String>>, packageName: String ) { val packageState = packageStates[packageName] @@ -145,7 +168,8 @@ class AccessPolicy private constructor( appIds.getOrPut(appId) { isAppIdAdded = true IndexedListSet() - }.add(packageName) + } += packageName + this.knownPackages = knownPackages } if (isAppIdAdded) { forEachSchemePolicy { @@ -160,6 +184,7 @@ class AccessPolicy private constructor( fun MutateStateScope.onPackageRemoved( packageStates: Map<String, PackageState>, disabledSystemPackageStates: Map<String, PackageState>, + knownPackages: IntMap<Array<String>>, packageName: String, appId: Int ) { @@ -178,6 +203,7 @@ class AccessPolicy private constructor( isAppIdRemoved = true } } + this.knownPackages = knownPackages } forEachSchemePolicy { with(it) { onPackageRemoved(packageName, appId) } @@ -192,12 +218,14 @@ class AccessPolicy private constructor( fun MutateStateScope.onPackageInstalled( packageStates: Map<String, PackageState>, disabledSystemPackageStates: Map<String, PackageState>, + knownPackages: IntMap<Array<String>>, packageName: String, userId: Int ) { newState.systemState.apply { this.packageStates = packageStates this.disabledSystemPackageStates = disabledSystemPackageStates + this.knownPackages = knownPackages } val packageState = packageStates[packageName] // TODO(zhanghai): STOPSHIP: Remove check before feature enable. @@ -212,6 +240,7 @@ class AccessPolicy private constructor( fun MutateStateScope.onPackageUninstalled( packageStates: Map<String, PackageState>, disabledSystemPackageStates: Map<String, PackageState>, + knownPackages: IntMap<Array<String>>, packageName: String, appId: Int, userId: Int @@ -219,6 +248,7 @@ class AccessPolicy private constructor( newState.systemState.apply { this.packageStates = packageStates this.disabledSystemPackageStates = disabledSystemPackageStates + this.knownPackages = knownPackages } forEachSchemePolicy { with(it) { onPackageUninstalled(packageName, appId, userId) } diff --git a/services/permission/java/com/android/server/permission/access/collection/IntLongMap.kt b/services/permission/java/com/android/server/permission/access/collection/IntLongMap.kt new file mode 100644 index 000000000000..692bbd65d5da --- /dev/null +++ b/services/permission/java/com/android/server/permission/access/collection/IntLongMap.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.permission.access.collection + +import android.util.SparseLongArray + +typealias IntLongMap = SparseLongArray + +inline fun IntLongMap.allIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (!predicate(index, key, value)) { + return false + } + } + return true +} + +inline fun IntLongMap.anyIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return true + } + } + return false +} + +@Suppress("NOTHING_TO_INLINE") +inline fun IntLongMap.copy(): IntLongMap = clone() + +inline fun <R> IntLongMap.firstNotNullOfOrNullIndexed(transform: (Int, Int, Long) -> R): R? { + forEachIndexed { index, key, value -> + transform(index, key, value)?.let { return it } + } + return null +} + +inline fun IntLongMap.forEachIndexed(action: (Int, Int, Long) -> Unit) { + for (index in 0 until size) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun IntLongMap.forEachKeyIndexed(action: (Int, Int) -> Unit) { + for (index in 0 until size) { + action(index, keyAt(index)) + } +} + +inline fun IntLongMap.forEachReversedIndexed(action: (Int, Int, Long) -> Unit) { + for (index in lastIndex downTo 0) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun IntLongMap.forEachValueIndexed(action: (Int, Long) -> Unit) { + for (index in 0 until size) { + action(index, valueAt(index)) + } +} + +inline fun IntLongMap.getOrPut(key: Int, defaultValue: () -> Long): Long { + val index = indexOfKey(key) + return if (index >= 0) { + valueAt(index) + } else { + defaultValue().also { put(key, it) } + } +} + +@Suppress("NOTHING_TO_INLINE") +inline fun IntLongMap?.getWithDefault(key: Int, defaultValue: Long): Long { + this ?: return defaultValue + return get(key, defaultValue) +} + +inline val IntLongMap.lastIndex: Int + get() = size - 1 + +@Suppress("NOTHING_TO_INLINE") +inline operator fun IntLongMap.minusAssign(key: Int) { + delete(key) +} + +inline fun IntLongMap.noneIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return false + } + } + return true +} + +@Suppress("NOTHING_TO_INLINE") +inline fun IntLongMap.putWithDefault(key: Int, value: Long, defaultValue: Long): Long { + val index = indexOfKey(key) + if (index >= 0) { + val oldValue = valueAt(index) + if (value != oldValue) { + if (value == defaultValue) { + removeAt(index) + } else { + setValueAt(index, value) + } + } + return oldValue + } else { + if (value != defaultValue) { + put(key, value) + } + return defaultValue + } +} + +fun IntLongMap.remove(key: Int) { + delete(key) +} + +fun IntLongMap.remove(key: Int, defaultValue: Long): Long { + val index = indexOfKey(key) + return if (index >= 0) { + val oldValue = valueAt(index) + removeAt(index) + oldValue + } else { + defaultValue + } +} + +inline fun IntLongMap.removeAllIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +inline fun IntLongMap.retainAllIndexed(predicate: (Int, Int, Long) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (!predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun IntLongMap.set(key: Int, value: Long) { + put(key, value) +} + +inline val IntLongMap.size: Int + get() = size() diff --git a/services/permission/java/com/android/server/permission/access/permission/Permission.kt b/services/permission/java/com/android/server/permission/access/permission/Permission.kt index 35f00a72d3ea..7bfca1214b53 100644 --- a/services/permission/java/com/android/server/permission/access/permission/Permission.kt +++ b/services/permission/java/com/android/server/permission/access/permission/Permission.kt @@ -157,7 +157,7 @@ data class Permission( if (areGidsPerUser) { IntArray(gids.size) { i -> UserHandle.getUid(userId, gids[i]) } } else { - gids.clone() + gids.copyOf() } companion object { 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 e2c2c498192e..dd36c38c1bd4 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 @@ -137,6 +137,7 @@ class PermissionService( userManagerService = UserManagerService.getInstance() handlerThread = ServiceThread(LOG_TAG, Process.THREAD_PRIORITY_BACKGROUND, true) + .apply { start() } handler = Handler(handlerThread.looper) onPermissionsChangeListeners = OnPermissionsChangeListeners(FgThread.get().looper) onPermissionFlagsChangedListener = OnPermissionFlagsChangedListener() @@ -578,7 +579,7 @@ class PermissionService( // more consistent with the pre-S-refactor behavior. This is also because we are now // actively trimming the per-UID objects when empty. val permissionFlags = with(policy) { getUidPermissionFlags(appId, userId) } - ?: return globalGids.clone() + ?: return globalGids.copyOf() val gids = GrowingIntArray.wrap(globalGids) permissionFlags.forEachIndexed { _, permissionName, flags -> @@ -654,6 +655,11 @@ class PermissionService( ) } + if (!userManagerInternal.exists(userId)) { + Log.w(LOG_TAG, "$methodName: Unknown user $userId") + return + } + enforceCallingOrSelfCrossUserPermission( userId, enforceFullPermission = true, enforceShellRestriction = true, methodName ) @@ -664,11 +670,6 @@ class PermissionService( } context.enforceCallingOrSelfPermission(enforcedPermissionName, methodName) - if (!userManagerInternal.exists(userId)) { - Log.w(LOG_TAG, "$methodName: Unknown user $userId") - return - } - val packageState: PackageState? val permissionControllerPackageName = packageManagerInternal.getKnownPackageNames( KnownPackages.PACKAGE_PERMISSION_CONTROLLER, UserHandle.USER_SYSTEM @@ -870,6 +871,11 @@ class PermissionService( } override fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int { + if (!userManagerInternal.exists(userId)) { + Log.w(LOG_TAG, "getPermissionFlags: Unknown user $userId") + return 0 + } + enforceCallingOrSelfCrossUserPermission( userId, enforceFullPermission = true, enforceShellRestriction = false, "getPermissionFlags" @@ -880,11 +886,6 @@ class PermissionService( Manifest.permission.GET_RUNTIME_PERMISSIONS ) - if (!userManagerInternal.exists(userId)) { - Log.w(LOG_TAG, "getPermissionFlags: Unknown user $userId") - return 0 - } - val packageState = packageManagerLocal.withFilteredSnapshot() .use { it.getPackageState(packageName) } if (packageState == null) { @@ -910,16 +911,16 @@ class PermissionService( permissionName: String, userId: Int ): Boolean { - enforceCallingOrSelfCrossUserPermission( - userId, enforceFullPermission = true, enforceShellRestriction = false, - "isPermissionRevokedByPolicy" - ) - if (!userManagerInternal.exists(userId)) { Log.w(LOG_TAG, "isPermissionRevokedByPolicy: Unknown user $userId") return false } + enforceCallingOrSelfCrossUserPermission( + userId, enforceFullPermission = true, enforceShellRestriction = false, + "isPermissionRevokedByPolicy" + ) + val packageState = packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId) .use { it.getPackageState(packageName) } ?: return false @@ -954,16 +955,16 @@ class PermissionService( permissionName: String, userId: Int ): Boolean { - enforceCallingOrSelfCrossUserPermission( - userId, enforceFullPermission = true, enforceShellRestriction = false, - "shouldShowRequestPermissionRationale" - ) - if (!userManagerInternal.exists(userId)) { Log.w(LOG_TAG, "shouldShowRequestPermissionRationale: Unknown user $userId") return false } + enforceCallingOrSelfCrossUserPermission( + userId, enforceFullPermission = true, enforceShellRestriction = false, + "shouldShowRequestPermissionRationale" + ) + val callingUid = Binder.getCallingUid() val packageState = packageManagerLocal.withFilteredSnapshot(callingUid, userId) .use { it.getPackageState(packageName) } ?: return false @@ -1030,6 +1031,11 @@ class PermissionService( ) } + if (!userManagerInternal.exists(userId)) { + Log.w(LOG_TAG, "updatePermissionFlags: Unknown user $userId") + return + } + enforceCallingOrSelfCrossUserPermission( userId, enforceFullPermission = true, enforceShellRestriction = true, "updatePermissionFlags" @@ -1062,11 +1068,6 @@ class PermissionService( } } - if (!userManagerInternal.exists(userId)) { - Log.w(LOG_TAG, "updatePermissionFlags: Unknown user $userId") - return - } - // Using PackageManagerInternal instead of PackageManagerLocal for now due to need to access // shared user packages. // TODO: We probably shouldn't check the share user packages, since the package name is @@ -1124,6 +1125,11 @@ class PermissionService( ) } + if (!userManagerInternal.exists(userId)) { + Log.w(LOG_TAG, "updatePermissionFlagsForAllApps: Unknown user $userId") + return + } + enforceCallingOrSelfCrossUserPermission( userId, enforceFullPermission = true, enforceShellRestriction = true, "updatePermissionFlagsForAllApps" @@ -1133,11 +1139,6 @@ class PermissionService( Manifest.permission.REVOKE_RUNTIME_PERMISSIONS ) - if (!userManagerInternal.exists(userId)) { - Log.w(LOG_TAG, "updatePermissionFlagsForAllApps: Unknown user $userId") - return - } - val packageStates = packageManagerLocal.withUnfilteredSnapshot() .use { it.packageStates } service.mutateState { @@ -1229,16 +1230,16 @@ class PermissionService( Preconditions.checkFlagsArgument(allowlistedFlags, PERMISSION_ALLOWLIST_MASK) Preconditions.checkArgumentNonnegative(userId, "userId cannot be null") - enforceCallingOrSelfCrossUserPermission( - userId, enforceFullPermission = false, enforceShellRestriction = false, - "getAllowlistedRestrictedPermissions" - ) - if (!userManagerInternal.exists(userId)) { Log.w(LOG_TAG, "AllowlistedRestrictedPermission api: Unknown user $userId") return null } + enforceCallingOrSelfCrossUserPermission( + userId, enforceFullPermission = false, enforceShellRestriction = false, + "getAllowlistedRestrictedPermissions" + ) + val callingUid = Binder.getCallingUid() val packageState = packageManagerLocal.withFilteredSnapshot(callingUid, userId) .use { it.getPackageState(packageName) } ?: return null @@ -1517,11 +1518,11 @@ class PermissionService( } override fun resetRuntimePermissions(androidPackage: AndroidPackage, userId: Int) { - TODO("Not yet implemented") + // TODO("Not yet implemented") } override fun resetRuntimePermissionsForUser(userId: Int) { - TODO("Not yet implemented") + // TODO("Not yet implemented") } override fun addOnPermissionsChangeListener(listener: IOnPermissionsChangeListener) { @@ -1657,33 +1658,36 @@ class PermissionService( override fun getPermissionTEMP( permissionName: String ): com.android.server.pm.permission.Permission? { - TODO("Not yet implemented") + // TODO("Not yet implemented") + return null } override fun getLegacyPermissions(): List<LegacyPermission> { - TODO("Not yet implemented") + // TODO("Not yet implemented") + return emptyList() } override fun readLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) { // Package settings has been read when this method is called. service.initialize() - TODO("Not yet implemented") + // TODO("Not yet implemented") } override fun writeLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) { - TODO("Not yet implemented") + // TODO("Not yet implemented") } override fun getLegacyPermissionState(appId: Int): LegacyPermissionState { - TODO("Not yet implemented") + // TODO("Not yet implemented") + return LegacyPermissionState() } override fun readLegacyPermissionStateTEMP() { - TODO("Not yet implemented") + // TODO("Not yet implemented") } override fun writeLegacyPermissionStateTEMP() { - TODO("Not yet implemented") + // TODO("Not yet implemented") } override fun onSystemReady() { diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt index 4c3ffde6b3be..73fc0b2bd766 100644 --- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt @@ -271,7 +271,12 @@ class UidPermissionPolicy : SchemePolicy() { this.packageName = packageName protectionLevel = oldPermission.permissionInfo.protectionLevel } - val newPermission = Permission(newPermissionInfo, false, oldPermission.type, 0) + // Different from the old implementation, which removes the GIDs upon permission + // adoption, but adds them back on the next boot, we now just consistently keep the + // GIDs. + val newPermission = oldPermission.copy( + permissionInfo = newPermissionInfo, isReconciled = false, appId = 0 + ) permissions.setValueAt(permissionIndex, newPermission) systemState.requestWrite() changedPermissionNames += permissionName @@ -385,7 +390,10 @@ class UidPermissionPolicy : SchemePolicy() { } if (oldPermission.type == Permission.TYPE_CONFIG && !oldPermission.isReconciled) { // It's a config permission and has no owner, take ownership now. - Permission(newPermissionInfo, true, Permission.TYPE_CONFIG, packageState.appId) + oldPermission.copy( + permissionInfo = newPermissionInfo, isReconciled = true, + appId = packageState.appId + ) } else if (systemState.packageStates[oldPackageName]?.isSystem != true) { Log.w( LOG_TAG, "Overriding permission $permissionName with new declaration in" + @@ -398,8 +406,12 @@ class UidPermissionPolicy : SchemePolicy() { setPermissionFlags(appId, userId, permissionName, 0) } } + // Different from the old implementation, which removes the GIDs upon permission + // override, but adds them back on the next boot, we now just consistently keep + // the GIDs. Permission( - newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId + newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId, + oldPermission.gids, oldPermission.areGidsPerUser ) } else { Log.w( @@ -413,7 +425,17 @@ class UidPermissionPolicy : SchemePolicy() { // Different from the old implementation, which doesn't update the permission // definition upon app update, but does update it on the next boot, we now // consistently update the permission definition upon app update. - Permission(newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId) + @Suppress("IfThenToElvis") + if (oldPermission != null) { + oldPermission.copy( + permissionInfo = newPermissionInfo, isReconciled = true, + appId = packageState.appId + ) + } else { + Permission( + newPermissionInfo, true, Permission.TYPE_MANIFEST, packageState.appId + ) + } } if (parsedPermission.isTree) { @@ -498,7 +520,7 @@ class UidPermissionPolicy : SchemePolicy() { // TODO: STOPSHIP: Retain permissions requested by disabled system packages. } newState.userStates.forEachIndexed { _, userId, userState -> - userState.uidPermissionFlags[appId].forEachReversedIndexed { _, permissionName, _ -> + userState.uidPermissionFlags[appId]?.forEachReversedIndexed { _, permissionName, _ -> if (permissionName !in requestedPermissions) { setPermissionFlags(appId, userId, permissionName, 0) } @@ -852,10 +874,12 @@ class UidPermissionPolicy : SchemePolicy() { permissionName: String ): Boolean? { val permissionAllowlist = newState.systemState.permissionAllowlist - // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName. The below is only for - // passing compilation but won't actually work. + // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName. // val apexModuleName = androidPackage.apexModuleName - val apexModuleName = packageState.packageName + val apexModuleName = permissionAllowlist.apexPrivilegedAppAllowlists + .firstNotNullOfOrNullIndexed { _, apexModuleName, apexAllowlist -> + if (packageState.packageName in apexAllowlist) apexModuleName else null + } val packageName = packageState.packageName return when { packageState.isVendor -> permissionAllowlist.getVendorPrivilegedAppAllowlistState( @@ -901,7 +925,7 @@ class UidPermissionPolicy : SchemePolicy() { return targetSdkVersion } - private fun MutateStateScope.anyPackageInAppId( + private inline fun MutateStateScope.anyPackageInAppId( appId: Int, state: AccessState = newState, predicate: (PackageState) -> Boolean @@ -913,7 +937,7 @@ class UidPermissionPolicy : SchemePolicy() { } } - private fun MutateStateScope.forEachPackageInAppId( + private inline fun MutateStateScope.forEachPackageInAppId( appId: Int, state: AccessState = newState, action: (PackageState) -> Unit diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index 3727d660f9a7..b20e1ddec690 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -92,8 +92,11 @@ import org.mockito.MockitoAnnotations; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.PublicKey; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.PriorityQueue; @@ -141,8 +144,7 @@ public class PackageManagerSettingsTests { /** make sure our initialized KeySetManagerService metadata matches packages.xml */ @Test - public void testReadKeySetSettings() - throws ReflectiveOperationException, IllegalAccessException { + public void testReadKeySetSettings() throws Exception { /* write out files and read */ writeOldFiles(); Settings settings = makeSettings(); @@ -150,6 +152,29 @@ public class PackageManagerSettingsTests { verifyKeySetMetaData(settings); } + // Same as above but use the reserve copy. + @Test + public void testReadReserveCopyKeySetSettings() throws Exception { + /* write out files and read */ + writeReserveCopyOldFiles(); + Settings settings = makeSettings(); + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + verifyKeySetMetaData(settings); + } + + // Same as above but packages.xml is malformed. + @Test + public void testReadMalformedPackagesXmlKeySetSettings() throws Exception { + // write out files + writeReserveCopyOldFiles(); + // write corrupted packages.xml + writeCorruptedPackagesXml(); + + Settings settings = makeSettings(); + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + verifyKeySetMetaData(settings); + } + /** read in data, write it out, and read it back in. Verify same. */ @Test public void testWriteKeySetSettings() @@ -165,6 +190,39 @@ public class PackageManagerSettingsTests { verifyKeySetMetaData(settings); } + // Same as above, but corrupt the primary.xml in process. + @Test + public void testWriteCorruptReadKeySetSettings() throws Exception { + // write out files and read + writeOldFiles(); + Settings settings = makeSettings(); + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + + // write out + settings.writeLPr(computer, /*sync=*/true); + + File filesDir = InstrumentationRegistry.getContext().getFilesDir(); + File packageXml = new File(filesDir, "system/packages.xml"); + File packagesReserveCopyXml = new File(filesDir, "system/packages.xml.reservecopy"); + // Primary. + assertTrue(packageXml.exists()); + // Reserve copy. + assertTrue(packagesReserveCopyXml.exists()); + // Temporary backup. + assertFalse(new File(filesDir, "packages-backup.xml").exists()); + + // compare two copies, make sure they are the same + assertTrue(Arrays.equals(Files.readAllBytes(Path.of(packageXml.getAbsolutePath())), + Files.readAllBytes(Path.of(packagesReserveCopyXml.getAbsolutePath())))); + + // write corrupted packages.xml + writeCorruptedPackagesXml(); + + // read back in and verify the same + assertThat(settings.readLPw(computer, createFakeUsers()), is(true)); + verifyKeySetMetaData(settings); + } + @Test public void testSettingsReadOld() { // Write delegateshellthe package files and make sure they're parsed properly the first time @@ -1572,9 +1630,19 @@ public class PackageManagerSettingsTests { } } - private void writePackagesXml() { + private void writeCorruptedPackagesXml() { writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), "system/packages.xml"), ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>" + + "<packages>" + + "<last-platform-version internal=\"15\" external=\"0\" />" + + "<permission-trees>" + + "<item name=\"com.google.android.permtree\"" + ).getBytes()); + } + + private void writePackagesXml(String fileName) { + writeFile(new File(InstrumentationRegistry.getContext().getFilesDir(), fileName), + ("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>" + "<packages>" + "<last-platform-version internal=\"15\" external=\"0\" fingerprint=\"foo\" />" + "<permission-trees>" @@ -1715,7 +1783,14 @@ public class PackageManagerSettingsTests { private void writeOldFiles() { deleteSystemFolder(); - writePackagesXml(); + writePackagesXml("system/packages.xml"); + writeStoppedPackagesXml(); + writePackagesList(); + } + + private void writeReserveCopyOldFiles() { + deleteSystemFolder(); + writePackagesXml("system/packages.xml.reservecopy"); writeStoppedPackagesXml(); writePackagesList(); } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java index c8e26766646f..c6b7736eafb5 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java @@ -753,7 +753,7 @@ public class PackageParserTest { .setPVersionCode(pkg.getLongVersionCode()) .setPkgFlags(PackageInfoUtils.appInfoFlags(pkg, null)) .setPrivateFlags(PackageInfoUtils.appInfoPrivateFlags(pkg, null)) - .setSharedUserId(pkg.getSharedUserLabel()) + .setSharedUserId(pkg.getSharedUserLabelRes()) .build(); } @@ -761,9 +761,9 @@ public class PackageParserTest { public static void assertPackagesEqual(AndroidPackage a, AndroidPackage b) { assertEquals(a.getBaseRevisionCode(), b.getBaseRevisionCode()); - assertEquals(a.isBaseHardwareAccelerated(), b.isBaseHardwareAccelerated()); + assertEquals(a.isHardwareAccelerated(), b.isHardwareAccelerated()); assertEquals(a.getLongVersionCode(), b.getLongVersionCode()); - assertEquals(a.getSharedUserLabel(), b.getSharedUserLabel()); + assertEquals(a.getSharedUserLabelRes(), b.getSharedUserLabelRes()); assertEquals(a.getInstallLocation(), b.getInstallLocation()); assertEquals(a.isCoreApp(), b.isCoreApp()); assertEquals(a.isRequiredForAllUsers(), b.isRequiredForAllUsers()); @@ -1036,8 +1036,8 @@ public class PackageParserTest { permission.setParsedPermissionGroup(new ParsedPermissionGroupImpl()); ((ParsedPackage) pkg.setBaseRevisionCode(100) - .setBaseHardwareAccelerated(true) - .setSharedUserLabel(100) + .setHardwareAccelerated(true) + .setSharedUserLabelRes(100) .setInstallLocation(100) .setRequiredForAllUsers(true) .asSplit( @@ -1062,7 +1062,7 @@ public class PackageParserTest { .setSdkLibVersionMajor(42) .addUsesSdkLibrary("sdk23", 200, new String[]{"digest2"}) .setStaticSharedLibraryName("foo23") - .setStaticSharedLibVersion(100) + .setStaticSharedLibraryVersion(100) .addUsesStaticLibrary("foo23", 100, new String[]{"digest"}) .addLibraryName("foo10") .addUsesLibrary("foo11") diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java index b6f1b872d017..b5bd86978455 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java @@ -273,7 +273,7 @@ public class ScanTests { public void installStaticSharedLibrary() throws Exception { final ParsedPackage pkg = ((ParsedPackage) createBasicPackage("static.lib.pkg") .setStaticSharedLibraryName("static.lib") - .setStaticSharedLibVersion(123L) + .setStaticSharedLibraryVersion(123L) .hideAsParsed()) .setPackageName("static.lib.pkg.123") .setVersionCodeMajor(1) diff --git a/services/tests/PackageManagerServiceTests/unit/Android.bp b/services/tests/PackageManagerServiceTests/unit/Android.bp index 1c6ba33bd279..9b3b8c35736c 100644 --- a/services/tests/PackageManagerServiceTests/unit/Android.bp +++ b/services/tests/PackageManagerServiceTests/unit/Android.bp @@ -33,11 +33,16 @@ android_test { "junit", "kotlin-test", "kotlin-reflect", + "mockito-target-extended-minus-junit4", "services.core", "servicestests-utils", "servicestests-core-utils", "truth-prebuilt", ], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], platform_apis: true, test_suites: ["device-tests"], } diff --git a/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml b/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml index 2ef7a1f56e76..81f6c8256024 100644 --- a/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml +++ b/services/tests/PackageManagerServiceTests/unit/AndroidManifest.xml @@ -18,6 +18,9 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.server.pm.test"> + <!--required for using Mockito--> + <application android:debuggable="true" /> + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.server.pm.test" diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index c439639128d8..1619856e005a 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -147,27 +147,27 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag ) override val baseParams = listOf( + AndroidPackage::getApplicationClassName, AndroidPackage::getAppComponentFactory, AndroidPackage::getAutoRevokePermissions, AndroidPackage::getBackupAgentName, - AndroidPackage::getBanner, + AndroidPackage::getBannerRes, AndroidPackage::getBaseApkPath, AndroidPackage::getBaseRevisionCode, AndroidPackage::getCategory, AndroidPackage::getClassLoaderName, - AndroidPackage::getClassName, AndroidPackage::getCompatibleWidthLimitDp, AndroidPackage::getCompileSdkVersion, AndroidPackage::getCompileSdkVersionCodeName, - AndroidPackage::getDataExtractionRules, + AndroidPackage::getDataExtractionRulesRes, AndroidPackage::getDescriptionRes, - AndroidPackage::getFullBackupContent, + AndroidPackage::getFullBackupContentRes, AndroidPackage::getGwpAsanMode, AndroidPackage::getIconRes, AndroidPackage::getInstallLocation, AndroidPackage::getLabelRes, AndroidPackage::getLargestWidthLimitDp, - AndroidPackage::getLogo, + AndroidPackage::getLogoRes, AndroidPackage::getLocaleConfigRes, AndroidPackage::getManageSpaceActivityName, AndroidPackage::getMaxSdkVersion, @@ -195,15 +195,15 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag PackageImpl::getSecondaryCpuAbi, AndroidPackage::getSecondaryNativeLibraryDir, AndroidPackage::getSharedUserId, - AndroidPackage::getSharedUserLabel, + AndroidPackage::getSharedUserLabelRes, AndroidPackage::getSdkLibraryName, AndroidPackage::getSdkLibVersionMajor, AndroidPackage::getStaticSharedLibraryName, - AndroidPackage::getStaticSharedLibVersion, + AndroidPackage::getStaticSharedLibraryVersion, AndroidPackage::getTargetSandboxVersion, AndroidPackage::getTargetSdkVersion, AndroidPackage::getTaskAffinity, - AndroidPackage::getTheme, + AndroidPackage::getThemeRes, AndroidPackage::getUiOptions, AndroidPackage::getUid, AndroidPackage::getVersionName, @@ -215,7 +215,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isAllowNativeHeapPointerTagging, AndroidPackage::isAllowTaskReparenting, AndroidPackage::isBackupInForeground, - AndroidPackage::isBaseHardwareAccelerated, + AndroidPackage::isHardwareAccelerated, AndroidPackage::isCantSaveState, AndroidPackage::isCoreApp, AndroidPackage::isCrossProfile, @@ -260,7 +260,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isUsesNonSdkApi, AndroidPackage::isVisibleToInstantApps, AndroidPackage::isVmSafeMode, - AndroidPackage::isLeavingSharedUid, + AndroidPackage::isLeavingSharedUser, AndroidPackage::isResetEnabledSettingsOnAppDataCleared, AndroidPackage::getMaxAspectRatio, AndroidPackage::getMinAspectRatio, @@ -293,7 +293,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag adder(AndroidPackage::getUsesOptionalLibraries, "testUsesOptionalLibrary"), adder(AndroidPackage::getUsesOptionalNativeLibraries, "testUsesOptionalNativeLibrary"), getSetByValue( - AndroidPackage::areAttributionsUserVisible, + AndroidPackage::isAttributionsUserVisible, PackageImpl::setAttributionsAreUserVisible, true ), diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/uninstall/UninstallCompleteCallbackTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/uninstall/UninstallCompleteCallbackTest.kt new file mode 100644 index 000000000000..52fc91dfa9e6 --- /dev/null +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/uninstall/UninstallCompleteCallbackTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm.test.uninstall + +import android.app.PackageDeleteObserver +import android.content.Intent +import android.content.pm.IPackageDeleteObserver2 +import android.content.pm.PackageManager +import android.content.pm.PackageManager.UninstallCompleteCallback +import android.os.Parcel +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations.initMocks +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +class UninstallCompleteCallbackTest { + + val PACKAGE_NAME: String = "com.example.package" + val ERROR_MSG: String = "no error" + + @get:Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + @Mock + lateinit var mockAdapter: PackageDeleteObserver + + val mockBinder: IPackageDeleteObserver2.Stub = object : IPackageDeleteObserver2.Stub() { + override fun onUserActionRequired(intent: Intent) { + mockAdapter.onUserActionRequired(intent) + } + override fun onPackageDeleted(basePackageName: String, returnCode: Int, msg: String) { + mockAdapter.onPackageDeleted(basePackageName, returnCode, msg) + } + } + + @Before + fun setUp() { + initMocks(this) + } + + @Test + fun testCallDelegation () { + doReturn(mockBinder).`when`(mockAdapter).binder + + val callback = UninstallCompleteCallback(mockAdapter.binder.asBinder()) + callback.onUninstallComplete(PACKAGE_NAME, PackageManager.DELETE_SUCCEEDED, ERROR_MSG) + + verify(mockAdapter, times(1)).onPackageDeleted(PACKAGE_NAME, + PackageManager.DELETE_SUCCEEDED, ERROR_MSG) + } + + @Test + fun testClassIsParcelable() { + doReturn(mockBinder).`when`(mockAdapter).binder + + val callback = UninstallCompleteCallback(mockAdapter.binder.asBinder()) + + val parcel = Parcel.obtain() + callback.writeToParcel(parcel, callback.describeContents()) + parcel.setDataPosition(0) + + val callbackFromParcel = UninstallCompleteCallback.CREATOR.createFromParcel(parcel) + + callbackFromParcel.onUninstallComplete(PACKAGE_NAME, PackageManager.DELETE_SUCCEEDED, + ERROR_MSG) + + verify(mockAdapter, times(1)).onPackageDeleted(PACKAGE_NAME, + PackageManager.DELETE_SUCCEEDED, ERROR_MSG) + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java index 8d78cd6fb012..f56b0b469060 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueModernImplTest.java @@ -197,7 +197,7 @@ public class BroadcastQueueModernImplTest { private void enqueueOrReplaceBroadcast(BroadcastProcessQueue queue, BroadcastRecord record, int recordIndex, long enqueueTime) { - queue.enqueueOrReplaceBroadcast(record, recordIndex); + queue.enqueueOrReplaceBroadcast(record, recordIndex, null /* replacedBroadcastConsumer */); record.enqueueTime = enqueueTime; } @@ -327,7 +327,7 @@ public class BroadcastQueueModernImplTest { final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); queue.setProcessCached(false); final long notCachedRunnableAt = queue.getRunnableAt(); @@ -349,12 +349,12 @@ public class BroadcastQueueModernImplTest { // enqueue a bg-priority broadcast then a fg-priority one final Intent timezone = new Intent(Intent.ACTION_TIMEZONE_CHANGED); final BroadcastRecord timezoneRecord = makeBroadcastRecord(timezone); - queue.enqueueOrReplaceBroadcast(timezoneRecord, 0); + queue.enqueueOrReplaceBroadcast(timezoneRecord, 0, null /* replacedBroadcastConsumer */); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); airplane.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); // verify that: // (a) the queue is immediately runnable by existence of a fg-priority broadcast @@ -385,7 +385,7 @@ public class BroadcastQueueModernImplTest { final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, null, List.of(withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 10), withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 0)), true); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 1); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 1, null /* replacedBroadcastConsumer */); assertFalse(queue.isRunnable()); assertEquals(BroadcastProcessQueue.REASON_BLOCKED, queue.getRunnableAtReason()); @@ -408,7 +408,7 @@ public class BroadcastQueueModernImplTest { final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); final BroadcastRecord airplaneRecord = makeBroadcastRecord(airplane, List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(airplaneRecord, 0); + queue.enqueueOrReplaceBroadcast(airplaneRecord, 0, null /* replacedBroadcastConsumer */); mConstants.MAX_PENDING_BROADCASTS = 128; queue.invalidateRunnableAt(); @@ -434,11 +434,11 @@ public class BroadcastQueueModernImplTest { new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED), List.of(makeMockRegisteredReceiver())); - queue.enqueueOrReplaceBroadcast(lazyRecord, 0); + queue.enqueueOrReplaceBroadcast(lazyRecord, 0, null /* replacedBroadcastConsumer */); assertThat(queue.getRunnableAt()).isGreaterThan(lazyRecord.enqueueTime); assertThat(queue.getRunnableAtReason()).isNotEqualTo(testRunnableAtReason); - queue.enqueueOrReplaceBroadcast(testRecord, 0); + queue.enqueueOrReplaceBroadcast(testRecord, 0, null /* replacedBroadcastConsumer */); assertThat(queue.getRunnableAt()).isAtMost(testRecord.enqueueTime); assertThat(queue.getRunnableAtReason()).isEqualTo(testRunnableAtReason); } @@ -507,20 +507,26 @@ public class BroadcastQueueModernImplTest { queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED) - .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0); + .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, + null /* replacedBroadcastConsumer */); queue.enqueueOrReplaceBroadcast( - makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0); + makeBroadcastRecord(new Intent(Intent.ACTION_TIMEZONE_CHANGED)), 0, + null /* replacedBroadcastConsumer */); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0); + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, + null /* replacedBroadcastConsumer */); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_ALARM_CHANGED) - .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0); + .addFlags(Intent.FLAG_RECEIVER_OFFLOAD)), 0, + null /* replacedBroadcastConsumer */); queue.enqueueOrReplaceBroadcast( - makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0); + makeBroadcastRecord(new Intent(Intent.ACTION_TIME_TICK)), 0, + null /* replacedBroadcastConsumer */); queue.enqueueOrReplaceBroadcast( makeBroadcastRecord(new Intent(Intent.ACTION_LOCALE_CHANGED) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0); + .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)), 0, + null /* replacedBroadcastConsumer */); queue.makeActiveNextPending(); assertEquals(Intent.ACTION_LOCKED_BOOT_COMPLETED, queue.getActive().intent.getAction()); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index d79c4d8d83af..6800d5282806 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -154,6 +154,7 @@ public class BroadcastQueueTest { private ActivityManagerService mAms; private BroadcastQueue mQueue; + BroadcastConstants mConstants; /** * Desired behavior of the next @@ -277,10 +278,9 @@ public class BroadcastQueueTest { }).when(mAms).getProcessRecordLocked(any(), anyInt()); doNothing().when(mAms).appNotResponding(any(), any()); - final BroadcastConstants constants = new BroadcastConstants( - Settings.Global.BROADCAST_FG_CONSTANTS); - constants.TIMEOUT = 100; - constants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; + mConstants = new BroadcastConstants(Settings.Global.BROADCAST_FG_CONSTANTS); + mConstants.TIMEOUT = 100; + mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; final BroadcastSkipPolicy emptySkipPolicy = new BroadcastSkipPolicy(mAms) { public boolean shouldSkip(BroadcastRecord r, Object o) { // Ignored @@ -291,7 +291,7 @@ public class BroadcastQueueTest { return null; } }; - final BroadcastHistory emptyHistory = new BroadcastHistory(constants) { + final BroadcastHistory emptyHistory = new BroadcastHistory(mConstants) { public void addBroadcastToHistoryLocked(BroadcastRecord original) { // Ignored } @@ -299,13 +299,13 @@ public class BroadcastQueueTest { if (mImpl == Impl.DEFAULT) { var q = new BroadcastQueueImpl(mAms, mHandlerThread.getThreadHandler(), TAG, - constants, emptySkipPolicy, emptyHistory, false, + mConstants, emptySkipPolicy, emptyHistory, false, ProcessList.SCHED_GROUP_DEFAULT); q.mReceiverBatch.mDeepReceiverCopy = true; mQueue = q; } else if (mImpl == Impl.MODERN) { var q = new BroadcastQueueModernImpl(mAms, mHandlerThread.getThreadHandler(), - constants, constants, emptySkipPolicy, emptyHistory); + mConstants, mConstants, emptySkipPolicy, emptyHistory); q.mReceiverBatch.mDeepReceiverCopy = true; mQueue = q; } else { @@ -1703,6 +1703,28 @@ public class BroadcastQueueTest { } @Test + public void testReplacePending_withPrioritizedBroadcasts() throws Exception { + mConstants.MAX_RUNNING_ACTIVE_BROADCASTS = 1; + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_GREEN); + + final Intent userPresent = new Intent(Intent.ACTION_USER_PRESENT) + .addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + + final List receivers = List.of( + withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_GREEN), 100), + withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_RED), 50), + withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_YELLOW), 10), + withPriority(makeManifestReceiver(PACKAGE_GREEN, CLASS_BLUE), 0)); + + // Enqueue the broadcast a few times and verify that broadcast queues are not stuck + // and are emptied eventually. + for (int i = 0; i < 6; ++i) { + enqueueBroadcast(makeBroadcastRecord(userPresent, callerApp, receivers)); + } + waitForIdle(); + } + + @Test public void testIdleAndBarrier() throws Exception { final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); final ProcessRecord receiverApp = makeActiveProcessRecord(PACKAGE_GREEN); diff --git a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java index cd2f205f5552..3480af62e6c4 100644 --- a/services/tests/servicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,9 @@ package com.android.server.backup; import static com.google.common.truth.Truth.assertThat; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -25,11 +28,11 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.backup.BackupAgent; import android.app.backup.BackupAnnotations.BackupDestination; +import android.app.backup.BackupRestoreEventLogger.DataTypeResult; import android.app.backup.IBackupManagerMonitor; import android.app.backup.IBackupObserver; import android.content.Context; @@ -37,6 +40,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.platform.test.annotations.Presubmit; +import android.util.FeatureFlagUtils; import androidx.test.filters.FlakyTest; import androidx.test.runner.AndroidJUnit4; @@ -47,15 +51,21 @@ import com.android.server.backup.params.BackupParams; import com.android.server.backup.transport.BackupTransportClient; import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.utils.BackupEligibilityRules; +import com.android.server.backup.utils.BackupManagerMonitorUtils; import com.google.common.collect.ImmutableSet; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; +import java.util.Arrays; +import java.util.List; import java.util.function.IntConsumer; @Presubmit @@ -63,6 +73,7 @@ import java.util.function.IntConsumer; public class UserBackupManagerServiceTest { private static final String TEST_PACKAGE = "package1"; private static final String[] TEST_PACKAGES = new String[] { TEST_PACKAGE }; + private static final String TEST_TRANSPORT = "transport"; private static final int WORKER_THREAD_TIMEOUT_MILLISECONDS = 1; @Mock Context mContext; @@ -70,21 +81,38 @@ public class UserBackupManagerServiceTest { @Mock IBackupObserver mBackupObserver; @Mock PackageManager mPackageManager; @Mock TransportConnection mTransportConnection; + @Mock TransportManager mTransportManager; @Mock BackupTransportClient mBackupTransport; @Mock BackupEligibilityRules mBackupEligibilityRules; @Mock LifecycleOperationStorage mOperationStorage; + private MockitoSession mSession; private TestBackupService mService; @Before public void setUp() throws Exception { + mSession = mockitoSession() + .initMocks(this) + .mockStatic(BackupManagerMonitorUtils.class) + .mockStatic(FeatureFlagUtils.class) + // TODO(b/263239775): Remove unnecessary stubbing. + .strictness(Strictness.LENIENT) + .startMocking(); MockitoAnnotations.initMocks(this); - mService = new TestBackupService(mContext, mPackageManager, mOperationStorage); + mService = new TestBackupService(mContext, mPackageManager, mOperationStorage, + mTransportManager); mService.setEnabled(true); mService.setSetupComplete(true); } + @After + public void tearDown() { + if (mSession != null) { + mSession.finishMocking(); + } + } + @Test public void initializeBackupEnableState_doesntWriteStateToDisk() { mService.initializeBackupEnableState(); @@ -201,6 +229,26 @@ public class UserBackupManagerServiceTest { .cancelOperation(anyInt(), anyBoolean(), any(IntConsumer.class)); } + @Test + public void testReportDelayedRestoreResult_sendsLogsToMonitor() throws Exception { + PackageInfo packageInfo = getPackageInfo(TEST_PACKAGE); + when(mPackageManager.getPackageInfoAsUser(anyString(), + any(PackageManager.PackageInfoFlags.class), anyInt())).thenReturn(packageInfo); + when(mTransportManager.getCurrentTransportName()).thenReturn(TEST_TRANSPORT); + when(mTransportManager.getTransportClientOrThrow(eq(TEST_TRANSPORT), anyString())) + .thenReturn(mTransportConnection); + when(mTransportConnection.connectOrThrow(any())).thenReturn(mBackupTransport); + when(mBackupTransport.getBackupManagerMonitor()).thenReturn(mBackupManagerMonitor); + + + List<DataTypeResult> results = Arrays.asList(new DataTypeResult(/* dataType */ "type_1"), + new DataTypeResult(/* dataType */ "type_2")); + mService.reportDelayedRestoreResult(TEST_PACKAGE, results); + + verify(() -> BackupManagerMonitorUtils.sendAgentLoggingResults( + eq(mBackupManagerMonitor), eq(packageInfo), eq(results))); + } + private static PackageInfo getPackageInfo(String packageName) { PackageInfo packageInfo = new PackageInfo(); packageInfo.applicationInfo = new ApplicationInfo(); @@ -215,8 +263,8 @@ public class UserBackupManagerServiceTest { private volatile Thread mWorkerThread = null; TestBackupService(Context context, PackageManager packageManager, - LifecycleOperationStorage operationStorage) { - super(context, packageManager, operationStorage); + LifecycleOperationStorage operationStorage, TransportManager transportManager) { + super(context, packageManager, operationStorage, transportManager); } @Override diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java index c4ff4f0307d2..a8b8f917b0b1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobSchedulerServiceTest.java @@ -254,6 +254,12 @@ public class JobSchedulerServiceTest { ConnectivityController connectivityController = mService.getConnectivityController(); spyOn(connectivityController); + mService.mConstants.RUNTIME_MIN_GUARANTEE_MS = 10 * MINUTE_IN_MILLIS; + mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS = 15 * MINUTE_IN_MILLIS; + mService.mConstants.RUNTIME_DATA_TRANSFER_LIMIT_MS = 60 * MINUTE_IN_MILLIS; + mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_BUFFER_FACTOR = 1.5f; + mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS = HOUR_IN_MILLIS; + mService.mConstants.RUNTIME_USER_INITIATED_DATA_TRANSFER_LIMIT_MS = 6 * HOUR_IN_MILLIS; assertEquals(mService.mConstants.RUNTIME_MIN_EJ_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(ejMax)); @@ -268,7 +274,7 @@ public class JobSchedulerServiceTest { assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobDef)); grantRunLongJobsPermission(false); // Without permission - assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobDT)); grantRunLongJobsPermission(true); // With permission doReturn(ConnectivityController.UNKNOWN_TIME) @@ -288,12 +294,16 @@ public class JobSchedulerServiceTest { assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobDT)); // UserInitiated - assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, - mService.getMinJobExecutionGuaranteeMs(jobUI)); grantRunLongJobsPermission(false); - assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + // Permission isn't granted, so it should just be treated as a regular data transfer job. + assertEquals(mService.mConstants.RUNTIME_MIN_DATA_TRANSFER_GUARANTEE_MS, mService.getMinJobExecutionGuaranteeMs(jobUIDT)); + // Permission isn't granted, so it should just be treated as a regular job. + assertEquals(mService.mConstants.RUNTIME_MIN_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUI)); grantRunLongJobsPermission(true); // With permission + assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_GUARANTEE_MS, + mService.getMinJobExecutionGuaranteeMs(jobUI)); doReturn(ConnectivityController.UNKNOWN_TIME) .when(connectivityController).getEstimatedTransferTimeMs(any()); assertEquals(mService.mConstants.RUNTIME_MIN_USER_INITIATED_DATA_TRANSFER_GUARANTEE_MS, diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt index 28c78b20aae2..cffd027a58a3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/SharedLibrariesImplTest.kt @@ -391,7 +391,7 @@ class SharedLibrariesImplTest { libraries?.forEach { pkg.addLibraryName(it) } staticLibrary?.let { pkg.setStaticSharedLibraryName(it) - pkg.setStaticSharedLibVersion(staticLibraryVersion) + pkg.setStaticSharedLibraryVersion(staticLibraryVersion) pkg.setStaticSharedLibrary(true) } usesLibraries?.forEach { pkg.addUsesLibrary(it) } @@ -435,7 +435,7 @@ class SharedLibrariesImplTest { libraries?.forEach { addLibraryName(it) } staticLibrary?.let { setStaticSharedLibraryName(it) - setStaticSharedLibVersion(staticLibraryVersion) + setStaticSharedLibraryVersion(staticLibraryVersion) setStaticSharedLibrary(true) } usesLibraries?.forEach { addUsesLibrary(it) } diff --git a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java index ee9d59b11ed5..08a0878e6e7c 100644 --- a/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java +++ b/services/tests/servicestests/src/com/android/server/audio/NoOpAudioSystemAdapter.java @@ -75,7 +75,13 @@ public class NoOpAudioSystemAdapter extends AudioSystemAdapter { } @Override - public int removeDevicesRoleForStrategy(int strategy, int role) { + public int removeDevicesRoleForStrategy(int strategy, int role, + @NonNull List<AudioDeviceAttributes> devices) { + return AudioSystem.AUDIO_STATUS_OK; + } + + @Override + public int clearDevicesRoleForStrategy(int strategy, int role) { return AudioSystem.AUDIO_STATUS_OK; } diff --git a/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java b/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java index cadc890f64fd..87ade963e121 100644 --- a/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/backup/utils/BackupManagerMonitorUtilsTest.java @@ -172,11 +172,30 @@ public class BackupManagerMonitorUtilsTest { .when(agent) .getLoggerResults(any()); - IBackupManagerMonitor result = + IBackupManagerMonitor monitor = BackupManagerMonitorUtils.monitorAgentLoggingResults( mMonitorMock, packageInfo, agent); - assertThat(result).isEqualTo(mMonitorMock); + assertCorrectBundleSentToMonitor(monitor); + } + + @Test + public void sendAgentLoggingResults_fillsBundleCorrectly() throws Exception { + PackageInfo packageInfo = new PackageInfo(); + packageInfo.packageName = "test.package"; + List<BackupRestoreEventLogger.DataTypeResult> loggingResults = new ArrayList<>(); + loggingResults.add(new BackupRestoreEventLogger.DataTypeResult("testLoggingResult")); + + IBackupManagerMonitor monitor = BackupManagerMonitorUtils.sendAgentLoggingResults( + mMonitorMock, packageInfo, loggingResults); + + assertCorrectBundleSentToMonitor(monitor); + } + + private void assertCorrectBundleSentToMonitor(IBackupManagerMonitor monitor) throws Exception { + + + assertThat(monitor).isEqualTo(mMonitorMock); ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(mMonitorMock).onEvent(bundleCaptor.capture()); Bundle eventBundle = bundleCaptor.getValue(); 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 ac880ce231d5..89eaa2c3d85a 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 @@ -35,7 +35,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.argThat; -import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -97,8 +96,9 @@ import android.view.DisplayInfo; import android.view.KeyEvent; import android.view.WindowManager; -import androidx.test.InstrumentationRegistry; +import androidx.test.platform.app.InstrumentationRegistry; +import com.android.compatibility.common.util.AdoptShellPermissionsRule; import com.android.internal.app.BlockedAppStreamingActivity; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; @@ -107,6 +107,7 @@ import com.android.server.sensors.SensorManagerInternal; import com.google.android.collect.Sets; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -177,17 +178,14 @@ public class VirtualDeviceManagerServiceTest { .setAssociatedDisplayId(DISPLAY_ID_1) .build(); private static final VirtualTouchscreenConfig TOUCHSCREEN_CONFIG = - new VirtualTouchscreenConfig.Builder() + new VirtualTouchscreenConfig.Builder(WIDTH, HEIGHT) .setVendorId(VENDOR_ID) .setProductId(PRODUCT_ID) .setInputDeviceName(DEVICE_NAME) .setAssociatedDisplayId(DISPLAY_ID_1) - .setWidthInPixels(WIDTH) - .setHeightInPixels(HEIGHT) .build(); private static final VirtualNavigationTouchpadConfig NAVIGATION_TOUCHPAD_CONFIG = - new VirtualNavigationTouchpadConfig.Builder( - /* touchpadHeight= */ HEIGHT, /* touchpadWidth= */ WIDTH) + new VirtualNavigationTouchpadConfig.Builder(WIDTH, HEIGHT) .setVendorId(VENDOR_ID) .setProductId(PRODUCT_ID) .setInputDeviceName(DEVICE_NAME) @@ -195,6 +193,11 @@ public class VirtualDeviceManagerServiceTest { .build(); private static final String TEST_SITE = "http://test"; + @Rule + public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule( + InstrumentationRegistry.getInstrumentation().getUiAutomation(), + Manifest.permission.CREATE_VIRTUAL_DEVICE); + private Context mContext; private InputManagerMockHelper mInputManagerMockHelper; private VirtualDeviceImpl mDeviceImpl; @@ -304,10 +307,9 @@ public class VirtualDeviceManagerServiceTest { LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); - mContext = Mockito.spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + mContext = Mockito.spy(new ContextWrapper( + InstrumentationRegistry.getInstrumentation().getTargetContext())); doReturn(mContext).when(mContext).createContextAsUser(eq(Process.myUserHandle()), anyInt()); - doNothing().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)).thenReturn( mDevicePolicyManagerMock); @@ -726,48 +728,28 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() { - mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - final VirtualTouchscreenConfig zeroConfig = - new VirtualTouchscreenConfig.Builder() - .setVendorId(VENDOR_ID) - .setProductId(PRODUCT_ID) - .setInputDeviceName(DEVICE_NAME) - .setAssociatedDisplayId(DISPLAY_ID_1) - .setWidthInPixels(0) - .setHeightInPixels(0) - .build(); assertThrows(IllegalArgumentException.class, - () -> mDeviceImpl.createVirtualTouchscreen(zeroConfig, BINDER)); + () -> new VirtualTouchscreenConfig.Builder( + /* touchscrenWidth= */ 0, /* touchscreenHeight= */ 0)); } @Test public void createVirtualTouchscreen_negativeDisplayDimension_failsIllegalArgumentException() { - mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - final VirtualTouchscreenConfig negativeConfig = - new VirtualTouchscreenConfig.Builder() - .setVendorId(VENDOR_ID) - .setProductId(PRODUCT_ID) - .setInputDeviceName(DEVICE_NAME) - .setAssociatedDisplayId(DISPLAY_ID_1) - .setWidthInPixels(-100) - .setHeightInPixels(-100) - .build(); assertThrows(IllegalArgumentException.class, - () -> mDeviceImpl.createVirtualTouchscreen(negativeConfig, BINDER)); - + () -> new VirtualTouchscreenConfig.Builder( + /* touchscrenWidth= */ -100, /* touchscreenHeight= */ -100)); } @Test public void createVirtualTouchscreen_positiveDisplayDimension_successful() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); VirtualTouchscreenConfig positiveConfig = - new VirtualTouchscreenConfig.Builder() + new VirtualTouchscreenConfig.Builder( + /* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800) .setVendorId(VENDOR_ID) .setProductId(PRODUCT_ID) .setInputDeviceName(DEVICE_NAME) .setAssociatedDisplayId(DISPLAY_ID_1) - .setWidthInPixels(600) - .setHeightInPixels(800) .build(); mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER); assertWithMessage( @@ -784,36 +766,16 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() { - mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); assertThrows(IllegalArgumentException.class, - () -> { - final VirtualNavigationTouchpadConfig zeroConfig = - new VirtualNavigationTouchpadConfig.Builder( - /* touchpadHeight= */ 0, /* touchpadWidth= */ 0) - .setVendorId(VENDOR_ID) - .setProductId(PRODUCT_ID) - .setInputDeviceName(DEVICE_NAME) - .setAssociatedDisplayId(DISPLAY_ID_1) - .build(); - mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER); - }); + () -> new VirtualNavigationTouchpadConfig.Builder( + /* touchpadHeight= */ 0, /* touchpadWidth= */ 0)); } @Test public void createVirtualNavigationTouchpad_negativeDisplayDimension_failsWithException() { - mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); assertThrows(IllegalArgumentException.class, - () -> { - final VirtualNavigationTouchpadConfig negativeConfig = - new VirtualNavigationTouchpadConfig.Builder( - /* touchpadHeight= */ -50, /* touchpadWidth= */ 50) - .setVendorId(VENDOR_ID) - .setProductId(PRODUCT_ID) - .setInputDeviceName(DEVICE_NAME) - .setAssociatedDisplayId(DISPLAY_ID_1) - .build(); - mDeviceImpl.createVirtualNavigationTouchpad(negativeConfig, BINDER); - }); + () -> new VirtualNavigationTouchpadConfig.Builder( + /* touchpadHeight= */ -50, /* touchpadWidth= */ 50)); } @Test @@ -844,76 +806,76 @@ public class VirtualDeviceManagerServiceTest { @Test public void createVirtualDpad_noPermission_failsSecurityException() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER)); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER)); + } } @Test public void createVirtualKeyboard_noPermission_failsSecurityException() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)); + } } @Test public void createVirtualMouse_noPermission_failsSecurityException() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)); + } } @Test public void createVirtualTouchscreen_noPermission_failsSecurityException() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)); + } } @Test public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows(SecurityException.class, - () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, - BINDER)); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, + BINDER)); + } } @Test public void createVirtualSensor_noPermission_failsSecurityException() { - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows( - SecurityException.class, - () -> mDeviceImpl.createVirtualSensor( - BINDER, - new VirtualSensorConfig.Builder( - Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build())); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows( + SecurityException.class, + () -> mDeviceImpl.createVirtualSensor( + BINDER, + new VirtualSensorConfig.Builder( + Sensor.TYPE_ACCELEROMETER, DEVICE_NAME).build())); + } } @Test public void onAudioSessionStarting_noPermission_failsSecurityException() { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID_1); - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows(SecurityException.class, - () -> mDeviceImpl.onAudioSessionStarting( - DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback)); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows(SecurityException.class, + () -> mDeviceImpl.onAudioSessionStarting( + DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback)); + } } @Test public void onAudioSessionEnded_noPermission_failsSecurityException() { - doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( - eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); - assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded()); + try (DropShellPermissionsTemporarily drop = new DropShellPermissionsTemporarily()) { + assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded()); + } } @Test @@ -1593,4 +1555,18 @@ public class VirtualDeviceManagerServiceTest { mVdms.addVirtualDevice(virtualDeviceImpl); return virtualDeviceImpl; } + + /** Helper class to drop permissions temporarily and restore them at the end of a test. */ + static final class DropShellPermissionsTemporarily implements AutoCloseable { + DropShellPermissionsTemporarily() { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .dropShellPermissionIdentity(); + } + + @Override + public void close() { + InstrumentationRegistry.getInstrumentation().getUiAutomation() + .adoptShellPermissionIdentity(); + } + } } 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 d90f53ad0e55..1c44da1fdb89 100644 --- a/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java @@ -6,6 +6,8 @@ import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -15,6 +17,7 @@ import static org.mockito.Mockito.when; import android.app.job.JobInfo; import android.app.job.JobInfo.Builder; +import android.app.job.JobWorkItem; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManagerInternal; @@ -31,6 +34,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.job.JobStore.JobSet; import com.android.server.job.controllers.JobStatus; @@ -43,6 +47,10 @@ import org.junit.runner.RunWith; import java.io.File; import java.time.Clock; import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Set; /** * Test reading and writing correctly from file. @@ -690,20 +698,46 @@ public class JobStoreTest { taskStatus.getJob().isRequireBatteryNotLow()); } + @Test + public void testJobWorkItems() throws Exception { + JobWorkItem item1 = new JobWorkItem.Builder().build(); + item1.bumpDeliveryCount(); + PersistableBundle bundle = new PersistableBundle(); + bundle.putBoolean("test", true); + JobWorkItem item2 = new JobWorkItem.Builder().setExtras(bundle).build(); + item2.bumpDeliveryCount(); + JobWorkItem item3 = new JobWorkItem.Builder().setEstimatedNetworkBytes(1, 2).build(); + JobWorkItem item4 = new JobWorkItem.Builder().setMinimumNetworkChunkBytes(3).build(); + JobWorkItem item5 = new JobWorkItem.Builder().build(); + + JobInfo jobInfo = new JobInfo.Builder(0, mComponent) + .setPersisted(true) + .build(); + JobStatus jobStatus = + JobStatus.createFromJobInfo(jobInfo, SOME_UID, null, -1, null, null); + jobStatus.executingWork = new ArrayList<>(List.of(item1, item2)); + jobStatus.pendingWork = new ArrayList<>(List.of(item3, item4, item5)); + assertPersistedEquals(jobStatus); + } + /** * Helper function to kick a {@link JobInfo} through a persistence cycle and * assert that it's unchanged. */ private void assertPersistedEquals(JobInfo firstInfo) throws Exception { + assertPersistedEquals( + JobStatus.createFromJobInfo(firstInfo, SOME_UID, null, -1, null, null)); + } + + private void assertPersistedEquals(JobStatus original) throws Exception { mTaskStoreUnderTest.clear(); - JobStatus first = JobStatus.createFromJobInfo(firstInfo, SOME_UID, null, -1, null, null); - mTaskStoreUnderTest.add(first); + mTaskStoreUnderTest.add(original); waitForPendingIo(); final JobSet jobStatusSet = new JobSet(); mTaskStoreUnderTest.readJobMapFromDisk(jobStatusSet, true); final JobStatus second = jobStatusSet.getAllJobs().iterator().next(); - assertJobsEqual(first, second); + assertJobsEqual(original, second); } /** @@ -729,6 +763,59 @@ public class JobStoreTest { expected.getEarliestRunTime(), actual.getEarliestRunTime()); compareTimestampsSubjectToIoLatency("Late run-times not the same after read.", expected.getLatestRunTimeElapsed(), actual.getLatestRunTimeElapsed()); + + assertEquals(expected.hasWorkLocked(), actual.hasWorkLocked()); + if (expected.hasWorkLocked()) { + List<JobWorkItem> allWork = new ArrayList<>(); + if (expected.executingWork != null) { + allWork.addAll(expected.executingWork); + } + if (expected.pendingWork != null) { + allWork.addAll(expected.pendingWork); + } + // All work for freshly loaded Job will be pending. + assertNotNull(actual.pendingWork); + assertTrue(ArrayUtils.isEmpty(actual.executingWork)); + assertEquals(allWork.size(), actual.pendingWork.size()); + for (int i = 0; i < allWork.size(); ++i) { + JobWorkItem expectedItem = allWork.get(i); + JobWorkItem actualItem = actual.pendingWork.get(i); + assertJobWorkItemsEqual(expectedItem, actualItem); + } + } + } + + private void assertJobWorkItemsEqual(JobWorkItem expected, JobWorkItem actual) { + if (expected == null) { + assertNull(actual); + return; + } + assertNotNull(actual); + assertEquals(expected.getDeliveryCount(), actual.getDeliveryCount()); + assertEquals(expected.getEstimatedNetworkDownloadBytes(), + actual.getEstimatedNetworkDownloadBytes()); + assertEquals(expected.getEstimatedNetworkUploadBytes(), + actual.getEstimatedNetworkUploadBytes()); + assertEquals(expected.getMinimumNetworkChunkBytes(), actual.getMinimumNetworkChunkBytes()); + if (expected.getIntent() == null) { + assertNull(actual.getIntent()); + } else { + // filterEquals() just so happens to check almost everything that is persisted to disk. + assertTrue(expected.getIntent().filterEquals(actual.getIntent())); + assertEquals(expected.getIntent().getFlags(), actual.getIntent().getFlags()); + } + assertEquals(expected.getGrants(), actual.getGrants()); + PersistableBundle expectedExtras = expected.getExtras(); + PersistableBundle actualExtras = actual.getExtras(); + if (expectedExtras == null) { + assertNull(actualExtras); + } else { + assertEquals(expectedExtras.size(), actualExtras.size()); + Set<String> keys = expectedExtras.keySet(); + for (String key : keys) { + assertTrue(Objects.equals(expectedExtras.get(key), actualExtras.get(key))); + } + } } /** diff --git a/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java b/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java index 6cdae53bc6da..56df9d9cbf0d 100644 --- a/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java +++ b/services/tests/servicestests/src/com/android/server/locales/FakePackageConfigurationUpdater.java @@ -16,6 +16,8 @@ package com.android.server.locales; +import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; + import android.annotation.Nullable; import android.os.LocaleList; @@ -29,6 +31,8 @@ class FakePackageConfigurationUpdater implements PackageConfigurationUpdater { FakePackageConfigurationUpdater() {} + private int mGender = GRAMMATICAL_GENDER_NOT_SPECIFIED; + LocaleList mLocales = null; @Override @@ -43,6 +47,12 @@ class FakePackageConfigurationUpdater implements PackageConfigurationUpdater { } @Override + public PackageConfigurationUpdater setGrammaticalGender(int gender) { + mGender = gender; + return this; + } + + @Override public boolean commit() { return mLocales != null; } @@ -56,4 +66,10 @@ class FakePackageConfigurationUpdater implements PackageConfigurationUpdater { return mLocales; } + /** + * Returns the gender that were stored during the test run. + */ + int getGender() { + return mGender; + } } diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index 79ed7d1ff783..065aec5b2f64 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -16,6 +16,8 @@ package com.android.server.locales; +import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; + import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; @@ -234,7 +236,8 @@ public class LocaleManagerServiceTest { throws Exception { doReturn(DEFAULT_UID).when(mMockPackageManager) .getPackageUidAsUser(anyString(), any(), anyInt()); - doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES)) + doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES, + GRAMMATICAL_GENDER_NOT_SPECIFIED)) .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); String imPkgName = getCurrentInputMethodPackageName(); doReturn(Binder.getCallingUid()).when(mMockPackageManager) @@ -274,7 +277,8 @@ public class LocaleManagerServiceTest { doReturn(DEFAULT_UID).when(mMockPackageManager) .getPackageUidAsUser(anyString(), any(), anyInt()); setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES); - doReturn(new PackageConfig(/* nightMode = */ 0, /* locales = */ null)) + doReturn(new PackageConfig(/* nightMode = */ 0, /* locales = */ null, + GRAMMATICAL_GENDER_NOT_SPECIFIED)) .when(mMockActivityTaskManager).getApplicationConfig(any(), anyInt()); LocaleList locales = mLocaleManagerService.getApplicationLocales( @@ -288,7 +292,8 @@ public class LocaleManagerServiceTest { throws Exception { doReturn(Binder.getCallingUid()).when(mMockPackageManager) .getPackageUidAsUser(anyString(), any(), anyInt()); - doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES)) + doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES, + GRAMMATICAL_GENDER_NOT_SPECIFIED)) .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); LocaleList locales = @@ -303,7 +308,8 @@ public class LocaleManagerServiceTest { doReturn(DEFAULT_UID).when(mMockPackageManager) .getPackageUidAsUser(anyString(), any(), anyInt()); setUpPassingPermissionCheckFor(Manifest.permission.READ_APP_SPECIFIC_LOCALES); - doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES)) + doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES, + GRAMMATICAL_GENDER_NOT_SPECIFIED)) .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); LocaleList locales = @@ -319,7 +325,8 @@ public class LocaleManagerServiceTest { .getPackageUidAsUser(eq(DEFAULT_PACKAGE_NAME), any(), anyInt()); doReturn(Binder.getCallingUid()).when(mMockPackageManager) .getPackageUidAsUser(eq(DEFAULT_INSTALLER_PACKAGE_NAME), any(), anyInt()); - doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES)) + doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES, + GRAMMATICAL_GENDER_NOT_SPECIFIED)) .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); LocaleList locales = @@ -334,7 +341,8 @@ public class LocaleManagerServiceTest { throws Exception { doReturn(DEFAULT_UID).when(mMockPackageManager) .getPackageUidAsUser(anyString(), any(), anyInt()); - doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES)) + doReturn(new PackageConfig(/* nightMode = */ 0, DEFAULT_LOCALES, + GRAMMATICAL_GENDER_NOT_SPECIFIED)) .when(mMockActivityTaskManager).getApplicationConfig(anyString(), anyInt()); String imPkgName = getCurrentInputMethodPackageName(); doReturn(Binder.getCallingUid()).when(mMockPackageManager) diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java index cbf555ef4b5a..494796ee48eb 100644 --- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java @@ -16,6 +16,8 @@ package com.android.server.locales; +import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; @@ -168,7 +170,8 @@ public class SystemAppUpdateTrackerTest { /* isUpdatedSystemApp = */ true)) .when(mMockPackageManager).getApplicationInfo(eq(DEFAULT_PACKAGE_NAME_1), any()); doReturn(new ActivityTaskManagerInternal.PackageConfig(/* nightMode = */ 0, - DEFAULT_LOCALES)).when(mMockActivityTaskManager) + DEFAULT_LOCALES, GRAMMATICAL_GENDER_NOT_SPECIFIED)) + .when(mMockActivityTaskManager) .getApplicationConfig(anyString(), anyInt()); mPackageMonitor.onPackageUpdateFinished(DEFAULT_PACKAGE_NAME_1, @@ -186,7 +189,8 @@ public class SystemAppUpdateTrackerTest { /* isUpdatedSystemApp = */ true)) .when(mMockPackageManager).getApplicationInfo(eq(DEFAULT_PACKAGE_NAME_1), any()); doReturn(new ActivityTaskManagerInternal.PackageConfig(/* nightMode = */ 0, - DEFAULT_LOCALES)).when(mMockActivityTaskManager) + DEFAULT_LOCALES, GRAMMATICAL_GENDER_NOT_SPECIFIED)) + .when(mMockActivityTaskManager) .getApplicationConfig(anyString(), anyInt()); // first update diff --git a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java index c90064eaa810..9f914a1332b3 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/ConversationInfoTest.java @@ -30,6 +30,8 @@ import android.app.people.ConversationStatus; import android.content.LocusId; import android.content.pm.ShortcutInfo; import android.net.Uri; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; import org.junit.Test; import org.junit.runner.RunWith; @@ -270,4 +272,58 @@ public final class ConversationInfoTest { assertTrue(conversationInfoFromBackup.isContactStarred()); // ConversationStatus is a transient object and not persisted } + + @Test + public void testBuildFromProtoPayload() throws Exception { + ConversationStatus cs = new ConversationStatus.Builder("id", ACTIVITY_ANNIVERSARY).build(); + ConversationStatus cs2 = new ConversationStatus.Builder("id2", ACTIVITY_GAME).build(); + + ConversationInfo conversationInfo = new ConversationInfo.Builder() + .setShortcutId(SHORTCUT_ID) + .setLocusId(LOCUS_ID) + .setContactUri(CONTACT_URI) + .setContactPhoneNumber(PHONE_NUMBER) + .setNotificationChannelId(NOTIFICATION_CHANNEL_ID) + .setParentNotificationChannelId(PARENT_NOTIFICATION_CHANNEL_ID) + .setLastEventTimestamp(100L) + .setCreationTimestamp(200L) + .setShortcutFlags(ShortcutInfo.FLAG_LONG_LIVED + | ShortcutInfo.FLAG_CACHED_NOTIFICATIONS) + .setImportant(true) + .setNotificationSilenced(true) + .setBubbled(true) + .setDemoted(true) + .setPersonImportant(true) + .setPersonBot(true) + .setContactStarred(true) + .addOrUpdateStatus(cs) + .addOrUpdateStatus(cs2) + .build(); + + final ProtoOutputStream protoOutputStream = new ProtoOutputStream(); + conversationInfo.writeToProto(protoOutputStream); + ConversationInfo conversationInfoFromBackup = + ConversationInfo.readFromProto(new ProtoInputStream(protoOutputStream.getBytes())); + + assertEquals(SHORTCUT_ID, conversationInfoFromBackup.getShortcutId()); + assertEquals(LOCUS_ID, conversationInfoFromBackup.getLocusId()); + assertEquals(CONTACT_URI, conversationInfoFromBackup.getContactUri()); + assertEquals(PHONE_NUMBER, conversationInfoFromBackup.getContactPhoneNumber()); + assertEquals( + NOTIFICATION_CHANNEL_ID, conversationInfoFromBackup.getNotificationChannelId()); + assertEquals(PARENT_NOTIFICATION_CHANNEL_ID, + conversationInfoFromBackup.getParentNotificationChannelId()); + assertEquals(100L, conversationInfoFromBackup.getLastEventTimestamp()); + assertEquals(200L, conversationInfoFromBackup.getCreationTimestamp()); + assertTrue(conversationInfoFromBackup.isShortcutLongLived()); + assertTrue(conversationInfoFromBackup.isShortcutCachedForNotification()); + assertTrue(conversationInfoFromBackup.isImportant()); + assertTrue(conversationInfoFromBackup.isNotificationSilenced()); + assertTrue(conversationInfoFromBackup.isBubbled()); + assertTrue(conversationInfoFromBackup.isDemoted()); + assertTrue(conversationInfoFromBackup.isPersonImportant()); + assertTrue(conversationInfoFromBackup.isPersonBot()); + assertTrue(conversationInfoFromBackup.isContactStarred()); + // ConversationStatus is a transient object and not persisted + } } diff --git a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java index 050fbeaedfe2..304344ea6bff 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/DataManagerTest.java @@ -1678,7 +1678,7 @@ public final class DataManagerTest { mParentNotificationChannel.getImportance(), null, null, mParentNotificationChannel, null, null, true, 0, false, -1, false, null, null, - false, false, false, null, 0, false); + false, false, false, null, 0, false, 0); return true; }).when(mRankingMap).getRanking(eq(GENERIC_KEY), any(NotificationListenerService.Ranking.class)); @@ -1704,7 +1704,7 @@ public final class DataManagerTest { mNotificationChannel.getImportance(), null, null, mNotificationChannel, null, null, true, 0, false, -1, false, null, null, false, - false, false, null, 0, false); + false, false, null, 0, false, 0); return true; }).when(mRankingMap).getRanking(eq(CUSTOM_KEY), any(NotificationListenerService.Ranking.class)); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java index 12cd834d1d66..8a99c2cdcc6f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java @@ -193,7 +193,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { tweak.isConversation(), tweak.getConversationShortcutInfo(), tweak.getRankingAdjustment(), - tweak.isBubble() + tweak.isBubble(), + tweak.getProposedImportance() ); assertNotEquals(nru, nru2); } @@ -274,7 +275,8 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { isConversation(i), getShortcutInfo(i), getRankingAdjustment(i), - isBubble(i) + isBubble(i), + getProposedImportance(i) ); rankings[i] = ranking; } @@ -402,6 +404,10 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { return index % 3 - 1; } + private int getProposedImportance(int index) { + return index % 5 - 1; + } + private boolean isBubble(int index) { return index % 4 == 0; } @@ -443,6 +449,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase { assertEquals(comment, a.getConversationShortcutInfo().getId(), b.getConversationShortcutInfo().getId()); assertActionsEqual(a.getSmartActions(), b.getSmartActions()); + assertEquals(a.getProposedImportance(), b.getProposedImportance()); } private void detailedAssertEquals(RankingMap a, RankingMap b) { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java new file mode 100644 index 000000000000..87e86cb00f56 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordExtractorDataTest.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.notification; + +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_LOW; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.PendingIntent; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.UserHandle; +import android.service.notification.Adjustment; +import android.service.notification.SnoozeCriterion; +import android.service.notification.StatusBarNotification; + +import com.android.server.UiServiceTestCase; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Objects; + +public class NotificationRecordExtractorDataTest extends UiServiceTestCase { + + @Test + public void testHasDiffs_noDiffs() { + NotificationRecord r = generateRecord(); + + NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData( + 1, + r.getPackageVisibilityOverride(), + r.canShowBadge(), + r.canBubble(), + r.getNotification().isBubbleNotification(), + r.getChannel(), + r.getGroupKey(), + r.getPeopleOverride(), + r.getSnoozeCriteria(), + r.getUserSentiment(), + r.getSuppressedVisualEffects(), + r.getSystemGeneratedSmartActions(), + r.getSmartReplies(), + r.getImportance(), + r.getRankingScore(), + r.isConversation(), + r.getProposedImportance()); + + assertFalse(extractorData.hasDiffForRankingLocked(r, 1)); + assertFalse(extractorData.hasDiffForLoggingLocked(r, 1)); + } + + @Test + public void testHasDiffs_proposedImportanceChange() { + NotificationRecord r = generateRecord(); + + NotificationRecordExtractorData extractorData = new NotificationRecordExtractorData( + 1, + r.getPackageVisibilityOverride(), + r.canShowBadge(), + r.canBubble(), + r.getNotification().isBubbleNotification(), + r.getChannel(), + r.getGroupKey(), + r.getPeopleOverride(), + r.getSnoozeCriteria(), + r.getUserSentiment(), + r.getSuppressedVisualEffects(), + r.getSystemGeneratedSmartActions(), + r.getSmartReplies(), + r.getImportance(), + r.getRankingScore(), + r.isConversation(), + r.getProposedImportance()); + + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_HIGH); + Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0); + r.addAdjustment(adjustment); + r.applyAdjustments(); + + assertTrue(extractorData.hasDiffForRankingLocked(r, 1)); + assertTrue(extractorData.hasDiffForLoggingLocked(r, 1)); + } + + private NotificationRecord generateRecord() { + NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_LOW); + final Notification.Builder builder = new Notification.Builder(getContext()) + .setContentTitle("foo") + .setSmallIcon(android.R.drawable.sym_def_app_icon); + Notification n = builder.build(); + StatusBarNotification sbn = new StatusBarNotification("", "", 0, "", 0, + 0, n, UserHandle.ALL, null, System.currentTimeMillis()); + return new NotificationRecord(getContext(), sbn, channel); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index 5468220d9564..14b004827ece 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -19,6 +19,7 @@ import static android.app.NotificationChannel.USER_LOCKED_IMPORTANCE; import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; import static android.service.notification.Adjustment.KEY_IMPORTANCE; import static android.service.notification.Adjustment.KEY_NOT_CONVERSATION; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; @@ -755,6 +756,24 @@ public class NotificationRecordTest extends UiServiceTestCase { } @Test + public void testProposedImportance() { + StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, + true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, + false /* lights */, false /* defaultLights */, groupId /* group */); + NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel); + + assertEquals(IMPORTANCE_UNSPECIFIED, record.getProposedImportance()); + + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_IMPORTANCE_PROPOSAL, IMPORTANCE_DEFAULT); + record.addAdjustment(new Adjustment(mPkg, record.getKey(), signals, null, sbn.getUserId())); + + record.applyAdjustments(); + + assertEquals(IMPORTANCE_DEFAULT, record.getProposedImportance()); + } + + @Test public void testAppImportance_returnsCorrectly() { StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */, true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java index b16ca8b92848..b4a294d09b7e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -19,6 +19,7 @@ package com.android.server.notification; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; +import android.os.Parcel; import android.service.notification.ZenPolicy; import android.service.notification.nano.DNDPolicyProto; import android.test.suitebuilder.annotation.SmallTest; @@ -32,9 +33,13 @@ import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import org.junit.Test; import org.junit.runner.RunWith; +import java.lang.reflect.Field; +import java.util.ArrayList; + @SmallTest @RunWith(AndroidJUnit4.class) public class ZenPolicyTest extends UiServiceTestCase { + private static final String CLASS = "android.service.notification.ZenPolicy"; @Test public void testZenPolicyApplyAllowedToDisallowed() { @@ -524,6 +529,66 @@ public class ZenPolicyTest extends UiServiceTestCase { assertProtoMatches(policy, policy.toProto()); } + @Test + public void testTooLongLists_fromParcel() { + ArrayList<Integer> longList = new ArrayList<Integer>(50); + for (int i = 0; i < 50; i++) { + longList.add(ZenPolicy.STATE_UNSET); + } + + ZenPolicy.Builder builder = new ZenPolicy.Builder(); + ZenPolicy policy = builder.build(); + + try { + Field priorityCategories = Class.forName(CLASS).getDeclaredField( + "mPriorityCategories"); + priorityCategories.setAccessible(true); + priorityCategories.set(policy, longList); + + Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects"); + visualEffects.setAccessible(true); + visualEffects.set(policy, longList); + } catch (NoSuchFieldException e) { + fail(e.toString()); + } catch (ClassNotFoundException e) { + fail(e.toString()); + } catch (IllegalAccessException e) { + fail(e.toString()); + } + + Parcel parcel = Parcel.obtain(); + policy.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel); + + // Confirm that all the fields are accessible and UNSET + assertAllPriorityCategoriesUnsetExcept(fromParcel, -1); + assertAllVisualEffectsUnsetExcept(fromParcel, -1); + + // Because we don't access the lists directly, we also need to use reflection to make sure + // the lists are the right length. + try { + Field priorityCategories = Class.forName(CLASS).getDeclaredField( + "mPriorityCategories"); + priorityCategories.setAccessible(true); + ArrayList<Integer> pcList = (ArrayList<Integer>) priorityCategories.get(fromParcel); + assertEquals(ZenPolicy.NUM_PRIORITY_CATEGORIES, pcList.size()); + + + Field visualEffects = Class.forName(CLASS).getDeclaredField("mVisualEffects"); + visualEffects.setAccessible(true); + ArrayList<Integer> veList = (ArrayList<Integer>) visualEffects.get(fromParcel); + assertEquals(ZenPolicy.NUM_VISUAL_EFFECTS, veList.size()); + } catch (NoSuchFieldException e) { + fail(e.toString()); + } catch (ClassNotFoundException e) { + fail(e.toString()); + } catch (IllegalAccessException e) { + fail(e.toString()); + } + } + private void assertAllPriorityCategoriesUnsetExcept(ZenPolicy policy, int except) { if (except != ZenPolicy.PRIORITY_CATEGORY_REMINDERS) { assertEquals(ZenPolicy.STATE_UNSET, policy.getPriorityCategoryReminders()); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java index 5eecb0ac9305..8f110a880c48 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -1748,7 +1748,7 @@ public class ActivityStarterTests extends WindowTestsBase { TaskFragment inTaskFragment) { starter.startActivityInner(target, source, null /* voiceSession */, null /* voiceInteractor */, 0 /* startFlags */, - options, inTask, inTaskFragment, false /* restrictedBgActivity */, - null /* intentGrants */); + options, inTask, inTaskFragment, + BackgroundActivityStartController.BAL_ALLOW_DEFAULT, null /* intentGrants */); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index d1234e3de81a..8bb79e3f7ddc 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -398,7 +398,8 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { final ClientTransaction transaction = ClientTransaction.obtain( mActivity.app.getThread(), mActivity.token); transaction.addCallback(RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE)); - transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false)); + transaction.setLifecycleStateRequest(ResumeActivityItem.obtain( + /* isForward */ false, /* shouldSendCompatFakeFocus */ false)); verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0)) .scheduleTransaction(eq(transaction)); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java index 3c5bc1704954..b2abd44254d9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java @@ -188,7 +188,7 @@ public class DisplayWindowPolicyControllerTests extends WindowTestsBase { /* options */null, /* inTask */null, /* inTaskFragment */ null, - /* restrictedBgActivity */false, + /* balCode */ BackgroundActivityStartController.BAL_ALLOW_DEFAULT, /* intentGrants */null); assertEquals(result, START_ABORTED); @@ -212,7 +212,7 @@ public class DisplayWindowPolicyControllerTests extends WindowTestsBase { /* options= */null, /* inTask= */null, /* inTaskFragment= */ null, - /* restrictedBgActivity= */false, + /* balCode= */ BackgroundActivityStartController.BAL_ALLOW_DEFAULT, /* intentGrants= */null); assertEquals(result, START_ABORTED); diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java index 266565946d93..d2cca9f3644e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java @@ -40,6 +40,7 @@ import java.util.concurrent.TimeUnit; @SmallTest @Presubmit public class SurfaceSyncGroupTest { + private static final String TAG = "SurfaceSyncGroupTest"; private final Executor mExecutor = Runnable::run; @@ -51,7 +52,7 @@ public class SurfaceSyncGroupTest { @Test public void testSyncOne() throws InterruptedException { final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG); syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); SyncTarget syncTarget = new SyncTarget(); syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */); @@ -66,7 +67,7 @@ public class SurfaceSyncGroupTest { @Test public void testSyncMultiple() throws InterruptedException { final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG); syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); SyncTarget syncTarget1 = new SyncTarget(); SyncTarget syncTarget2 = new SyncTarget(); @@ -91,7 +92,7 @@ public class SurfaceSyncGroupTest { @Test public void testAddSyncWhenSyncComplete() { - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG); SyncTarget syncTarget1 = new SyncTarget(); SyncTarget syncTarget2 = new SyncTarget(); @@ -107,8 +108,8 @@ public class SurfaceSyncGroupTest { public void testMultipleSyncGroups() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG); syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); @@ -137,8 +138,8 @@ public class SurfaceSyncGroupTest { public void testAddSyncGroup() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG); syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); @@ -173,8 +174,8 @@ public class SurfaceSyncGroupTest { public void testAddSyncAlreadyComplete() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG); syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); @@ -205,8 +206,8 @@ public class SurfaceSyncGroupTest { public void testAddSyncAlreadyInASync_NewSyncReadyFirst() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG); syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); @@ -250,8 +251,8 @@ public class SurfaceSyncGroupTest { public void testAddSyncAlreadyInASync_OldSyncFinishesFirst() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(TAG); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(TAG); syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); @@ -295,7 +296,7 @@ public class SurfaceSyncGroupTest { SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction); final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG); syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); SurfaceControl.Transaction targetTransaction = spy(new StubTransaction()); @@ -320,7 +321,7 @@ public class SurfaceSyncGroupTest { SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction); final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG); syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); SurfaceControl.Transaction targetTransaction = spy(new StubTransaction()); @@ -339,7 +340,7 @@ public class SurfaceSyncGroupTest { @Test public void testAddToSameParentNoCrash() { final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(TAG); syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); SyncTarget syncTarget = new SyncTarget(); syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */); @@ -358,6 +359,10 @@ public class SurfaceSyncGroupTest { } private static class SyncTarget extends SurfaceSyncGroup { + SyncTarget() { + super("FakeSyncTarget"); + } + void onBufferReady() { SurfaceControl.Transaction t = new StubTransaction(); onTransactionReady(t); diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java index 2df3085c8751..1fa0c2300173 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupValidatorTestCase.java @@ -132,7 +132,7 @@ public class SurfaceSyncGroupValidatorTestCase implements ISurfaceValidatorTestC mLastExpanded = !mLastExpanded; mRenderingThread.pauseRendering(); - mSyncGroup = new SurfaceSyncGroup(); + mSyncGroup = new SurfaceSyncGroup(TAG); mSyncGroup.addToSync(mParent.getRootSurfaceControl()); ViewGroup.LayoutParams svParams = mSurfaceView.getLayoutParams(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java index 8bd414856394..d6cfd000abd0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; @@ -414,9 +415,10 @@ public class WindowProcessControllerTests extends WindowTestsBase { public void testTopActivityUiModeChangeScheduleConfigChange() { final ActivityRecord activity = createActivityRecord(mWpc); activity.setVisibleRequested(true); - doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any()); + doReturn(true).when(activity).applyAppSpecificConfig(anyInt(), any(), anyInt()); mWpc.updateAppSpecificSettingsForAllActivitiesInPackage(DEFAULT_COMPONENT_PACKAGE_NAME, - Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA")); + Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"), + GRAMMATICAL_GENDER_NOT_SPECIFIED); verify(activity).ensureActivityConfiguration(anyInt(), anyBoolean()); } @@ -425,8 +427,9 @@ public class WindowProcessControllerTests extends WindowTestsBase { final ActivityRecord activity = createActivityRecord(mWpc); activity.setVisibleRequested(true); mWpc.updateAppSpecificSettingsForAllActivitiesInPackage("com.different.package", - Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA")); - verify(activity, never()).applyAppSpecificConfig(anyInt(), any()); + Configuration.UI_MODE_NIGHT_YES, LocaleList.forLanguageTags("en-XA"), + GRAMMATICAL_GENDER_NOT_SPECIFIED); + verify(activity, never()).applyAppSpecificConfig(anyInt(), any(), anyInt()); verify(activity, never()).ensureActivityConfiguration(anyInt(), anyBoolean()); } diff --git a/services/usb/java/com/android/server/usb/UsbPortManager.java b/services/usb/java/com/android/server/usb/UsbPortManager.java index 1399458f0eca..f920f0f1f281 100644 --- a/services/usb/java/com/android/server/usb/UsbPortManager.java +++ b/services/usb/java/com/android/server/usb/UsbPortManager.java @@ -1082,6 +1082,7 @@ public class UsbPortManager { private void handlePortComplianceWarningLocked(PortInfo portInfo, IndentingPrintWriter pw) { logAndPrint(Log.INFO, pw, "USB port compliance warning changed: " + portInfo); + logToStatsdComplianceWarnings(portInfo); sendComplianceWarningBroadcastLocked(portInfo); } @@ -1108,6 +1109,33 @@ public class UsbPortManager { } } + // Constants have to be converted to stats-log constants + private static int[] toStatsLogConstant(@NonNull int[] complianceWarnings) { + IntArray complianceWarningsProto = new IntArray(); + for (int warning : complianceWarnings) { + switch (warning) { + case UsbPortStatus.COMPLIANCE_WARNING_OTHER: + complianceWarningsProto.add(FrameworkStatsLog + .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_OTHER); + continue; + case UsbPortStatus.COMPLIANCE_WARNING_DEBUG_ACCESSORY: + complianceWarningsProto.add(FrameworkStatsLog + .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_DEBUG_ACCESSORY); + continue; + case UsbPortStatus.COMPLIANCE_WARNING_BC_1_2: + complianceWarningsProto.add(FrameworkStatsLog + .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_BC_1_2); + continue; + case UsbPortStatus.COMPLIANCE_WARNING_MISSING_RP: + complianceWarningsProto.add(FrameworkStatsLog + .USB_COMPLIANCE_WARNINGS_REPORTED__COMPLIANCE_WARNINGS__COMPLIANCE_WARNING_MISSING_RP); + continue; + } + } + return complianceWarningsProto.toArray(); + } + + private void sendPortChangedBroadcastLocked(PortInfo portInfo) { final Intent intent = new Intent(UsbManager.ACTION_USB_PORT_CHANGED); intent.addFlags( @@ -1219,6 +1247,20 @@ public class UsbPortManager { } } + // Need to create new version to prevent double counting existing stats due + // to new broadcast + private void logToStatsdComplianceWarnings(PortInfo portInfo) { + if (portInfo.mUsbPortStatus == null) { + FrameworkStatsLog.write(FrameworkStatsLog.USB_COMPLIANCE_WARNINGS_REPORTED, + portInfo.mUsbPort.getId(), new int[0]); + return; + } + + FrameworkStatsLog.write(FrameworkStatsLog.USB_COMPLIANCE_WARNINGS_REPORTED, + portInfo.mUsbPort.getId(), + toStatsLogConstant(portInfo.mUsbPortStatus.getComplianceWarnings())); + } + public static void logAndPrint(int priority, IndentingPrintWriter pw, String msg) { Slog.println(priority, TAG, msg); if (pw != null) { diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 6a7a2f98d481..276bccdb9f23 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -25,6 +25,7 @@ import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENT import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP; import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER; +import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH; import android.annotation.NonNull; import android.annotation.Nullable; @@ -541,6 +542,11 @@ final class HotwordDetectionConnection { HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED); }); } + // Can improve to log exit reason if needed + HotwordMetricsLogger.writeKeyphraseTriggerEvent( + mDetectorType, + HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__SERVICE_CRASH, + mVoiceInteractionServiceUid); } @Override diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index f041adcc7d77..964328271866 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -22,6 +22,8 @@ import static android.app.ActivityManager.START_VOICE_HIDDEN_SESSION; import static android.app.ActivityManager.START_VOICE_NOT_ACTIVE_SESSION; import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; +import static com.android.server.policy.PhoneWindowManager.SYSTEM_DIALOG_REASON_ASSIST; + import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; @@ -62,6 +64,7 @@ import android.service.voice.IVoiceInteractionSession; import android.service.voice.VoiceInteractionService; import android.service.voice.VoiceInteractionServiceInfo; import android.system.OsConstants; +import android.text.TextUtils; import android.util.PrintWriterPrinter; import android.util.Slog; import android.view.IWindowManager; @@ -118,7 +121,9 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne public void onReceive(Context context, Intent intent) { if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) { String reason = intent.getStringExtra("reason"); - if (!CLOSE_REASON_VOICE_INTERACTION.equals(reason) && !"dream".equals(reason)) { + if (!CLOSE_REASON_VOICE_INTERACTION.equals(reason) + && !TextUtils.equals("dream", reason) + && !SYSTEM_DIALOG_REASON_ASSIST.equals(reason)) { synchronized (mServiceStub) { if (mActiveSession != null && mActiveSession.mSession != null) { try { diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java index efe242ce728e..d3a5885b324d 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/MeshActivity.java @@ -32,6 +32,7 @@ import android.view.View; import java.nio.FloatBuffer; import java.nio.ShortBuffer; +import java.util.ArrayList; public class MeshActivity extends Activity { @Override @@ -64,7 +65,9 @@ public class MeshActivity extends Activity { vertexBuffer.put(5, 400.0f); vertexBuffer.rewind(); Mesh mesh = Mesh.make( - meshSpec, Mesh.Mode.Triangles, vertexBuffer, 3, new Rect(0, 0, 1000, 1000)); + meshSpec, Mesh.TRIANGLES, vertexBuffer, 3, new Rect(0, 0, 1000, 1000)); + + canvas.drawMesh(mesh, BlendMode.COLOR, new Paint()); int numTriangles = 100; // number of triangles plus first 2 vertices @@ -95,12 +98,10 @@ public class MeshActivity extends Activity { } iVertexBuffer.rewind(); indexBuffer.rewind(); - Mesh mesh2 = Mesh.makeIndexed(meshSpec, Mesh.Mode.Triangles, iVertexBuffer, 102, - indexBuffer, new Rect(0, 0, 1000, 1000)); - + Mesh mesh2 = Mesh.makeIndexed(meshSpec, Mesh.TRIANGLES, iVertexBuffer, 102, indexBuffer, + new Rect(0, 0, 1000, 1000)); Paint paint = new Paint(); paint.setColor(Color.RED); - canvas.drawMesh(mesh, BlendMode.COLOR, new Paint()); canvas.drawMesh(mesh2, BlendMode.COLOR, paint); } @@ -114,10 +115,9 @@ public class MeshActivity extends Activity { + " color = vec4(1.0, 0.0, 0.0, 1.0);" + " return varyings.position;\n" + "}"; - Attribute[] attList = - new Attribute[] {new Attribute(MeshSpecification.FLOAT2, 0, "position")}; - Varying[] varyList = - new MeshSpecification.Varying[] {}; + ArrayList<Attribute> attList = new ArrayList<>(); + attList.add(new Attribute(MeshSpecification.FLOAT2, 0, "position")); + ArrayList<Varying> varyList = new ArrayList<>(); return MeshSpecification.make(attList, 8, varyList, vs, fs); } } diff --git a/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java new file mode 100644 index 000000000000..b24ac3cae795 --- /dev/null +++ b/tests/Internal/src/com/android/internal/app/AppLocaleCollectorTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import static com.android.internal.app.AppLocaleStore.AppLocaleResult.LocaleStatus.GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyObject; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.app.LocaleStore.LocaleInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** + * Unit tests for the {@link AppLocaleCollector}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppLocaleCollectorTest { + private static final String TAG = "AppLocaleCollectorTest"; + private AppLocaleCollector mAppLocaleCollector; + private LocaleStore.LocaleInfo mAppCurrentLocale; + private Set<LocaleInfo> mAllAppActiveLocales; + private Set<LocaleInfo> mImeLocales; + private List<LocaleInfo> mSystemCurrentLocales; + private Set<LocaleInfo> mSystemSupportedLocales; + private AppLocaleStore.AppLocaleResult mResult; + private static final String PKG1 = "pkg1"; + private static final int NONE = LocaleInfo.SUGGESTION_TYPE_NONE; + private static final int SIM = LocaleInfo.SUGGESTION_TYPE_SIM; + private static final int CFG = LocaleInfo.SUGGESTION_TYPE_CFG; + private static final int SIM_CFG = SIM | CFG; + private static final int CURRENT = LocaleInfo.SUGGESTION_TYPE_CURRENT; + private static final int SYSTEM = LocaleInfo.SUGGESTION_TYPE_SYSTEM_LANGUAGE; + private static final int OTHERAPP = LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE; + + @Before + public void setUp() throws Exception { + mAppLocaleCollector = spy( + new AppLocaleCollector(InstrumentationRegistry.getContext(), PKG1)); + + mAppCurrentLocale = createLocaleInfo("en-US", CURRENT); + mAllAppActiveLocales = initAllAppActivatedLocales(); + mImeLocales = initImeLocales(); + mSystemSupportedLocales = initSystemSupportedLocales(); + mSystemCurrentLocales = initSystemCurrentLocales(); + mResult = new AppLocaleStore.AppLocaleResult(GET_SUPPORTED_LANGUAGE_FROM_LOCAL_CONFIG, + initAppSupportedLocale()); + } + + @Test + public void testGetSupportedLocaleList() { + doReturn(mAppCurrentLocale).when(mAppLocaleCollector).getAppCurrentLocale(); + doReturn(mResult).when(mAppLocaleCollector).getAppSupportedLocales(); + doReturn(mAllAppActiveLocales).when(mAppLocaleCollector).getAllAppActiveLocales(); + doReturn(mImeLocales).when(mAppLocaleCollector).getActiveImeLocales(); + doReturn(mSystemSupportedLocales).when(mAppLocaleCollector).getSystemSupportedLocale( + anyObject(), eq(null), eq(true)); + doReturn(mSystemCurrentLocales).when(mAppLocaleCollector).getSystemCurrentLocale(); + + Set<LocaleInfo> result = mAppLocaleCollector.getSupportedLocaleList(null, true, false); + + HashMap<String, Integer> expectedResult = getExpectedResult(); + assertEquals(result.size(), expectedResult.size()); + for (LocaleInfo source : result) { + int suggestionFlags = expectedResult.getOrDefault(source.getId(), -1); + assertEquals(source.mSuggestionFlags, suggestionFlags); + } + } + + private HashMap<String, Integer> getExpectedResult() { + HashMap<String, Integer> map = new HashMap<>(); + map.put("en-US", CURRENT); // The locale current App activates. + map.put("fr", NONE); // The locale App and system support. + map.put("zu", NONE); // The locale App and system support. + map.put("en", NONE); // Use en because System supports en while APP supports en-CA, en-GB. + map.put("ko", NONE); // The locale App and system support. + map.put("en-AU", OTHERAPP); // The locale other App activates and current App supports. + map.put("en-CA", OTHERAPP); // The locale other App activates and current App supports. + map.put("ja-JP", OTHERAPP); // The locale other App activates and current App supports. + map.put("zh-Hant-TW", SIM); // The locale system activates. + map.put(createLocaleInfo("", SYSTEM).getId(), SYSTEM); // System language title + return map; + } + + private Set<LocaleInfo> initSystemSupportedLocales() { + return Set.of( + createLocaleInfo("en", NONE), + createLocaleInfo("fr", NONE), + createLocaleInfo("zu", NONE), + createLocaleInfo("ko", NONE), + // will be filtered because current App doesn't support. + createLocaleInfo("es-US", SIM_CFG) + ); + } + + private List<LocaleInfo> initSystemCurrentLocales() { + return List.of(createLocaleInfo("zh-Hant-TW", SIM), + // will be filtered because current App activates this locale. + createLocaleInfo("en-US", SIM)); + } + + private Set<LocaleInfo> initAllAppActivatedLocales() { + return Set.of( + createLocaleInfo("en-CA", OTHERAPP), + createLocaleInfo("en-AU", OTHERAPP), + createLocaleInfo("ja-JP", OTHERAPP), + // will be filtered because current App activates this locale. + createLocaleInfo("en-US", OTHERAPP)); + } + + private Set<LocaleInfo> initImeLocales() { + return Set.of( + // will be filtered because system activates zh-Hant-TW. + createLocaleInfo("zh-TW", OTHERAPP), + // will be filtered because current App's activats this locale. + createLocaleInfo("en-US", OTHERAPP)); + } + + private HashSet<Locale> initAppSupportedLocale() { + HashSet<Locale> hs = new HashSet(); + hs.add(Locale.forLanguageTag("en-US")); + hs.add(Locale.forLanguageTag("en-CA")); + hs.add(Locale.forLanguageTag("en-GB")); + hs.add(Locale.forLanguageTag("zh-TW")); + hs.add(Locale.forLanguageTag("ja")); + hs.add(Locale.forLanguageTag("fr")); + hs.add(Locale.forLanguageTag("zu")); + hs.add(Locale.forLanguageTag("ko")); + // will be filtered because it's not in the system language. + hs.add(Locale.forLanguageTag("mn")); + return hs; + } + + private LocaleInfo createLocaleInfo(String languageTag, int suggestionFlag) { + LocaleInfo localeInfo = LocaleStore.fromLocale(Locale.forLanguageTag(languageTag)); + localeInfo.mSuggestionFlags = suggestionFlag; + localeInfo.setTranslated(true); + return localeInfo; + } +} diff --git a/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java new file mode 100644 index 000000000000..bf6ece1ac19a --- /dev/null +++ b/tests/Internal/src/com/android/internal/app/LocaleStoreTest.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.app.LocaleStore.LocaleInfo; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; +import java.util.Set; + +/** + * Unit tests for the {@link LocaleStore}. + */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class LocaleStoreTest { + @Before + public void setUp() { + } + + @Test + public void testTransformImeLanguageTagToLocaleInfo() { + List<InputMethodSubtype> list = List.of( + new InputMethodSubtypeBuilder().setLanguageTag("en-US").build(), + new InputMethodSubtypeBuilder().setLanguageTag("zh-TW").build(), + new InputMethodSubtypeBuilder().setLanguageTag("ja-JP").build()); + + Set<LocaleInfo> localeSet = LocaleStore.transformImeLanguageTagToLocaleInfo(list); + + Set<String> expectedLanguageTag = Set.of("en-US", "zh-TW", "ja-JP"); + assertEquals(localeSet.size(), expectedLanguageTag.size()); + for (LocaleInfo info : localeSet) { + assertEquals(info.mSuggestionFlags, LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE); + assertTrue(expectedLanguageTag.contains(info.getId())); + } + } +} diff --git a/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java b/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java index 03f61fa49926..d5983d064c80 100644 --- a/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java +++ b/tests/SurfaceViewSyncTest/src/com/android/test/SurfaceViewSyncActivity.java @@ -89,7 +89,7 @@ public class SurfaceViewSyncActivity extends Activity implements SurfaceHolder.C mLastExpanded = !mLastExpanded; if (mEnableSyncSwitch.isChecked()) { - mSyncGroup = new SurfaceSyncGroup(); + mSyncGroup = new SurfaceSyncGroup(TAG); mSyncGroup.addToSync(container.getRootSurfaceControl()); } diff --git a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt index 01575963951b..9a7f8fa53d87 100644 --- a/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt +++ b/tools/lint/common/src/main/java/com/google/android/lint/PermissionMethodUtils.kt @@ -19,8 +19,10 @@ package com.google.android.lint import com.android.tools.lint.detector.api.getUMethod import org.jetbrains.uast.UAnnotation import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement import org.jetbrains.uast.UMethod import org.jetbrains.uast.UParameter +import org.jetbrains.uast.UQualifiedReferenceExpression fun isPermissionMethodCall(callExpression: UCallExpression): Boolean { val method = callExpression.resolve()?.getUMethod() ?: return false @@ -36,3 +38,15 @@ fun getPermissionMethodAnnotation(method: UMethod?): UAnnotation? = method?.uAnn fun hasPermissionNameAnnotation(parameter: UParameter) = parameter.annotations.any { it.hasQualifiedName(ANNOTATION_PERMISSION_NAME) } + +/** + * Attempts to return a CallExpression from a QualifiedReferenceExpression (or returns it directly if passed directly) + * @param callOrReferenceCall expected to be UCallExpression or UQualifiedReferenceExpression + * @return UCallExpression, if available + */ +fun findCallExpression(callOrReferenceCall: UElement?): UCallExpression? = + when (callOrReferenceCall) { + is UCallExpression -> callOrReferenceCall + is UQualifiedReferenceExpression -> callOrReferenceCall.selector as? UCallExpression + else -> null + } diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt index ee7dd62aaa36..485765ba9090 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionFix.kt @@ -20,28 +20,36 @@ import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.LintFix import com.android.tools.lint.detector.api.Location import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationBooleanValue +import com.android.tools.lint.detector.api.UastLintUtils.Companion.getAnnotationStringValues +import com.android.tools.lint.detector.api.findSelector import com.android.tools.lint.detector.api.getUMethod +import com.google.android.lint.findCallExpression import com.google.android.lint.getPermissionMethodAnnotation import com.google.android.lint.hasPermissionNameAnnotation import com.google.android.lint.isPermissionMethodCall +import com.intellij.psi.PsiClassType import com.intellij.psi.PsiType import org.jetbrains.kotlin.psi.psiUtil.parameterIndex +import org.jetbrains.uast.UBinaryExpression +import org.jetbrains.uast.UBlockExpression import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UExpressionList +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UThrowExpression +import org.jetbrains.uast.UastBinaryOperator import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.skipParenthesizedExprDown import org.jetbrains.uast.visitor.AbstractUastVisitor /** * Helper class that facilitates the creation of lint auto fixes - * - * Handles "Single" permission checks that should be migrated to @EnforcePermission(...), as well as consecutive checks - * that should be migrated to @EnforcePermission(allOf={...}) - * - * TODO: handle anyOf style annotations */ data class EnforcePermissionFix( val locations: List<Location>, val permissionNames: List<String>, val errorLevel: Boolean, + val anyOf: Boolean, ) { fun toLintFix(annotationLocation: Location): LintFix { val removeFixes = this.locations.map { @@ -67,8 +75,13 @@ data class EnforcePermissionFix( get() { val quotedPermissions = permissionNames.joinToString(", ") { """"$it"""" } + val attributeName = + if (permissionNames.size > 1) { + if (anyOf) "anyOf" else "allOf" + } else null + val annotationParameter = - if (permissionNames.size > 1) "allOf={$quotedPermissions}" + if (attributeName != null) "$attributeName={$quotedPermissions}" else quotedPermissions return "@$ANNOTATION_ENFORCE_PERMISSION($annotationParameter)" @@ -76,7 +89,7 @@ data class EnforcePermissionFix( companion object { /** - * conditionally constructs EnforcePermissionFix from a UCallExpression + * Conditionally constructs EnforcePermissionFix from a UCallExpression * @return EnforcePermissionFix if the called method is annotated with @PermissionMethod, else null */ fun fromCallExpression( @@ -85,26 +98,94 @@ data class EnforcePermissionFix( ): EnforcePermissionFix? { val method = callExpression.resolve()?.getUMethod() ?: return null val annotation = getPermissionMethodAnnotation(method) ?: return null - val enforces = method.returnType == PsiType.VOID + val returnsVoid = method.returnType == PsiType.VOID val orSelf = getAnnotationBooleanValue(annotation, "orSelf") ?: false + val anyOf = getAnnotationBooleanValue(annotation, "anyOf") ?: false return EnforcePermissionFix( listOf(getPermissionCheckLocation(context, callExpression)), getPermissionCheckValues(callExpression), - // If we detect that the PermissionMethod enforces that permission is granted, - // AND is of the "orSelf" variety, we are very confident that this is a behavior - // preserving migration to @EnforcePermission. Thus, the incident should be ERROR - // level. - errorLevel = enforces && orSelf + errorLevel = isErrorLevel(throws = returnsVoid, orSelf = orSelf), + anyOf, ) } + /** + * Conditionally constructs EnforcePermissionFix from a UCallExpression + * @return EnforcePermissionFix IF AND ONLY IF: + * * The condition of the if statement compares the return value of a + * PermissionMethod to one of the PackageManager.PermissionResult values + * * The expression inside the if statement does nothing but throw SecurityException + */ + fun fromIfExpression( + context: JavaContext, + ifExpression: UIfExpression + ): EnforcePermissionFix? { + val condition = ifExpression.condition.skipParenthesizedExprDown() + if (condition !is UBinaryExpression) return null + + val maybeLeftCall = findCallExpression(condition.leftOperand) + val maybeRightCall = findCallExpression(condition.rightOperand) + + val (callExpression, comparison) = + if (maybeLeftCall is UCallExpression) { + Pair(maybeLeftCall, condition.rightOperand) + } else if (maybeRightCall is UCallExpression) { + Pair(maybeRightCall, condition.leftOperand) + } else return null + + val permissionMethodAnnotation = getPermissionMethodAnnotation( + callExpression.resolve()?.getUMethod()) ?: return null + + val equalityCheck = + when (comparison.findSelector().asSourceString() + .filterNot(Char::isWhitespace)) { + "PERMISSION_GRANTED" -> UastBinaryOperator.IDENTITY_NOT_EQUALS + "PERMISSION_DENIED" -> UastBinaryOperator.IDENTITY_EQUALS + else -> return null + } + + if (condition.operator != equalityCheck) return null + + val throwExpression: UThrowExpression? = + ifExpression.thenExpression as? UThrowExpression + ?: (ifExpression.thenExpression as? UBlockExpression) + ?.expressions?.firstOrNull() + as? UThrowExpression - fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix = - EnforcePermissionFix( - individuals.flatMap { it.locations }, - individuals.flatMap { it.permissionNames }, - errorLevel = individuals.all(EnforcePermissionFix::errorLevel) + + val thrownClass = (throwExpression?.thrownExpression?.getExpressionType() + as? PsiClassType)?.resolve() ?: return null + if (!context.evaluator.inheritsFrom( + thrownClass, "java.lang.SecurityException")){ + return null + } + + val orSelf = getAnnotationBooleanValue(permissionMethodAnnotation, "orSelf") ?: false + val anyOf = getAnnotationBooleanValue(permissionMethodAnnotation, "anyOf") ?: false + + return EnforcePermissionFix( + listOf(context.getLocation(ifExpression)), + getPermissionCheckValues(callExpression), + errorLevel = isErrorLevel(throws = true, orSelf = orSelf), + anyOf = anyOf ) + } + + + fun compose(individuals: List<EnforcePermissionFix>): EnforcePermissionFix { + val anyOfs = individuals.filter(EnforcePermissionFix::anyOf) + // anyOf/allOf should be consistent. If we encounter some @PermissionMethods that are anyOf + // and others that aren't, we don't know what to do. + if (anyOfs.isNotEmpty() && anyOfs.size < individuals.size) { + throw AnyOfAllOfException() + } + return EnforcePermissionFix( + individuals.flatMap(EnforcePermissionFix::locations), + individuals.flatMap(EnforcePermissionFix::permissionNames), + errorLevel = individuals.all(EnforcePermissionFix::errorLevel), + anyOf = anyOfs.isNotEmpty() + ) + } /** * Given a permission check, get its proper location @@ -130,6 +211,7 @@ data class EnforcePermissionFix( * and pull out the permission value(s) being used. Also evaluates nested calls * to @PermissionMethod(s) in the given method's body. */ + @Throws(AnyOfAllOfException::class) private fun getPermissionCheckValues( callExpression: UCallExpression ): List<String> { @@ -139,38 +221,110 @@ data class EnforcePermissionFix( val visitedCalls = mutableSetOf<UCallExpression>() // don't visit the same call twice val bfsQueue = ArrayDeque(listOf(callExpression)) - // Breadth First Search - evalutaing nested @PermissionMethod(s) in the available + var anyOfAllOfState: AnyOfAllOfState = AnyOfAllOfState.INITIAL + + // Bread First Search - evaluating nested @PermissionMethod(s) in the available // source code for @PermissionName(s). while (bfsQueue.isNotEmpty()) { - val current = bfsQueue.removeFirst() - visitedCalls.add(current) - result.addAll(findPermissions(current)) - - current.resolve()?.getUMethod()?.accept(object : AbstractUastVisitor() { - override fun visitCallExpression(node: UCallExpression): Boolean { - if (isPermissionMethodCall(node) && node !in visitedCalls) { - bfsQueue.add(node) - } - return false - } - }) + val currentCallExpression = bfsQueue.removeFirst() + visitedCalls.add(currentCallExpression) + val currentPermissions = findPermissions(currentCallExpression) + result.addAll(currentPermissions) + + val currentAnnotation = getPermissionMethodAnnotation( + currentCallExpression.resolve()?.getUMethod()) + val currentAnyOf = getAnnotationBooleanValue(currentAnnotation, "anyOf") ?: false + + // anyOf/allOf should be consistent. If we encounter a nesting of @PermissionMethods + // where we start in an anyOf state and switch to allOf, or vice versa, + // we don't know what to do. + if (anyOfAllOfState == AnyOfAllOfState.INITIAL) { + if (currentAnyOf) anyOfAllOfState = AnyOfAllOfState.ANY_OF + else if (result.isNotEmpty()) anyOfAllOfState = AnyOfAllOfState.ALL_OF + } + + if (anyOfAllOfState == AnyOfAllOfState.ALL_OF && currentAnyOf) { + throw AnyOfAllOfException() + } + + if (anyOfAllOfState == AnyOfAllOfState.ANY_OF && + !currentAnyOf && currentPermissions.size > 1) { + throw AnyOfAllOfException() + } + + currentCallExpression.resolve()?.getUMethod() + ?.accept(PermissionCheckValuesVisitor(visitedCalls, bfsQueue)) } return result.toList() } + private enum class AnyOfAllOfState { + INITIAL, + ANY_OF, + ALL_OF + } + + /** + * Adds visited permission method calls to the provided + * queue in support of the BFS traversal happening while + * this is used + */ + private class PermissionCheckValuesVisitor( + val visitedCalls: Set<UCallExpression>, + val bfsQueue: ArrayDeque<UCallExpression> + ) : AbstractUastVisitor() { + override fun visitCallExpression(node: UCallExpression): Boolean { + if (isPermissionMethodCall(node) && node !in visitedCalls) { + bfsQueue.add(node) + } + return false + } + } + private fun findPermissions( callExpression: UCallExpression, ): List<String> { + val annotation = getPermissionMethodAnnotation(callExpression.resolve()?.getUMethod()) + + val hardCodedPermissions = (getAnnotationStringValues(annotation, "value") + ?: emptyArray()) + .toList() + val indices = callExpression.resolve()?.getUMethod() - ?.uastParameters - ?.filter(::hasPermissionNameAnnotation) - ?.mapNotNull { it.sourcePsi?.parameterIndex() } - ?: emptyList() + ?.uastParameters + ?.filter(::hasPermissionNameAnnotation) + ?.mapNotNull { it.sourcePsi?.parameterIndex() } + ?: emptyList() - return indices.mapNotNull { - callExpression.getArgumentForParameter(it)?.evaluateString() - } + val argPermissions = indices + .flatMap { i -> + when (val argument = callExpression.getArgumentForParameter(i)) { + null -> listOf(null) + is UExpressionList -> // varargs e.g. someMethod(String...) + argument.expressions.map(UExpression::evaluateString) + else -> listOf(argument.evaluateString()) + } + } + .filterNotNull() + + return hardCodedPermissions + argPermissions } + + /** + * If we detect that the PermissionMethod enforces that permission is granted, + * AND is of the "orSelf" variety, we are very confident that this is a behavior + * preserving migration to @EnforcePermission. Thus, the incident should be ERROR + * level. + */ + private fun isErrorLevel(throws: Boolean, orSelf: Boolean): Boolean = throws && orSelf } } +/** + * anyOf/allOf @PermissionMethods must be consistent to apply @EnforcePermission - + * meaning if we encounter some @PermissionMethods that are anyOf, and others are allOf, + * we don't know which to apply. + */ +class AnyOfAllOfException : Exception() { + override val message: String = "anyOf/allOf permission methods cannot be mixed" +} diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt index 9999a0ba7e06..11a283af3049 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetector.kt @@ -23,12 +23,11 @@ import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity +import com.google.android.lint.findCallExpression import org.jetbrains.uast.UBlockExpression -import org.jetbrains.uast.UCallExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.UIfExpression import org.jetbrains.uast.UMethod -import org.jetbrains.uast.UQualifiedReferenceExpression import org.jetbrains.uast.skipParenthesizedExprDown /** @@ -80,40 +79,41 @@ class SimpleManualPermissionEnforcementDetector : AidlImplementationDetector() { * as some other business logic is happening that prevents an automated fix. */ private fun accumulateSimplePermissionCheckFixes( - methodBody: UBlockExpression, - context: JavaContext - ): - EnforcePermissionFix? { - val singleFixes = mutableListOf<EnforcePermissionFix>() - for (expression in methodBody.expressions) { - singleFixes.add(getPermissionCheckFix(expression.skipParenthesizedExprDown(), context) - ?: break) - } - return when (singleFixes.size) { - 0 -> null - 1 -> singleFixes[0] - else -> EnforcePermissionFix.compose(singleFixes) + methodBody: UBlockExpression, + context: JavaContext + ): EnforcePermissionFix? { + try { + val singleFixes = mutableListOf<EnforcePermissionFix>() + for (expression in methodBody.expressions) { + val fix = getPermissionCheckFix( + expression.skipParenthesizedExprDown(), + context) ?: break + singleFixes.add(fix) + } + return when (singleFixes.size) { + 0 -> null + 1 -> singleFixes[0] + else -> EnforcePermissionFix.compose(singleFixes) + } + } catch (e: AnyOfAllOfException) { + return null } } + /** * If an expression boils down to a permission check, return * the helper for creating a lint auto fix to @EnforcePermission */ private fun getPermissionCheckFix(startingExpression: UElement?, context: JavaContext): EnforcePermissionFix? { - return when (startingExpression) { - is UQualifiedReferenceExpression -> getPermissionCheckFix( - startingExpression.selector, context - ) - - is UIfExpression -> getPermissionCheckFix(startingExpression.condition, context) - - is UCallExpression -> return EnforcePermissionFix - .fromCallExpression(context, startingExpression) - - else -> null + if (startingExpression is UIfExpression) { + return EnforcePermissionFix.fromIfExpression(context, startingExpression) + } + findCallExpression(startingExpression)?.let { + return EnforcePermissionFix.fromCallExpression(context, it) } + return null } companion object { diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt index bdf9c897b946..2ac550bd468a 100644 --- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/SimpleManualPermissionEnforcementDetectorTest.kt @@ -564,7 +564,228 @@ class SimpleManualPermissionEnforcementDetectorTest : LintDetectorTest() { ) } + fun testIfExpression() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Error: ITest permission check should be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + ^ + 1 errors, 0 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - if (mContext.checkCallingOrSelfPermission("android.permission.READ_CONTACTS", "foo") + - != PackageManager.PERMISSION_GRANTED) { + - throw new SecurityException("yikes!"); + - } + """ + ) + } + + fun testIfExpression_orSelfFalse_warning() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:7: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + ^ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 7: Annotate with @EnforcePermission: + @@ -5 +5 + + @android.annotation.EnforcePermission("android.permission.READ_CONTACTS") + @@ -7 +8 + - if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + - != PackageManager.PERMISSION_GRANTED) { + - throw new SecurityException("yikes!"); + - } + """ + ) + } + fun testIfExpression_otherSideEffect_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + public void test() throws android.os.RemoteException { + if (mContext.checkCallingPermission("android.permission.READ_CONTACTS", "foo") + != PackageManager.PERMISSION_GRANTED) { + doSomethingElse(); + throw new SecurityException("yikes!"); + } + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testAnyOf_hardCodedAndVarArgs() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod(anyOf = true) + private void helperHelper() { + helper("FOO", "BAR"); + } + + @android.content.pm.PermissionMethod(anyOf = true, value = {"BAZ", "BUZZ"}) + private void helper(@android.content.pm.PermissionName String... extraPermissions) {} + + @Override + public void test() throws android.os.RemoteException { + helperHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/Foo.java:17: Warning: ITest permission check can be converted to @EnforcePermission annotation [SimpleManualPermissionEnforcement] + helperHelper(); + ~~~~~~~~~~~~~~~ + 0 errors, 1 warnings + """ + ) + .expectFixDiffs( + """ + Fix for src/Foo.java line 17: Annotate with @EnforcePermission: + @@ -15 +15 + + @android.annotation.EnforcePermission(anyOf={"BAZ", "BUZZ", "FOO", "BAR"}) + @@ -17 +18 + - helperHelper(); + """ + ) + } + + + fun testAnyOfAllOf_mixedConsecutiveCalls_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod + private void allOfhelper() { + mContext.enforceCallingOrSelfPermission("FOO"); + mContext.enforceCallingOrSelfPermission("BAR"); + } + + @android.content.pm.PermissionMethod(anyOf = true, permissions = {"BAZ", "BUZZ"}) + private void anyOfHelper() {} + + @Override + public void test() throws android.os.RemoteException { + allOfhelper(); + anyOfHelper(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testAnyOfAllOf_mixedNestedCalls_ignored() { + lint().files( + java( + """ + import android.content.Context; + import android.content.pm.PermissionName;import android.test.ITest; + + public class Foo extends ITest.Stub { + private Context mContext; + + @android.content.pm.PermissionMethod(anyOf = true) + private void anyOfCheck(@PermissionName String... permissions) { + allOfCheck("BAZ", "BUZZ"); + } + + @android.content.pm.PermissionMethod + private void allOfCheck(@PermissionName String... permissions) {} + + @Override + public void test() throws android.os.RemoteException { + anyOfCheck("FOO", "BAR"); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } companion object { val stubs = arrayOf( diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt index 5ac8a0ba2604..362ac61ff6e8 100644 --- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt @@ -23,6 +23,8 @@ val contextStub: TestFile = java( public void enforceCallingPermission(@android.content.pm.PermissionName String permission, String message) {} @android.content.pm.PermissionMethod(orSelf = true) public int checkCallingOrSelfPermission(@android.content.pm.PermissionName String permission, String message) {} + @android.content.pm.PermissionMethod + public int checkCallingPermission(@android.content.pm.PermissionName String permission, String message) {} } """ ).indented() |