diff options
456 files changed, 13428 insertions, 3355 deletions
diff --git a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java index 3c361d772d3d..95730e836056 100644 --- a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java +++ b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java @@ -122,6 +122,8 @@ public class CanvasPerfTest { Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true) .recycle(); } + source.recycle(); + Runtime.getRuntime().gc(); } @Test @@ -141,6 +143,8 @@ public class CanvasPerfTest { Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true) .recycle(); } + source.recycle(); + Runtime.getRuntime().gc(); } @Test @@ -158,5 +162,7 @@ public class CanvasPerfTest { Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true) .recycle(); } + source.recycle(); + Runtime.getRuntime().gc(); } } diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index 44afbe6aff51..14cce19ef676 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -16,6 +16,11 @@ package com.android.server.job.controllers; +import static android.app.job.JobInfo.PRIORITY_DEFAULT; +import static android.app.job.JobInfo.PRIORITY_HIGH; +import static android.app.job.JobInfo.PRIORITY_LOW; +import static android.app.job.JobInfo.PRIORITY_MAX; +import static android.app.job.JobInfo.PRIORITY_MIN; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; @@ -43,9 +48,12 @@ import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.IndentingPrintWriter; +import android.util.KeyValueListParser; import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import android.util.SparseArrayMap; +import android.util.SparseIntArray; import android.util.SparseLongArray; import android.util.TimeUtils; @@ -91,6 +99,23 @@ public final class FlexibilityController extends StateController { */ private long mFallbackFlexibilityDeadlineMs = FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS; + /** + * The default deadline that all flexible constraints should be dropped by if a job lacks + * a deadline, keyed by job priority. + */ + private SparseLongArray mFallbackFlexibilityDeadlines = + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES; + /** + * The scores to use for each job, keyed by job priority. + */ + private SparseIntArray mFallbackFlexibilityDeadlineScores = + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES; + /** + * The amount of time to add (scaled by job run score) to the fallback flexibility deadline, + * keyed by job priority. + */ + private SparseLongArray mFallbackFlexibilityAdditionalScoreTimeFactors = + FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS; private long mRescheduledJobDeadline = FcConfig.DEFAULT_RESCHEDULED_JOB_DEADLINE_MS; private long mMaxRescheduledDeadline = FcConfig.DEFAULT_MAX_RESCHEDULED_DEADLINE_MS; @@ -117,10 +142,10 @@ public final class FlexibilityController extends StateController { /** * The percent of a job's lifecycle to drop number of required constraints. - * mPercentToDropConstraints[i] denotes that at x% of a Jobs lifecycle, - * the controller should have i+1 constraints dropped. + * mPercentsToDropConstraints[i] denotes that at x% of a Jobs lifecycle, + * the controller should have i+1 constraints dropped. Keyed by job priority. */ - private int[] mPercentToDropConstraints; + private SparseArray<int[]> mPercentsToDropConstraints; /** * Keeps track of what flexible constraints are satisfied at the moment. @@ -199,6 +224,86 @@ public final class FlexibilityController extends StateController { } }; + /** Helper object to track job run score for each app. */ + private static class JobScoreTracker { + private static class JobScoreBucket { + @ElapsedRealtimeLong + public long startTimeElapsed; + public int score; + + private void reset() { + startTimeElapsed = 0; + score = 0; + } + } + + private static final int NUM_SCORE_BUCKETS = 24; + private static final long MAX_TIME_WINDOW_MS = 24 * HOUR_IN_MILLIS; + private final JobScoreBucket[] mScoreBuckets = new JobScoreBucket[NUM_SCORE_BUCKETS]; + private int mScoreBucketIndex = 0; + + public void addScore(int add, long nowElapsed) { + JobScoreBucket bucket = mScoreBuckets[mScoreBucketIndex]; + if (bucket == null) { + bucket = new JobScoreBucket(); + bucket.startTimeElapsed = nowElapsed; + mScoreBuckets[mScoreBucketIndex] = bucket; + } else if (bucket.startTimeElapsed < nowElapsed - MAX_TIME_WINDOW_MS) { + // The bucket is too old. + bucket.reset(); + bucket.startTimeElapsed = nowElapsed; + } else if (bucket.startTimeElapsed + < nowElapsed - MAX_TIME_WINDOW_MS / NUM_SCORE_BUCKETS) { + // The current bucket's duration has completed. Move on to the next bucket. + mScoreBucketIndex = (mScoreBucketIndex + 1) % NUM_SCORE_BUCKETS; + addScore(add, nowElapsed); + return; + } + + bucket.score += add; + } + + public int getScore(long nowElapsed) { + int score = 0; + final long earliestElapsed = nowElapsed - MAX_TIME_WINDOW_MS; + for (JobScoreBucket bucket : mScoreBuckets) { + if (bucket != null && bucket.startTimeElapsed >= earliestElapsed) { + score += bucket.score; + } + } + return score; + } + + public void dump(@NonNull IndentingPrintWriter pw, long nowElapsed) { + pw.print("{"); + + boolean printed = false; + for (int x = 0; x < mScoreBuckets.length; ++x) { + final int idx = (mScoreBucketIndex + 1 + x) % mScoreBuckets.length; + final JobScoreBucket jsb = mScoreBuckets[idx]; + if (jsb == null || jsb.startTimeElapsed == 0) { + continue; + } + if (printed) { + pw.print(", "); + } + TimeUtils.formatDuration(jsb.startTimeElapsed, nowElapsed, pw); + pw.print("="); + pw.print(jsb.score); + printed = true; + } + + pw.print("}"); + } + } + + /** + * Set of {@link JobScoreTracker JobScoreTrackers} for each app. + * Keyed by source UID -> source package. + **/ + private final SparseArrayMap<String, JobScoreTracker> mJobScoreTrackers = + new SparseArrayMap<>(); + private static final int MSG_CHECK_ALL_JOBS = 0; /** Check the jobs in {@link #mJobsToCheck} */ private static final int MSG_CHECK_JOBS = 1; @@ -228,8 +333,8 @@ public final class FlexibilityController extends StateController { mFcConfig = new FcConfig(); mFlexibilityAlarmQueue = new FlexibilityAlarmQueue( mContext, AppSchedulingModuleThread.get().getLooper()); - mPercentToDropConstraints = - mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + mPercentsToDropConstraints = + FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; mPrefetchController = prefetchController; if (mFlexibilityEnabled) { @@ -272,6 +377,36 @@ public final class FlexibilityController extends StateController { } @Override + public void prepareForExecutionLocked(JobStatus jobStatus) { + // Use the job's requested priority to determine its score since that is what the developer + // selected and it will be stable across job runs. + final int score = mFallbackFlexibilityDeadlineScores + .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100); + JobScoreTracker jobScoreTracker = + mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName()); + if (jobScoreTracker == null) { + jobScoreTracker = new JobScoreTracker(); + mJobScoreTrackers.add(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), + jobScoreTracker); + } + jobScoreTracker.addScore(score, sElapsedRealtimeClock.millis()); + } + + @Override + public void unprepareFromExecutionLocked(JobStatus jobStatus) { + // The job didn't actually start. Undo the score increase. + JobScoreTracker jobScoreTracker = + mJobScoreTrackers.get(jobStatus.getSourceUid(), jobStatus.getSourcePackageName()); + if (jobScoreTracker == null) { + Slog.e(TAG, "Unprepared a job that didn't result in a score change"); + return; + } + final int score = mFallbackFlexibilityDeadlineScores + .get(jobStatus.getJob().getPriority(), jobStatus.getJob().getPriority() / 100); + jobScoreTracker.addScore(-score, sElapsedRealtimeClock.millis()); + } + + @Override @GuardedBy("mLock") public void maybeStopTrackingJobLocked(JobStatus js, JobStatus incomingJob) { if (js.clearTrackingController(JobStatus.TRACKING_FLEXIBILITY)) { @@ -286,12 +421,33 @@ public final class FlexibilityController extends StateController { public void onAppRemovedLocked(String packageName, int uid) { final int userId = UserHandle.getUserId(uid); mPrefetchLifeCycleStart.delete(userId, packageName); + mJobScoreTrackers.delete(uid, packageName); + for (int i = mJobsToCheck.size() - 1; i >= 0; --i) { + final JobStatus js = mJobsToCheck.valueAt(i); + if ((js.getSourceUid() == uid && js.getSourcePackageName().equals(packageName)) + || (js.getUid() == uid && js.getCallingPackageName().equals(packageName))) { + mJobsToCheck.removeAt(i); + } + } } @Override @GuardedBy("mLock") public void onUserRemovedLocked(int userId) { mPrefetchLifeCycleStart.delete(userId); + for (int u = mJobScoreTrackers.numMaps() - 1; u >= 0; --u) { + final int uid = mJobScoreTrackers.keyAt(u); + if (UserHandle.getUserId(uid) == userId) { + mJobScoreTrackers.deleteAt(u); + } + } + for (int i = mJobsToCheck.size() - 1; i >= 0; --i) { + final JobStatus js = mJobsToCheck.valueAt(i); + if (UserHandle.getUserId(js.getSourceUid()) == userId + || UserHandle.getUserId(js.getUid()) == userId) { + mJobsToCheck.removeAt(i); + } + } } boolean isEnabled() { @@ -308,9 +464,9 @@ public final class FlexibilityController extends StateController { || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP // Only exclude DEFAULT+ priority jobs for BFGS+ apps || (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE - && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT) + && js.getEffectivePriority() >= PRIORITY_DEFAULT) // For apps in the power allowlist, automatically exclude DEFAULT+ priority jobs. - || (js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT + || (js.getEffectivePriority() >= PRIORITY_DEFAULT && mPowerAllowlistedApps.contains(js.getSourcePackageName())) || hasEnoughSatisfiedConstraintsLocked(js) || mService.isCurrentlyRunningLocked(js); @@ -462,7 +618,14 @@ public final class FlexibilityController extends StateController { @VisibleForTesting @GuardedBy("mLock") - long getLifeCycleEndElapsedLocked(JobStatus js, long earliest) { + int getScoreLocked(int uid, @NonNull String pkgName, long nowElapsed) { + final JobScoreTracker scoreTracker = mJobScoreTrackers.get(uid, pkgName); + return scoreTracker == null ? 0 : scoreTracker.getScore(nowElapsed); + } + + @VisibleForTesting + @GuardedBy("mLock") + long getLifeCycleEndElapsedLocked(JobStatus js, long nowElapsed, long earliest) { if (js.getJob().isPrefetch()) { final long estimatedLaunchTime = mPrefetchController.getNextEstimatedLaunchTimeLocked(js); @@ -486,15 +649,28 @@ public final class FlexibilityController extends StateController { (long) Math.scalb(mRescheduledJobDeadline, js.getNumPreviousAttempts() - 2), mMaxRescheduledDeadline); } - return js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME - ? earliest + mFallbackFlexibilityDeadlineMs : js.getLatestRunTimeElapsed(); + if (js.getLatestRunTimeElapsed() == JobStatus.NO_LATEST_RUNTIME) { + // Intentionally use the effective priority here. If a job's priority was effectively + // lowered, it will be less likely to run quickly given other policies in JobScheduler. + // Thus, there's no need to further delay the job based on flex policy. + final int jobPriority = js.getEffectivePriority(); + final int jobScore = + getScoreLocked(js.getSourceUid(), js.getSourcePackageName(), nowElapsed); + // Set an upper limit on the fallback deadline so that the delay doesn't become extreme. + final long fallbackDeadlineMs = Math.min(3 * mFallbackFlexibilityDeadlineMs, + mFallbackFlexibilityDeadlines.get(jobPriority, mFallbackFlexibilityDeadlineMs) + + mFallbackFlexibilityAdditionalScoreTimeFactors + .get(jobPriority, MINUTE_IN_MILLIS) * jobScore); + return earliest + fallbackDeadlineMs; + } + return js.getLatestRunTimeElapsed(); } @VisibleForTesting @GuardedBy("mLock") int getCurPercentOfLifecycleLocked(JobStatus js, long nowElapsed) { final long earliest = getLifeCycleBeginningElapsedLocked(js); - final long latest = getLifeCycleEndElapsedLocked(js, earliest); + final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest); if (latest == NO_LIFECYCLE_END || earliest >= nowElapsed) { return 0; } @@ -510,7 +686,8 @@ public final class FlexibilityController extends StateController { @GuardedBy("mLock") long getNextConstraintDropTimeElapsedLocked(JobStatus js) { final long earliest = getLifeCycleBeginningElapsedLocked(js); - final long latest = getLifeCycleEndElapsedLocked(js, earliest); + final long latest = + getLifeCycleEndElapsedLocked(js, sElapsedRealtimeClock.millis(), earliest); return getNextConstraintDropTimeElapsedLocked(js, earliest, latest); } @@ -518,15 +695,27 @@ public final class FlexibilityController extends StateController { @ElapsedRealtimeLong @GuardedBy("mLock") long getNextConstraintDropTimeElapsedLocked(JobStatus js, long earliest, long latest) { + final int[] percentsToDropConstraints = + getPercentsToDropConstraints(js.getEffectivePriority()); if (latest == NO_LIFECYCLE_END - || js.getNumDroppedFlexibleConstraints() == mPercentToDropConstraints.length) { + || js.getNumDroppedFlexibleConstraints() == percentsToDropConstraints.length) { return NO_LIFECYCLE_END; } - final int percent = mPercentToDropConstraints[js.getNumDroppedFlexibleConstraints()]; + final int percent = percentsToDropConstraints[js.getNumDroppedFlexibleConstraints()]; final long percentInTime = ((latest - earliest) * percent) / 100; return earliest + percentInTime; } + @NonNull + private int[] getPercentsToDropConstraints(int priority) { + int[] percentsToDropConstraints = mPercentsToDropConstraints.get(priority); + if (percentsToDropConstraints == null) { + Slog.wtf(TAG, "No %-to-drop for priority " + JobInfo.getPriorityString(priority)); + return new int[]{50, 60, 70, 80}; + } + return percentsToDropConstraints; + } + @Override @GuardedBy("mLock") public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { @@ -681,10 +870,12 @@ public final class FlexibilityController extends StateController { Integer.bitCount(getRelevantAppliedConstraintsLocked(js)); js.setNumAppliedFlexibleConstraints(numAppliedConstraints); + final int[] percentsToDropConstraints = + getPercentsToDropConstraints(js.getEffectivePriority()); final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed); int toDrop = 0; for (int i = 0; i < numAppliedConstraints; i++) { - if (curPercent >= mPercentToDropConstraints[i]) { + if (curPercent >= percentsToDropConstraints[i]) { toDrop++; } } @@ -705,8 +896,10 @@ public final class FlexibilityController extends StateController { final int curPercent = getCurPercentOfLifecycleLocked(js, nowElapsed); int toDrop = 0; final int jsMaxFlexibleConstraints = js.getNumAppliedFlexibleConstraints(); + final int[] percentsToDropConstraints = + getPercentsToDropConstraints(js.getEffectivePriority()); for (int i = 0; i < jsMaxFlexibleConstraints; i++) { - if (curPercent >= mPercentToDropConstraints[i]) { + if (curPercent >= percentsToDropConstraints[i]) { toDrop++; } } @@ -733,7 +926,7 @@ public final class FlexibilityController extends StateController { return mTrackedJobs.size(); } - public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate) { + public void dump(IndentingPrintWriter pw, Predicate<JobStatus> predicate, long nowElapsed) { for (int i = 0; i < mTrackedJobs.size(); i++) { ArraySet<JobStatus> jobs = mTrackedJobs.get(i); for (int j = 0; j < jobs.size(); j++) { @@ -744,8 +937,18 @@ public final class FlexibilityController extends StateController { js.printUniqueId(pw); pw.print(" from "); UserHandle.formatUid(pw, js.getSourceUid()); - pw.print(" Num Required Constraints: "); + pw.print("-> Num Required Constraints: "); pw.print(js.getNumRequiredFlexibleConstraints()); + + pw.print(", lifecycle=["); + final long earliest = getLifeCycleBeginningElapsedLocked(js); + pw.print(earliest); + pw.print(", ("); + pw.print(getCurPercentOfLifecycleLocked(js, nowElapsed)); + pw.print("%), "); + pw.print(getLifeCycleEndElapsedLocked(js, nowElapsed, earliest)); + pw.print("]"); + pw.println(); } } @@ -768,7 +971,7 @@ public final class FlexibilityController extends StateController { public void scheduleDropNumConstraintsAlarm(JobStatus js, long nowElapsed) { synchronized (mLock) { final long earliest = getLifeCycleBeginningElapsedLocked(js); - final long latest = getLifeCycleEndElapsedLocked(js, earliest); + final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest); final long nextTimeElapsed = getNextConstraintDropTimeElapsedLocked(js, earliest, latest); @@ -936,10 +1139,16 @@ public final class FlexibilityController extends StateController { FC_CONFIG_PREFIX + "flexibility_deadline_proximity_limit_ms"; static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE = FC_CONFIG_PREFIX + "fallback_flexibility_deadline_ms"; + static final String KEY_FALLBACK_FLEXIBILITY_DEADLINES = + FC_CONFIG_PREFIX + "fallback_flexibility_deadlines"; + static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES = + FC_CONFIG_PREFIX + "fallback_flexibility_deadline_scores"; + static final String KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS = + FC_CONFIG_PREFIX + "fallback_flexibility_deadline_additional_score_time_factors"; static final String KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = FC_CONFIG_PREFIX + "min_time_between_flexibility_alarms_ms"; - static final String KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS = - FC_CONFIG_PREFIX + "percents_to_drop_num_flexible_constraints"; + static final String KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS = + FC_CONFIG_PREFIX + "percents_to_drop_flexible_constraints"; static final String KEY_MAX_RESCHEDULED_DEADLINE_MS = FC_CONFIG_PREFIX + "max_rescheduled_deadline_ms"; static final String KEY_RESCHEDULED_JOB_DEADLINE_MS = @@ -952,9 +1161,50 @@ public final class FlexibilityController extends StateController { static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS; @VisibleForTesting static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 24 * HOUR_IN_MILLIS; - private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS; + static final SparseLongArray DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES = new SparseLongArray(); + static final SparseIntArray DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES = + new SparseIntArray(); + static final SparseLongArray + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS = + new SparseLongArray(); @VisibleForTesting - final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80}; + static final SparseArray<int[]> DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS = + new SparseArray<>(); + + static { + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_MAX, HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_HIGH, 6 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_DEFAULT, 12 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_LOW, 24 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.put(PRIORITY_MIN, 48 * HOUR_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_MAX, 5); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_HIGH, 4); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_DEFAULT, 3); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_LOW, 2); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put(PRIORITY_MIN, 1); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_MAX, 0); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_HIGH, 4 * MINUTE_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_DEFAULT, 3 * MINUTE_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_LOW, 2 * MINUTE_IN_MILLIS); + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .put(PRIORITY_MIN, 1 * MINUTE_IN_MILLIS); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_MAX, new int[]{1, 2, 3, 4}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_HIGH, new int[]{33, 50, 60, 75}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_DEFAULT, new int[]{50, 60, 70, 80}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_LOW, new int[]{50, 60, 70, 80}); + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .put(PRIORITY_MIN, new int[]{55, 65, 75, 85}); + } + + private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS; private static final long DEFAULT_RESCHEDULED_JOB_DEADLINE_MS = HOUR_IN_MILLIS; private static final long DEFAULT_MAX_RESCHEDULED_DEADLINE_MS = 5 * DAY_IN_MILLIS; @VisibleForTesting @@ -968,9 +1218,11 @@ public final class FlexibilityController extends StateController { public long FALLBACK_FLEXIBILITY_DEADLINE_MS = DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS; public long MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS; - /** The percentages of a jobs' lifecycle to drop the number of required constraints. */ - public int[] PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS = - DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + /** + * The percentages of a jobs' lifecycle to drop the number of required constraints. + * Keyed by job priority. + */ + public SparseArray<int[]> PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS = new SparseArray<>(); /** Initial fallback flexible deadline for rescheduled jobs. */ public long RESCHEDULED_JOB_DEADLINE_MS = DEFAULT_RESCHEDULED_JOB_DEADLINE_MS; /** The max deadline for rescheduled jobs. */ @@ -980,10 +1232,56 @@ public final class FlexibilityController extends StateController { * it in order to run jobs. */ public long UNSEEN_CONSTRAINT_GRACE_PERIOD_MS = DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS; + /** + * The base fallback deadlines to use if a job doesn't have its own deadline. Values are in + * milliseconds and keyed by job priority. + */ + public final SparseLongArray FALLBACK_FLEXIBILITY_DEADLINES = new SparseLongArray(); + /** + * The score to ascribe to each job, keyed by job priority. + */ + public final SparseIntArray FALLBACK_FLEXIBILITY_DEADLINE_SCORES = new SparseIntArray(); + /** + * How much additional time to increase the fallback deadline by based on the app's current + * job run score. Values are in + * milliseconds and keyed by job priority. + */ + public final SparseLongArray FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS = + new SparseLongArray(); + + FcConfig() { + // Copy the values from the DEFAULT_* data structures to avoid accidentally modifying + // the DEFAULT_* data structures in other parts of the code. + for (int i = 0; i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.size(); ++i) { + FALLBACK_FLEXIBILITY_DEADLINES.put( + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.keyAt(i), + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.valueAt(i)); + } + for (int i = 0; i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.size(); ++i) { + FALLBACK_FLEXIBILITY_DEADLINE_SCORES.put( + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.keyAt(i), + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES.valueAt(i)); + } + for (int i = 0; + i < DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS.size(); + ++i) { + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS.put( + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .keyAt(i), + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS + .valueAt(i)); + } + for (int i = 0; i < DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.size(); ++i) { + PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.put( + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.keyAt(i), + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS.valueAt(i)); + } + } @GuardedBy("mLock") public void processConstantLocked(@NonNull DeviceConfig.Properties properties, @NonNull String key) { + // TODO(257322915): add appropriate minimums and maximums to constants when parsing switch (key) { case KEY_APPLIED_CONSTRAINTS: APPLIED_CONSTRAINTS = @@ -1034,6 +1332,33 @@ public final class FlexibilityController extends StateController { properties.getLong(key, DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS); if (mFallbackFlexibilityDeadlineMs != FALLBACK_FLEXIBILITY_DEADLINE_MS) { mFallbackFlexibilityDeadlineMs = FALLBACK_FLEXIBILITY_DEADLINE_MS; + } + break; + case KEY_FALLBACK_FLEXIBILITY_DEADLINES: + if (parsePriorityToLongKeyValueString( + properties.getString(key, null), + FALLBACK_FLEXIBILITY_DEADLINES, + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES)) { + mFallbackFlexibilityDeadlines = FALLBACK_FLEXIBILITY_DEADLINES; + mShouldReevaluateConstraints = true; + } + break; + case KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES: + if (parsePriorityToIntKeyValueString( + properties.getString(key, null), + FALLBACK_FLEXIBILITY_DEADLINE_SCORES, + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_SCORES)) { + mFallbackFlexibilityDeadlineScores = FALLBACK_FLEXIBILITY_DEADLINE_SCORES; + mShouldReevaluateConstraints = true; + } + break; + case KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS: + if (parsePriorityToLongKeyValueString( + properties.getString(key, null), + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS, + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS)) { + mFallbackFlexibilityAdditionalScoreTimeFactors = + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS; mShouldReevaluateConstraints = true; } break; @@ -1056,25 +1381,69 @@ public final class FlexibilityController extends StateController { mShouldReevaluateConstraints = true; } break; - case KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS: - String dropPercentString = properties.getString(key, ""); - PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS = - parsePercentToDropString(dropPercentString); - if (PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS != null - && !Arrays.equals(mPercentToDropConstraints, - PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS)) { - mPercentToDropConstraints = PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS; + case KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS: + if (parsePercentToDropKeyValueString( + properties.getString(key, null), + PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS)) { + mPercentsToDropConstraints = PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; mShouldReevaluateConstraints = true; } break; } } - private int[] parsePercentToDropString(String s) { - String[] dropPercentString = s.split(","); + private boolean parsePercentToDropKeyValueString(@Nullable String s, + SparseArray<int[]> into, SparseArray<int[]> defaults) { + final KeyValueListParser priorityParser = new KeyValueListParser(','); + try { + priorityParser.setString(s); + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Bad percent to drop key value string given", e); + // Clear the string and continue with the defaults. + priorityParser.setString(null); + } + + final int[] oldMax = into.get(PRIORITY_MAX); + final int[] oldHigh = into.get(PRIORITY_HIGH); + final int[] oldDefault = into.get(PRIORITY_DEFAULT); + final int[] oldLow = into.get(PRIORITY_LOW); + final int[] oldMin = into.get(PRIORITY_MIN); + + final int[] newMax = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_MAX), null)); + final int[] newHigh = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_HIGH), null)); + final int[] newDefault = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_DEFAULT), null)); + final int[] newLow = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_LOW), null)); + final int[] newMin = parsePercentToDropString(priorityParser.getString( + String.valueOf(PRIORITY_MIN), null)); + + into.put(PRIORITY_MAX, newMax == null ? defaults.get(PRIORITY_MAX) : newMax); + into.put(PRIORITY_HIGH, newHigh == null ? defaults.get(PRIORITY_HIGH) : newHigh); + into.put(PRIORITY_DEFAULT, + newDefault == null ? defaults.get(PRIORITY_DEFAULT) : newDefault); + into.put(PRIORITY_LOW, newLow == null ? defaults.get(PRIORITY_LOW) : newLow); + into.put(PRIORITY_MIN, newMin == null ? defaults.get(PRIORITY_MIN) : newMin); + + return !Arrays.equals(oldMax, into.get(PRIORITY_MAX)) + || !Arrays.equals(oldHigh, into.get(PRIORITY_HIGH)) + || !Arrays.equals(oldDefault, into.get(PRIORITY_DEFAULT)) + || !Arrays.equals(oldLow, into.get(PRIORITY_LOW)) + || !Arrays.equals(oldMin, into.get(PRIORITY_MIN)); + } + + @Nullable + private int[] parsePercentToDropString(@Nullable String s) { + if (s == null || s.isEmpty()) { + return null; + } + final String[] dropPercentString = s.split("\\|"); int[] dropPercentInt = new int[Integer.bitCount(FLEXIBLE_CONSTRAINTS)]; if (dropPercentInt.length != dropPercentString.length) { - return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + return null; } int prevPercent = 0; for (int i = 0; i < dropPercentString.length; i++) { @@ -1083,11 +1452,15 @@ public final class FlexibilityController extends StateController { Integer.parseInt(dropPercentString[i]); } catch (NumberFormatException ex) { Slog.e(TAG, "Provided string was improperly formatted.", ex); - return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + return null; } if (dropPercentInt[i] < prevPercent) { Slog.wtf(TAG, "Percents to drop constraints were not in increasing order."); - return DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; + return null; + } + if (dropPercentInt[i] > 100) { + Slog.e(TAG, "Found % over 100"); + return null; } prevPercent = dropPercentInt[i]; } @@ -1095,6 +1468,102 @@ public final class FlexibilityController extends StateController { return dropPercentInt; } + /** + * Parses the input string, expecting it to a key-value string where the keys are job + * priorities, and replaces everything in {@code into} with the values from the string, + * or the default values if the string contains none. + * + * Returns true if any values changed. + */ + private boolean parsePriorityToIntKeyValueString(@Nullable String s, + SparseIntArray into, SparseIntArray defaults) { + final KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(s); + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Bad string given", e); + // Clear the string and continue with the defaults. + parser.setString(null); + } + + final int oldMax = into.get(PRIORITY_MAX); + final int oldHigh = into.get(PRIORITY_HIGH); + final int oldDefault = into.get(PRIORITY_DEFAULT); + final int oldLow = into.get(PRIORITY_LOW); + final int oldMin = into.get(PRIORITY_MIN); + + final int newMax = parser.getInt(String.valueOf(PRIORITY_MAX), + defaults.get(PRIORITY_MAX)); + final int newHigh = parser.getInt(String.valueOf(PRIORITY_HIGH), + defaults.get(PRIORITY_HIGH)); + final int newDefault = parser.getInt(String.valueOf(PRIORITY_DEFAULT), + defaults.get(PRIORITY_DEFAULT)); + final int newLow = parser.getInt(String.valueOf(PRIORITY_LOW), + defaults.get(PRIORITY_LOW)); + final int newMin = parser.getInt(String.valueOf(PRIORITY_MIN), + defaults.get(PRIORITY_MIN)); + + into.put(PRIORITY_MAX, newMax); + into.put(PRIORITY_HIGH, newHigh); + into.put(PRIORITY_DEFAULT, newDefault); + into.put(PRIORITY_LOW, newLow); + into.put(PRIORITY_MIN, newMin); + + return oldMax != newMax + || oldHigh != newHigh + || oldDefault != newDefault + || oldLow != newLow + || oldMin != newMin; + } + + /** + * Parses the input string, expecting it to a key-value string where the keys are job + * priorities, and replaces everything in {@code into} with the values from the string, + * or the default values if the string contains none. + * + * Returns true if any values changed. + */ + private boolean parsePriorityToLongKeyValueString(@Nullable String s, + SparseLongArray into, SparseLongArray defaults) { + final KeyValueListParser parser = new KeyValueListParser(','); + try { + parser.setString(s); + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Bad string given", e); + // Clear the string and continue with the defaults. + parser.setString(null); + } + + final long oldMax = into.get(PRIORITY_MAX); + final long oldHigh = into.get(PRIORITY_HIGH); + final long oldDefault = into.get(PRIORITY_DEFAULT); + final long oldLow = into.get(PRIORITY_LOW); + final long oldMin = into.get(PRIORITY_MIN); + + final long newMax = parser.getLong(String.valueOf(PRIORITY_MAX), + defaults.get(PRIORITY_MAX)); + final long newHigh = parser.getLong(String.valueOf(PRIORITY_HIGH), + defaults.get(PRIORITY_HIGH)); + final long newDefault = parser.getLong(String.valueOf(PRIORITY_DEFAULT), + defaults.get(PRIORITY_DEFAULT)); + final long newLow = parser.getLong(String.valueOf(PRIORITY_LOW), + defaults.get(PRIORITY_LOW)); + final long newMin = parser.getLong(String.valueOf(PRIORITY_MIN), + defaults.get(PRIORITY_MIN)); + + into.put(PRIORITY_MAX, newMax); + into.put(PRIORITY_HIGH, newHigh); + into.put(PRIORITY_DEFAULT, newDefault); + into.put(PRIORITY_LOW, newLow); + into.put(PRIORITY_MIN, newMin); + + return oldMax != newMax + || oldHigh != newHigh + || oldDefault != newDefault + || oldLow != newLow + || oldMin != newMin; + } + private void dump(IndentingPrintWriter pw) { pw.println(); pw.print(FlexibilityController.class.getSimpleName()); @@ -1111,10 +1580,15 @@ public final class FlexibilityController extends StateController { pw.println(")"); pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println(); pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println(); + pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINES, FALLBACK_FLEXIBILITY_DEADLINES).println(); + pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES, + FALLBACK_FLEXIBILITY_DEADLINE_SCORES).println(); + pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS, + FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS).println(); pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS, MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS).println(); - pw.print(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, - PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS).println(); + pw.print(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS).println(); pw.print(KEY_RESCHEDULED_JOB_DEADLINE_MS, RESCHEDULED_JOB_DEADLINE_MS).println(); pw.print(KEY_MAX_RESCHEDULED_DEADLINE_MS, MAX_RESCHEDULED_DEADLINE_MS).println(); pw.print(KEY_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS, UNSEEN_CONSTRAINT_GRACE_PERIOD_MS) @@ -1171,7 +1645,21 @@ public final class FlexibilityController extends StateController { pw.println(mPowerAllowlistedApps); pw.println(); - mFlexibilityTracker.dump(pw, predicate); + mFlexibilityTracker.dump(pw, predicate, nowElapsed); + + pw.println(); + pw.println("Job scores:"); + pw.increaseIndent(); + mJobScoreTrackers.forEach((uid, pkgName, jobScoreTracker) -> { + pw.print(uid); + pw.print("/"); + pw.print(pkgName); + pw.print(": "); + jobScoreTracker.dump(pw, nowElapsed); + pw.println(); + }); + pw.decreaseIndent(); + pw.println(); mFlexibilityAlarmQueue.dump(pw); } diff --git a/config/preloaded-classes b/config/preloaded-classes index c49971eb68ae..11b24f53ceaf 100644 --- a/config/preloaded-classes +++ b/config/preloaded-classes @@ -6172,8 +6172,6 @@ android.os.VibratorInfo$FrequencyProfile$1 android.os.VibratorInfo$FrequencyProfile android.os.VibratorInfo android.os.VibratorManager -android.os.VintfObject -android.os.VintfRuntimeInfo android.os.WorkSource$1 android.os.WorkSource$WorkChain$1 android.os.WorkSource$WorkChain diff --git a/core/api/current.txt b/core/api/current.txt index 49b6d2506680..671956d54d74 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -195,6 +195,7 @@ package android { field public static final String MANAGE_DEVICE_POLICY_SYSTEM_APPS = "android.permission.MANAGE_DEVICE_POLICY_SYSTEM_APPS"; field public static final String MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS = "android.permission.MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS"; field public static final String MANAGE_DEVICE_POLICY_SYSTEM_UPDATES = "android.permission.MANAGE_DEVICE_POLICY_SYSTEM_UPDATES"; + field @FlaggedApi("com.android.net.thread.flags.thread_user_restriction_enabled") public static final String MANAGE_DEVICE_POLICY_THREAD_NETWORK = "android.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK"; field public static final String MANAGE_DEVICE_POLICY_TIME = "android.permission.MANAGE_DEVICE_POLICY_TIME"; field public static final String MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING = "android.permission.MANAGE_DEVICE_POLICY_USB_DATA_SIGNALLING"; field public static final String MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER = "android.permission.MANAGE_DEVICE_POLICY_USB_FILE_TRANSFER"; @@ -12446,6 +12447,7 @@ package android.content.pm { method @NonNull public android.content.pm.PackageInstaller.Session openSession(int) throws java.io.IOException; method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback); method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback, @NonNull android.os.Handler); + method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalState(@NonNull android.content.pm.PackageInstaller.UnarchivalState) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalStatus(int, int, long, @Nullable android.app.PendingIntent) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String, @NonNull android.content.IntentSender) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException; @@ -12675,6 +12677,14 @@ package android.content.pm { field public static final int USER_ACTION_UNSPECIFIED = 0; // 0x0 } + @FlaggedApi("android.content.pm.archiving") public static final class PackageInstaller.UnarchivalState { + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createGenericErrorState(int); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createInsufficientStorageState(int, long, @Nullable android.app.PendingIntent); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createNoConnectivityState(int); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createOkState(int); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createUserActionRequiredState(int, @NonNull android.app.PendingIntent); + } + public class PackageItemInfo { ctor public PackageItemInfo(); ctor public PackageItemInfo(android.content.pm.PackageItemInfo); @@ -17875,6 +17885,7 @@ package android.graphics.text { field public static final int HYPHENATION_FREQUENCY_FULL = 2; // 0x2 field public static final int HYPHENATION_FREQUENCY_NONE = 0; // 0x0 field public static final int HYPHENATION_FREQUENCY_NORMAL = 1; // 0x1 + field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2 field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1 field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0 } @@ -22153,16 +22164,17 @@ package android.media { method public void onJetUserIdUpdate(android.media.JetPlayer, int, int); } - @FlaggedApi("android.media.audio.loudness_configurator_api") public class LoudnessCodecConfigurator { + @FlaggedApi("android.media.audio.loudness_configurator_api") public class LoudnessCodecController implements java.lang.AutoCloseable { method @FlaggedApi("android.media.audio.loudness_configurator_api") public boolean addMediaCodec(@NonNull android.media.MediaCodec); - method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecConfigurator create(); - method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecConfigurator create(@NonNull java.util.concurrent.Executor, @NonNull android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener); - method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public android.os.Bundle getLoudnessCodecParams(@NonNull android.media.AudioTrack, @NonNull android.media.MediaCodec); + method public void close(); + method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int); + method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int, @NonNull java.util.concurrent.Executor, @NonNull android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener); + method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public android.os.Bundle getLoudnessCodecParams(@NonNull android.media.MediaCodec); + method @FlaggedApi("android.media.audio.loudness_configurator_api") public void release(); method @FlaggedApi("android.media.audio.loudness_configurator_api") public void removeMediaCodec(@NonNull android.media.MediaCodec); - method @FlaggedApi("android.media.audio.loudness_configurator_api") public void setAudioTrack(@Nullable android.media.AudioTrack); } - @FlaggedApi("android.media.audio.loudness_configurator_api") public static interface LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener { + @FlaggedApi("android.media.audio.loudness_configurator_api") public static interface LoudnessCodecController.OnLoudnessCodecUpdateListener { method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public default android.os.Bundle onLoudnessCodecUpdate(@NonNull android.media.MediaCodec, @NonNull android.os.Bundle); } @@ -23316,6 +23328,8 @@ package android.media { field public static final String KEY_AUDIO_SESSION_ID = "audio-session-id"; field public static final String KEY_BITRATE_MODE = "bitrate-mode"; field public static final String KEY_BIT_RATE = "bitrate"; + field @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public static final String KEY_BUFFER_BATCH_MAX_OUTPUT_SIZE = "buffer-batch-max-output-size"; + field @FlaggedApi("com.android.media.codec.flags.large_audio_frame") public static final String KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE = "buffer-batch-threshold-output-size"; field public static final String KEY_CAPTION_SERVICE_NUMBER = "caption-service-number"; field public static final String KEY_CAPTURE_RATE = "capture-rate"; field public static final String KEY_CHANNEL_COUNT = "channel-count"; @@ -25472,34 +25486,34 @@ package android.media.audiofx { field public short preset; } - public class Virtualizer extends android.media.audiofx.AudioEffect { - ctor public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException; - method public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public boolean getStrengthSupported(); - method public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener); - method public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - method public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; - field public static final int PARAM_STRENGTH = 1; // 0x1 - field public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0 - field public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1 - field public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2 - field public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0 - field public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3 + @Deprecated public class Virtualizer extends android.media.audiofx.AudioEffect { + ctor @Deprecated public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public boolean getStrengthSupported(); + method @Deprecated public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener); + method @Deprecated public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + method @Deprecated public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; + field @Deprecated public static final int PARAM_STRENGTH = 1; // 0x1 + field @Deprecated public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0 + field @Deprecated public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1 + field @Deprecated public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2 + field @Deprecated public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0 + field @Deprecated public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3 } - public static interface Virtualizer.OnParameterChangeListener { - method public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short); + @Deprecated public static interface Virtualizer.OnParameterChangeListener { + method @Deprecated public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short); } - public static class Virtualizer.Settings { - ctor public Virtualizer.Settings(); - ctor public Virtualizer.Settings(String); - field public short strength; + @Deprecated public static class Virtualizer.Settings { + ctor @Deprecated public Virtualizer.Settings(); + ctor @Deprecated public Virtualizer.Settings(String); + field @Deprecated public short strength; } public class Visualizer { @@ -42218,9 +42232,11 @@ package android.telecom { method public android.graphics.drawable.Icon getIcon(); method public CharSequence getLabel(); method public CharSequence getShortDescription(); + method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @NonNull public java.util.Set<android.telecom.PhoneAccountHandle> getSimultaneousCallingRestriction(); method public android.net.Uri getSubscriptionAddress(); method public java.util.List<java.lang.String> getSupportedUriSchemes(); method public boolean hasCapabilities(int); + method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") public boolean hasSimultaneousCallingRestriction(); method public boolean isEnabled(); method public boolean supportsUriScheme(String); method public android.telecom.PhoneAccount.Builder toBuilder(); @@ -42261,12 +42277,14 @@ package android.telecom { ctor public PhoneAccount.Builder(android.telecom.PhoneAccount); method public android.telecom.PhoneAccount.Builder addSupportedUriScheme(String); method public android.telecom.PhoneAccount build(); + method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @NonNull public android.telecom.PhoneAccount.Builder clearSimultaneousCallingRestriction(); method public android.telecom.PhoneAccount.Builder setAddress(android.net.Uri); method public android.telecom.PhoneAccount.Builder setCapabilities(int); method public android.telecom.PhoneAccount.Builder setExtras(android.os.Bundle); method public android.telecom.PhoneAccount.Builder setHighlightColor(int); method public android.telecom.PhoneAccount.Builder setIcon(android.graphics.drawable.Icon); method public android.telecom.PhoneAccount.Builder setShortDescription(CharSequence); + method @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @NonNull public android.telecom.PhoneAccount.Builder setSimultaneousCallingRestriction(@NonNull java.util.Set<android.telecom.PhoneAccountHandle>); method public android.telecom.PhoneAccount.Builder setSubscriptionAddress(android.net.Uri); method public android.telecom.PhoneAccount.Builder setSupportedUriSchemes(java.util.List<java.lang.String>); } @@ -44944,7 +44962,7 @@ package android.telephony { method public void addOnSubscriptionsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.SubscriptionManager.OnSubscriptionsChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void addSubscriptionsIntoGroup(@NonNull java.util.List<java.lang.Integer>, @NonNull android.os.ParcelUuid); method public boolean canManageSubscription(android.telephony.SubscriptionInfo); - method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull public android.telephony.SubscriptionManager createForAllUserProfiles(); + method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) public android.telephony.SubscriptionManager createForAllUserProfiles(); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>); method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context); method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList(); @@ -46947,6 +46965,7 @@ package android.text { field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_ANY_OVERLAP; field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_ALL; field @NonNull public static final android.text.Layout.TextInclusionStrategy INCLUSION_STRATEGY_CONTAINS_CENTER; + field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; // 0x2 field public static final int JUSTIFICATION_MODE_INTER_WORD = 1; // 0x1 field public static final int JUSTIFICATION_MODE_NONE = 0; // 0x0 } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index debf1bfdfc8c..fe32bad40e1f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3959,6 +3959,7 @@ package android.content.pm { method public void setInstallAsInstantApp(boolean); method public void setInstallAsVirtualPreload(); method public void setRequestDowngrade(boolean); + method @FlaggedApi("android.content.pm.recoverability_detection") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackImpactLevel(int); method @FlaggedApi("android.content.pm.rollback_lifetime") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackLifetimeMillis(long); method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setStaged(); } @@ -4127,6 +4128,9 @@ package android.content.pm { field public static final int ROLLBACK_DATA_POLICY_RESTORE = 0; // 0x0 field public static final int ROLLBACK_DATA_POLICY_RETAIN = 2; // 0x2 field public static final int ROLLBACK_DATA_POLICY_WIPE = 1; // 0x1 + field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_HIGH = 1; // 0x1 + field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_LOW = 0; // 0x0 + field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2; // 0x2 field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_HIDDEN = 0; // 0x0 field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_VISIBLE = 1; // 0x1 field public static final int SYSTEM_APP_STATE_INSTALLED = 2; // 0x2 @@ -14712,7 +14716,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimIst(); - method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) public android.telephony.CellIdentity getLastKnownCellIdentity(); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public android.telephony.CellIdentity getLastKnownCellIdentity(); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Map<java.lang.Integer,java.lang.Integer> getLogicalToPhysicalSlotMapping(); method public int getMaxNumberOfSimultaneouslyActiveSims(); method public static long getMaxNumberVerificationTimeoutMillis(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index a866a34166f7..b8b98a3cb3ba 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -156,6 +156,7 @@ package android.app { field public static final int PROCESS_CAPABILITY_ALL_IMPLICIT = 6; // 0x6 field public static final int PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK = 8; // 0x8 field public static final int PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK = 32; // 0x20 + field public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 5; // 0x5 field public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; // 0x4 field public static final int PROCESS_STATE_TOP = 2; // 0x2 field public static final int STOP_USER_ON_SWITCH_DEFAULT = -1; // 0xffffffff @@ -376,6 +377,7 @@ package android.app { public class NotificationManager { method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean); method public void cleanUpCallersAfter(long); + method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy(); method public android.content.ComponentName getEffectsSuppressor(); method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String); method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean); @@ -1209,6 +1211,10 @@ package android.content.res { package android.content.rollback { + public final class RollbackInfo implements android.os.Parcelable { + method @FlaggedApi("android.content.pm.recoverability_detection") public int getRollbackImpactLevel(); + } + public final class RollbackManager { method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void blockRollbackManager(long); method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void expireRollbackForPackage(@NonNull String); @@ -3022,6 +3028,10 @@ package android.service.notification { method @Deprecated public boolean isBound(); } + public final class ZenPolicy implements android.os.Parcelable { + method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy); + } + public static final class ZenPolicy.Builder { ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy); } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index b2c64756e4bf..9d20f3c47bb5 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -712,6 +712,8 @@ public class ActivityManager { /** @hide Process is hosting a foreground service due to a system binding. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + @TestApi public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = ProcessStateEnum.BOUND_FOREGROUND_SERVICE; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 4b2e93fda171..d8d136ae4df9 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1533,10 +1533,11 @@ public class AppOpsManager { public static final int OP_RESERVED_FOR_TESTING = AppProtoEnums.APP_OP_RESERVED_FOR_TESTING; /** - * Rapid clearing of notifications by a notification listener, see b/289080543 for details + * Rapid clearing of notifications by a notification listener * * @hide */ + // See b/289080543 for more details public static final int OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER = AppProtoEnums.APP_OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER; @@ -2373,10 +2374,11 @@ public class AppOpsManager { "android:reserved_for_testing"; /** - * Rapid clearing of notifications by a notification listener, see b/289080543 for details + * Rapid clearing of notifications by a notification listener * * @hide */ + // See b/289080543 for more details @SystemApi @FlaggedApi(FLAG_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER_APP_OP_ENABLED) public static final String OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER = diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index afa513dbaaef..c6712c044539 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -413,7 +413,9 @@ public final class ApplicationStartInfo implements Parcelable { * @hide */ public void setIntent(Intent startIntent) { - mStartIntent = startIntent; + if (startIntent != null) { + mStartIntent = startIntent.maybeStripForHistory(); + } } /** @@ -548,6 +550,8 @@ public final class ApplicationStartInfo implements Parcelable { /** * The intent used to launch the application. * + * <p class="note"> Note: Intent is stripped and does not include extras.</p> + * * <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p> */ @SuppressLint("IntentBuilderName") @@ -662,6 +666,7 @@ public final class ApplicationStartInfo implements Parcelable { private static final String PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP = "timestamp"; private static final String PROTO_SERIALIZER_ATTRIBUTE_KEY = "key"; private static final String PROTO_SERIALIZER_ATTRIBUTE_TS = "ts"; + private static final String PROTO_SERIALIZER_ATTRIBUTE_INTENT = "intent"; /** * Write to a protocol buffer output stream. Protocol buffer message definition at {@link @@ -702,10 +707,17 @@ public final class ApplicationStartInfo implements Parcelable { } proto.write(ApplicationStartInfoProto.START_TYPE, mStartType); if (mStartIntent != null) { - Parcel parcel = Parcel.obtain(); - mStartIntent.writeToParcel(parcel, 0); - proto.write(ApplicationStartInfoProto.START_INTENT, parcel.marshall()); - parcel.recycle(); + ByteArrayOutputStream intentBytes = new ByteArrayOutputStream(); + ObjectOutputStream intentOut = new ObjectOutputStream(intentBytes); + TypedXmlSerializer serializer = Xml.resolveSerializer(intentOut); + serializer.startDocument(null, true); + serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent.saveToXml(serializer); + serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + serializer.endDocument(); + proto.write(ApplicationStartInfoProto.START_INTENT, + intentBytes.toByteArray()); + intentOut.close(); } proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode); proto.end(token); @@ -772,15 +784,17 @@ public final class ApplicationStartInfo implements Parcelable { mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE); break; case (int) ApplicationStartInfoProto.START_INTENT: - byte[] startIntentBytes = proto.readBytes( - ApplicationStartInfoProto.START_INTENT); - if (startIntentBytes.length > 0) { - Parcel parcel = Parcel.obtain(); - parcel.unmarshall(startIntentBytes, 0, startIntentBytes.length); - parcel.setDataPosition(0); - mStartIntent = Intent.CREATOR.createFromParcel(parcel); - parcel.recycle(); + ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes( + ApplicationStartInfoProto.START_INTENT)); + ObjectInputStream intentIn = new ObjectInputStream(intentBytes); + try { + TypedXmlPullParser parser = Xml.resolvePullParser(intentIn); + XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT); + mStartIntent = Intent.restoreFromXml(parser); + } catch (XmlPullParserException e) { + // Intent lost } + intentIn.close(); break; case (int) ApplicationStartInfoProto.LAUNCH_MODE: mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE); diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 7370fc36c23e..5b044f616487 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -119,7 +119,7 @@ interface IActivityClientController { oneway void setShowWhenLocked(in IBinder token, boolean showWhenLocked); oneway void setInheritShowWhenLocked(in IBinder token, boolean setInheritShownWhenLocked); - oneway void setTurnScreenOn(in IBinder token, boolean turnScreenOn); + void setTurnScreenOn(in IBinder token, boolean turnScreenOn); oneway void setAllowCrossUidActivitySwitchFromBelow(in IBinder token, boolean allowed); oneway void reportActivityFullyDrawn(in IBinder token, boolean restoredFromBundle); oneway void overrideActivityTransition(IBinder token, boolean open, int enterAnim, int exitAnim, diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index c3adbc30641f..578105f9f99e 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -38,6 +38,7 @@ import android.service.notification.IConditionProvider; import android.service.notification.INotificationListener; import android.service.notification.NotificationListenerFilter; import android.service.notification.StatusBarNotification; +import android.service.notification.ZenPolicy; import android.app.AutomaticZenRule; import android.service.notification.ZenModeConfig; @@ -213,6 +214,7 @@ interface INotificationManager boolean isNotificationPolicyAccessGrantedForPackage(String pkg); void setNotificationPolicyAccessGranted(String pkg, boolean granted); void setNotificationPolicyAccessGrantedForUser(String pkg, int userId, boolean granted); + ZenPolicy getDefaultZenPolicy(); AutomaticZenRule getAutomaticZenRule(String id); Map<String, AutomaticZenRule> getAutomaticZenRules(); // TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined. diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 0b6e24cf5545..366b45badcfd 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -1750,6 +1750,20 @@ public class NotificationManager { @NonNull ComponentName listener, boolean granted) { setNotificationListenerAccessGranted(listener, granted, true); } + /** + * Gets the device-default notification policy as a ZenPolicy. + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public @NonNull ZenPolicy getDefaultZenPolicy() { + INotificationManager service = getService(); + try { + return service.getDefaultZenPolicy(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, the diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 63f37f150d33..36b03c1b1f48 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -261,6 +261,13 @@ public class WallpaperManager { public static final String COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep"; /** + * Command for {@link #sendWallpaperCommand}: reported when a physical display switch event + * happens, e.g. fold and unfold. + * @hide + */ + public static final String COMMAND_DISPLAY_SWITCH = "android.wallpaper.displayswitch"; + + /** * Command for {@link #sendWallpaperCommand}: reported when the wallpaper that was already * set is re-applied by the user. * @hide diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 35ce10223aa6..b3ecd92c56c9 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -55,3 +55,10 @@ flag { description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped." bug: "293441361" } + +flag { + name: "default_sms_personal_app_suspension_fix_enabled" + namespace: "enterprise" + description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended" + bug: "309183330" +} diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig new file mode 100644 index 000000000000..029b93ab4534 --- /dev/null +++ b/core/java/android/app/background_install_control_manager.aconfig @@ -0,0 +1,9 @@ +package: "android.app" + +flag { + namespace: "background_install_control" + name: "bic_client" + description: "System API for background install control." + is_fixed_read_only: true + bug: "287507984" +} diff --git a/core/java/android/app/backup/BackupHelperWithLogger.java b/core/java/android/app/backup/BackupHelperWithLogger.java new file mode 100644 index 000000000000..1a59a5302f07 --- /dev/null +++ b/core/java/android/app/backup/BackupHelperWithLogger.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.os.ParcelFileDescriptor; + +/** + * Utility class for writing BackupHelpers with added logging capabilities. + * Used for passing a logger object to Helper in key shared backup agents + * + * @hide + */ +public abstract class BackupHelperWithLogger implements BackupHelper { + private BackupRestoreEventLogger mLogger; + private boolean mIsLoggerSet = false; + + public abstract void writeNewStateDescription(ParcelFileDescriptor newState); + + public abstract void restoreEntity(BackupDataInputStream data); + + public abstract void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState); + + /** + * Gets the logger so that the backuphelper can log success/error for each datatype handled + */ + public BackupRestoreEventLogger getLogger() { + return mLogger; + } + + /** + * Allow the shared backup agent to pass a logger to each of its backup helper + */ + public void setLogger(BackupRestoreEventLogger logger) { + mLogger = logger; + mIsLoggerSet = true; + } + + /** + * Allow the helper to check if its shared backup agent has passed a logger + */ + public boolean isLoggerSet() { + return mIsLoggerSet; + } +} diff --git a/core/java/android/app/backup/BlobBackupHelper.java b/core/java/android/app/backup/BlobBackupHelper.java index 82d0a94ce0da..a55ff4899296 100644 --- a/core/java/android/app/backup/BlobBackupHelper.java +++ b/core/java/android/app/backup/BlobBackupHelper.java @@ -39,7 +39,7 @@ import java.util.zip.InflaterInputStream; * * @hide */ -public abstract class BlobBackupHelper implements BackupHelper { +public abstract class BlobBackupHelper extends BackupHelperWithLogger { private static final String TAG = "BlobBackupHelper"; private static final boolean DEBUG = false; diff --git a/core/java/android/content/pm/ArchivedActivityInfo.java b/core/java/android/content/pm/ArchivedActivityInfo.java index 1faa4373d88f..166d26503625 100644 --- a/core/java/android/content/pm/ArchivedActivityInfo.java +++ b/core/java/android/content/pm/ArchivedActivityInfo.java @@ -91,26 +91,31 @@ public final class ArchivedActivityInfo { * @hide */ public static Bitmap drawableToBitmap(Drawable drawable, int iconSize) { - if (drawable instanceof BitmapDrawable) { - return ((BitmapDrawable) drawable).getBitmap(); - - } - Bitmap bitmap; - if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { - // Needed for drawables that are just a single color. - bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + if (drawable instanceof BitmapDrawable) { + bitmap = ((BitmapDrawable) drawable).getBitmap(); } else { - bitmap = + if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { + // Needed for drawables that are just a single color. + bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + } else { + bitmap = Bitmap.createBitmap( - drawable.getIntrinsicWidth(), - drawable.getIntrinsicHeight(), - Bitmap.Config.ARGB_8888); + drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); } - Canvas canvas = new Canvas(bitmap); - drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - drawable.draw(canvas); - if (iconSize > 0 && bitmap.getWidth() > iconSize * 2 || bitmap.getHeight() > iconSize * 2) { + if (iconSize <= 0) { + return bitmap; + } + + if (bitmap.getWidth() < iconSize || bitmap.getHeight() < iconSize + || bitmap.getWidth() > iconSize * 2 || bitmap.getHeight() > iconSize * 2) { var scaledBitmap = Bitmap.createScaledBitmap(bitmap, iconSize, iconSize, true); if (scaledBitmap != bitmap) { bitmap.recycle(); @@ -235,7 +240,7 @@ public final class ArchivedActivityInfo { } @DataClass.Generated( - time = 1698789991876L, + time = 1705615445673L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/ArchivedActivityInfo.java", inputSignatures = "private @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate @android.annotation.NonNull android.content.ComponentName mComponentName\nprivate @android.annotation.Nullable android.graphics.drawable.Drawable mIcon\nprivate @android.annotation.Nullable android.graphics.drawable.Drawable mMonochromeIcon\n @android.annotation.NonNull android.content.pm.ArchivedActivityParcel getParcel()\npublic static android.graphics.Bitmap drawableToBitmap(android.graphics.drawable.Drawable)\npublic static android.graphics.Bitmap drawableToBitmap(android.graphics.drawable.Drawable,int)\npublic static byte[] bytesFromBitmap(android.graphics.Bitmap)\nprivate static android.graphics.drawable.Drawable drawableFromCompressedBitmap(byte[])\nclass ArchivedActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genBuilder=false, genConstructor=false, genSetters=true)") diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index f0efed97d8ed..c4bf18d70242 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -738,7 +738,7 @@ public class PackageInstaller { /** * The set of error types that can be set for - * {@link #reportUnarchivalStatus(int, int, PendingIntent)}. + * {@link #reportUnarchivalState}. * * @hide */ @@ -2421,6 +2421,7 @@ public class PackageInstaller { * facilitate the unarchival flow (e.g. user needs to log in). * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists */ + // TODO(b/314960798) Remove old API once it's unused @RequiresPermission(anyOf = { Manifest.permission.INSTALL_PACKAGES, Manifest.permission.REQUEST_INSTALL_PACKAGES}) @@ -2438,6 +2439,30 @@ public class PackageInstaller { } } + /** + * Reports the state of an unarchival to the system. + * + * @see UnarchivalState for the different state options. + * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists + */ + @RequiresPermission(anyOf = { + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.REQUEST_INSTALL_PACKAGES}) + @FlaggedApi(Flags.FLAG_ARCHIVING) + public void reportUnarchivalState(@NonNull UnarchivalState unarchivalState) + throws PackageManager.NameNotFoundException { + Objects.requireNonNull(unarchivalState); + try { + mInstaller.reportUnarchivalStatus(unarchivalState.getUnarchiveId(), + unarchivalState.getStatus(), unarchivalState.getRequiredStorageBytes(), + unarchivalState.getUserActionIntent(), new UserHandle(mUserId)); + } catch (ParcelableException e) { + e.maybeRethrow(PackageManager.NameNotFoundException.class); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + // (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 @@ -2693,6 +2718,8 @@ public class PackageInstaller { /** @hide */ public long rollbackLifetimeMillis = 0; /** {@hide} */ + public int rollbackImpactLevel = PackageManager.ROLLBACK_USER_IMPACT_LOW; + /** {@hide} */ public boolean forceQueryableOverride; /** {@hide} */ public int requireUserAction = USER_ACTION_UNSPECIFIED; @@ -2749,6 +2776,7 @@ public class PackageInstaller { } rollbackDataPolicy = source.readInt(); rollbackLifetimeMillis = source.readLong(); + rollbackImpactLevel = source.readInt(); requireUserAction = source.readInt(); packageSource = source.readInt(); applicationEnabledSettingPersistent = source.readBoolean(); @@ -2783,6 +2811,7 @@ public class PackageInstaller { ret.dataLoaderParams = dataLoaderParams; ret.rollbackDataPolicy = rollbackDataPolicy; ret.rollbackLifetimeMillis = rollbackLifetimeMillis; + ret.rollbackImpactLevel = rollbackImpactLevel; ret.requireUserAction = requireUserAction; ret.packageSource = packageSource; ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent; @@ -3121,6 +3150,28 @@ public class PackageInstaller { } /** + * rollbackImpactLevel is a measure of impact a rollback has on the user. This can take one + * of 3 values: + * <ul> + * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_LOW} (default)</li> + * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_HIGH} (1)</li> + * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_ONLY_MANUAL} (2)</li> + * </ul> + * + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) + @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION) + public void setRollbackImpactLevel(@PackageManager.RollbackImpactLevel int impactLevel) { + if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) { + throw new IllegalArgumentException( + "Can't set rollbackImpactLevel when rollback is not enabled"); + } + rollbackImpactLevel = impactLevel; + } + + /** * @deprecated use {@link #setRequestDowngrade(boolean)}. * {@hide} */ @@ -3493,6 +3544,7 @@ public class PackageInstaller { pw.printPair("dataLoaderParams", dataLoaderParams); pw.printPair("rollbackDataPolicy", rollbackDataPolicy); pw.printPair("rollbackLifetimeMillis", rollbackLifetimeMillis); + pw.printPair("rollbackImpactLevel", rollbackImpactLevel); pw.printPair("applicationEnabledSettingPersistent", applicationEnabledSettingPersistent); pw.printHexPair("developmentInstallFlags", developmentInstallFlags); @@ -3536,6 +3588,7 @@ public class PackageInstaller { } dest.writeInt(rollbackDataPolicy); dest.writeLong(rollbackLifetimeMillis); + dest.writeInt(rollbackImpactLevel); dest.writeInt(requireUserAction); dest.writeInt(packageSource); dest.writeBoolean(applicationEnabledSettingPersistent); @@ -3734,6 +3787,9 @@ public class PackageInstaller { public long rollbackLifetimeMillis; /** {@hide} */ + public int rollbackImpactLevel; + + /** {@hide} */ public int requireUserAction; /** {@hide} */ @@ -3801,6 +3857,7 @@ public class PackageInstaller { isPreapprovalRequested = source.readBoolean(); rollbackDataPolicy = source.readInt(); rollbackLifetimeMillis = source.readLong(); + rollbackImpactLevel = source.readInt(); createdMillis = source.readLong(); requireUserAction = source.readInt(); installerUid = source.readInt(); @@ -4438,6 +4495,7 @@ public class PackageInstaller { dest.writeBoolean(isPreapprovalRequested); dest.writeInt(rollbackDataPolicy); dest.writeLong(rollbackLifetimeMillis); + dest.writeInt(rollbackImpactLevel); dest.writeLong(createdMillis); dest.writeInt(requireUserAction); dest.writeInt(installerUid); @@ -4708,10 +4766,10 @@ public class PackageInstaller { codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java", inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<android.content.pm.PackageInstaller.PreapprovalDetails> CREATOR\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\npublic @java.lang.Override int describeContents()\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\nprivate @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate @android.annotation.NonNull java.lang.String mPackageName\nprivate long mBuilderFieldsSet\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setIcon(android.graphics.Bitmap)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(java.lang.CharSequence)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLocale(android.icu.util.ULocale)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setPackageName(java.lang.String)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails build()\nprivate void checkNotUsed()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true)") + @Deprecated private void __metadata() {} - //@formatter:on // End of generated code @@ -5102,13 +5160,188 @@ public class PackageInstaller { codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java", inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mDeviceIdleRequired\nprivate final boolean mAppNotForegroundRequired\nprivate final boolean mAppNotInteractingRequired\nprivate final boolean mAppNotTopVisibleRequired\nprivate final boolean mNotInCallRequired\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mDeviceIdleRequired\nprivate boolean mAppNotForegroundRequired\nprivate boolean mAppNotInteractingRequired\nprivate boolean mAppNotTopVisibleRequired\nprivate boolean mNotInCallRequired\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setDeviceIdleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotForegroundRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotInteractingRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotTopVisibleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setNotInCallRequired()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)") + @Deprecated private void __metadata() {} - //@formatter:on // End of generated code } + /** + * Used to communicate the unarchival state in {@link #reportUnarchivalState}. + */ + @FlaggedApi(Flags.FLAG_ARCHIVING) + public static final class UnarchivalState { + + /** + * The caller is able to facilitate the unarchival for the given {@code unarchiveId}. + * + * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + */ + @NonNull + public static UnarchivalState createOkState(int unarchiveId) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_OK, /* requiredStorageBytes= */ -1, + /* userActionIntent= */ null); + } + + /** + * User action is required before commencing with the unarchival for the given + * {@code unarchiveId}. E.g., this could be used if it's necessary for the user to sign-in + * first. + * + * @param unarchiveId the ID provided by the system as part of the + * intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + * @param userActionIntent optional intent to start a follow up action required to + * facilitate the unarchival flow (e.g. user needs to log in). + */ + @NonNull + public static UnarchivalState createUserActionRequiredState(int unarchiveId, + @NonNull PendingIntent userActionIntent) { + Objects.requireNonNull(userActionIntent); + return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_USER_ACTION_NEEDED, + /* requiredStorageBytes= */ -1, userActionIntent); + } + + /** + * There is not enough storage to start the unarchival for the given {@code unarchiveId}. + * + * @param unarchiveId the ID provided by the system as part of the + * intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + * @param requiredStorageBytes ff the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this + * field should be set to specify how many additional bytes of + * storage are required to unarchive the app. + * @param userActionIntent can optionally be set to provide a custom storage-clearing + * action. + */ + @NonNull + public static UnarchivalState createInsufficientStorageState(int unarchiveId, + long requiredStorageBytes, @Nullable PendingIntent userActionIntent) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE, + requiredStorageBytes, userActionIntent); + } + + /** + * The device has no data connectivity and unarchival cannot be started for the given + * {@code unarchiveId}. + * + * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + */ + @NonNull + public static UnarchivalState createNoConnectivityState(int unarchiveId) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_NO_CONNECTIVITY, + /* requiredStorageBytes= */ -1,/* userActionIntent= */ null); + } + + /** + * Generic error state for all cases that are not covered by other methods in this class. + * + * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + */ + @NonNull + public static UnarchivalState createGenericErrorState(int unarchiveId) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_GENERIC_ERROR, + /* requiredStorageBytes= */ -1,/* userActionIntent= */ null); + } + + + /** + * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with + * EXTRA_UNARCHIVE_ID. + */ + private final int mUnarchiveId; + + /** Used for the system to provide the user with necessary follow-up steps or errors. */ + @UnarchivalStatus + private final int mStatus; + + /** + * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify + * how many additional bytes of storage are required to unarchive the app. + */ + private final long mRequiredStorageBytes; + + /** + * Optional intent to start a follow up action required to facilitate the unarchival flow + * (e.g., user needs to log in). + */ + @Nullable + private final PendingIntent mUserActionIntent; + + /** + * Creates a new UnarchivalState. + * + * @param unarchiveId The ID provided by the system as part of the + * intent.action.UNARCHIVE broadcast with + * EXTRA_UNARCHIVE_ID. + * @param status Used for the system to provide the user with necessary + * follow-up steps or errors. + * @param requiredStorageBytes If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this + * field should be set to specify + * how many additional bytes of storage are required to + * unarchive the app. + * @param userActionIntent Optional intent to start a follow up action required to + * facilitate the unarchival flow + * (e.g,. user needs to log in). + * @hide + */ + private UnarchivalState( + int unarchiveId, + @UnarchivalStatus int status, + long requiredStorageBytes, + @Nullable PendingIntent userActionIntent) { + this.mUnarchiveId = unarchiveId; + this.mStatus = status; + com.android.internal.util.AnnotationValidations.validate( + UnarchivalStatus.class, null, mStatus); + this.mRequiredStorageBytes = requiredStorageBytes; + this.mUserActionIntent = userActionIntent; + } + + /** + * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with + * EXTRA_UNARCHIVE_ID. + * + * @hide + */ + int getUnarchiveId() { + return mUnarchiveId; + } + + /** + * Used for the system to provide the user with necessary follow-up steps or errors. + * + * @hide + */ + @UnarchivalStatus int getStatus() { + return mStatus; + } + + /** + * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify + * how many additional bytes of storage are required to unarchive the app. + * + * @hide + */ + long getRequiredStorageBytes() { + return mRequiredStorageBytes; + } + + /** + * Optional intent to start a follow up action required to facilitate the unarchival flow + * (e.g. user needs to log in). + * + * @hide + */ + @Nullable PendingIntent getUserActionIntent() { + return mUserActionIntent; + } + } + } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index aabbe698703c..4724e866f094 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1501,6 +1501,44 @@ public abstract class PackageManager { public static final int ROLLBACK_DATA_POLICY_RETAIN = 2; /** @hide */ + @IntDef(prefix = {"ROLLBACK_USER_IMPACT_"}, value = { + ROLLBACK_USER_IMPACT_LOW, + ROLLBACK_USER_IMPACT_HIGH, + ROLLBACK_USER_IMPACT_ONLY_MANUAL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RollbackImpactLevel {} + + /** + * Rollback will be performed automatically in response to native crashes on startup or + * persistent service crashes. More suitable for apps that do not store any user data. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION) + public static final int ROLLBACK_USER_IMPACT_LOW = 0; + + /** + * Rollback will be performed automatically only when the device is found to be unrecoverable. + * More suitable for apps that store user data and have higher impact on user. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION) + public static final int ROLLBACK_USER_IMPACT_HIGH = 1; + + /** + * Rollback will not be performed automatically. It can be triggered externally. + * + * @hide + */ + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION) + public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2; + + /** @hide */ @IntDef(flag = true, prefix = { "INSTALL_" }, value = { INSTALL_REPLACE_EXISTING, INSTALL_ALLOW_TEST, diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index a2cd3e153b3e..e4e9fbaf2c55 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -146,3 +146,10 @@ flag { bug: "281848623" } +flag { + name: "recoverability_detection" + namespace: "package_manager_service" + description: "Feature flag to enable recoverability detection feature. It includes GMS core rollback and improvements to rescue party." + bug: "291135724" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 7c5d3054c945..5bfc012844f8 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -77,4 +77,11 @@ flag { namespace: "profile_experiences" description: "Move the quiet mode operations, happening on a background thread today, to a separate thread." bug: "320483504" +} + +flag { + name: "enable_private_space_autolock_on_restarts" + namespace: "profile_experiences" + description: "Enable auto-locking private space on device restarts" + bug: "296993385" }
\ No newline at end of file diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java index a4db733af013..bd74b0b9293c 100644 --- a/core/java/android/content/pm/overlay/OverlayPaths.java +++ b/core/java/android/content/pm/overlay/OverlayPaths.java @@ -49,6 +49,13 @@ public class OverlayPaths { public static class Builder { final OverlayPaths mPaths = new OverlayPaths(); + public Builder() {} + + public Builder(@NonNull OverlayPaths base) { + mPaths.mResourceDirs.addAll(base.getResourceDirs()); + mPaths.mOverlayPaths.addAll(base.getOverlayPaths()); + } + /** * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}. */ diff --git a/core/java/android/content/rollback/RollbackInfo.java b/core/java/android/content/rollback/RollbackInfo.java index a363718a8b1d..d128055fec6d 100644 --- a/core/java/android/content/rollback/RollbackInfo.java +++ b/core/java/android/content/rollback/RollbackInfo.java @@ -16,8 +16,12 @@ package android.content.rollback; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.annotation.TestApi; +import android.content.pm.Flags; +import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.os.Parcel; import android.os.Parcelable; @@ -25,17 +29,14 @@ import android.os.Parcelable; import java.util.List; /** - * Information about a set of packages that can be, or already have been - * rolled back together. + * Information about a set of packages that can be, or already have been rolled back together. * * @hide */ @SystemApi public final class RollbackInfo implements Parcelable { - /** - * A unique identifier for the rollback. - */ + /** A unique identifier for the rollback. */ private final int mRollbackId; private final List<PackageRollbackInfo> mPackages; @@ -44,15 +45,39 @@ public final class RollbackInfo implements Parcelable { private final boolean mIsStaged; private int mCommittedSessionId; + private int mRollbackImpactLevel; /** @hide */ - public RollbackInfo(int rollbackId, List<PackageRollbackInfo> packages, boolean isStaged, - List<VersionedPackage> causePackages, int committedSessionId) { + public RollbackInfo( + int rollbackId, + List<PackageRollbackInfo> packages, + boolean isStaged, + List<VersionedPackage> causePackages, + int committedSessionId, + @PackageManager.RollbackImpactLevel int rollbackImpactLevel) { this.mRollbackId = rollbackId; this.mPackages = packages; this.mIsStaged = isStaged; this.mCausePackages = causePackages; this.mCommittedSessionId = committedSessionId; + this.mRollbackImpactLevel = rollbackImpactLevel; + } + + /** @hide */ + public RollbackInfo( + int rollbackId, + List<PackageRollbackInfo> packages, + boolean isStaged, + List<VersionedPackage> causePackages, + int committedSessionId) { + // If impact level is not set default to 0 + this( + rollbackId, + packages, + isStaged, + causePackages, + committedSessionId, + PackageManager.ROLLBACK_USER_IMPACT_LOW); } private RollbackInfo(Parcel in) { @@ -61,34 +86,28 @@ public final class RollbackInfo implements Parcelable { mIsStaged = in.readBoolean(); mCausePackages = in.createTypedArrayList(VersionedPackage.CREATOR); mCommittedSessionId = in.readInt(); + mRollbackImpactLevel = in.readInt(); } - /** - * Returns a unique identifier for this rollback. - */ + /** Returns a unique identifier for this rollback. */ public int getRollbackId() { return mRollbackId; } - /** - * Returns the list of package that are rolled back. - */ + /** Returns the list of package that are rolled back. */ @NonNull public List<PackageRollbackInfo> getPackages() { return mPackages; } - /** - * Returns true if this rollback requires reboot to take effect after - * being committed. - */ + /** Returns true if this rollback requires reboot to take effect after being committed. */ public boolean isStaged() { return mIsStaged; } /** - * Returns the session ID for the committed rollback for staged rollbacks. - * Only applicable for rollbacks that have been committed. + * Returns the session ID for the committed rollback for staged rollbacks. Only applicable for + * rollbacks that have been committed. */ public int getCommittedSessionId() { return mCommittedSessionId; @@ -96,6 +115,7 @@ public final class RollbackInfo implements Parcelable { /** * Sets the session ID for the committed rollback for staged rollbacks. + * * @hide */ public void setCommittedSessionId(int sessionId) { @@ -103,15 +123,40 @@ public final class RollbackInfo implements Parcelable { } /** - * Gets the list of package versions that motivated this rollback. - * As provided to {@link #commitRollback} when the rollback was committed. - * This is only applicable for rollbacks that have been committed. + * Gets the list of package versions that motivated this rollback. As provided to {@link + * #commitRollback} when the rollback was committed. This is only applicable for rollbacks that + * have been committed. */ @NonNull public List<VersionedPackage> getCausePackages() { return mCausePackages; } + /** + * Get rollback impact level. Refer {@link + * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info + * on impact level. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION) + public @PackageManager.RollbackImpactLevel int getRollbackImpactLevel() { + return mRollbackImpactLevel; + } + + /** + * Set rollback impact level. Refer {@link + * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info + * on impact level. + * + * @hide + */ + public void setRollbackImpactLevel( + @PackageManager.RollbackImpactLevel int rollbackImpactLevel) { + mRollbackImpactLevel = rollbackImpactLevel; + } + @Override public int describeContents() { return 0; @@ -124,16 +169,17 @@ public final class RollbackInfo implements Parcelable { out.writeBoolean(mIsStaged); out.writeTypedList(mCausePackages); out.writeInt(mCommittedSessionId); + out.writeInt(mRollbackImpactLevel); } public static final @android.annotation.NonNull Parcelable.Creator<RollbackInfo> CREATOR = new Parcelable.Creator<RollbackInfo>() { - public RollbackInfo createFromParcel(Parcel in) { - return new RollbackInfo(in); - } - - public RollbackInfo[] newArray(int size) { - return new RollbackInfo[size]; - } - }; + public RollbackInfo createFromParcel(Parcel in) { + return new RollbackInfo(in); + } + + public RollbackInfo[] newArray(int size) { + return new RollbackInfo[size]; + } + }; } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 8a4f678b52f2..35ae3c9e23c7 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -40,12 +40,15 @@ import android.os.Looper; import android.os.OperationCanceledException; import android.os.SystemProperties; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.EventLog; import android.util.Log; import android.util.Pair; import android.util.Printer; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import dalvik.annotation.optimization.NeverCompile; @@ -103,8 +106,14 @@ public final class SQLiteDatabase extends SQLiteClosable { // Stores reference to all databases opened in the current process. // (The referent Object is not used at this time.) // INVARIANT: Guarded by sActiveDatabases. + @GuardedBy("sActiveDatabases") private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = new WeakHashMap<>(); + // Tracks which database files are currently open. If a database file is opened more than + // once at any given moment, the associated databases are marked as "concurrent". + @GuardedBy("sActiveDatabases") + private static final OpenTracker sOpenTracker = new OpenTracker(); + // Thread-local for database sessions that belong to this database. // Each thread has its own database session. // INVARIANT: Immutable. @@ -510,6 +519,7 @@ public final class SQLiteDatabase extends SQLiteClosable { private void dispose(boolean finalized) { final SQLiteConnectionPool pool; + final String path; synchronized (mLock) { if (mCloseGuardLocked != null) { if (finalized) { @@ -520,10 +530,12 @@ public final class SQLiteDatabase extends SQLiteClosable { pool = mConnectionPoolLocked; mConnectionPoolLocked = null; + path = isInMemoryDatabase() ? null : getPath(); } if (!finalized) { synchronized (sActiveDatabases) { + sOpenTracker.close(path); sActiveDatabases.remove(this); } @@ -1132,6 +1144,74 @@ public final class SQLiteDatabase extends SQLiteClosable { } } + /** + * Track the number of times a database file has been opened. There is a primary connection + * associated with every open database, and these can contend with each other, leading to + * unexpected SQLiteDatabaseLockedException exceptions. The tracking here is only advisory: + * multiply-opened databases are logged but no other action is taken. + * + * This class is not thread-safe. + */ + private static class OpenTracker { + // The list of currently-open databases. This maps the database file to the number of + // currently-active opens. + private final ArrayMap<String, Integer> mOpens = new ArrayMap<>(); + + // The maximum number of concurrently open database paths that will be stored. Once this + // many paths have been recorded, further paths are logged but not saved. + private static final int MAX_RECORDED_PATHS = 20; + + // The list of databases that were ever concurrently opened. + private final ArraySet<String> mConcurrent = new ArraySet<>(); + + /** Return the canonical path. On error, just return the input path. */ + private static String normalize(String path) { + try { + return new File(path).toPath().toRealPath().toString(); + } catch (Exception e) { + // If there is an IO or security exception, just continue, using the input path. + return path; + } + } + + /** Return true if the path is currently open in another SQLiteDatabase instance. */ + void open(@Nullable String path) { + if (path == null) return; + path = normalize(path); + + Integer count = mOpens.get(path); + if (count == null || count == 0) { + mOpens.put(path, 1); + return; + } else { + mOpens.put(path, count + 1); + if (mConcurrent.size() < MAX_RECORDED_PATHS) { + mConcurrent.add(path); + } + Log.w(TAG, "multiple primary connections on " + path); + return; + } + } + + void close(@Nullable String path) { + if (path == null) return; + path = normalize(path); + Integer count = mOpens.get(path); + if (count == null || count <= 0) { + Log.e(TAG, "open database counting failure on " + path); + } else if (count == 1) { + // Implicitly set the count to zero, and make mOpens smaller. + mOpens.remove(path); + } else { + mOpens.put(path, count - 1); + } + } + + ArraySet<String> getConcurrentDatabasePaths() { + return new ArraySet<>(mConcurrent); + } + } + private void open() { try { try { @@ -1153,14 +1233,17 @@ public final class SQLiteDatabase extends SQLiteClosable { } private void openInner() { + final String path; synchronized (mLock) { assert mConnectionPoolLocked == null; mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked); mCloseGuardLocked.open("close"); + path = isInMemoryDatabase() ? null : getPath(); } synchronized (sActiveDatabases) { sActiveDatabases.put(this, null); + sOpenTracker.open(path); } } @@ -2345,6 +2428,17 @@ public final class SQLiteDatabase extends SQLiteClosable { } /** + * Return list of databases that have been concurrently opened. + * @hide + */ + @VisibleForTesting + public static ArraySet<String> getConcurrentDatabasePaths() { + synchronized (sActiveDatabases) { + return sOpenTracker.getConcurrentDatabasePaths(); + } + } + + /** * Returns true if the new version code is greater than the current database version. * * @param newVersion The new version code. @@ -2766,6 +2860,19 @@ public final class SQLiteDatabase extends SQLiteClosable { dumpDatabaseDirectory(printer, new File(dir), isSystem); } } + + // Dump concurrently-opened database files, if any + final ArraySet<String> concurrent; + synchronized (sActiveDatabases) { + concurrent = sOpenTracker.getConcurrentDatabasePaths(); + } + if (concurrent.size() > 0) { + printer.println(""); + printer.println("Concurrently opened database files"); + for (String f : concurrent) { + printer.println(" " + f); + } + } } private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) { diff --git a/core/java/android/os/VintfObject.java b/core/java/android/os/VintfObject.java index 4fc5131617b2..505655775239 100644 --- a/core/java/android/os/VintfObject.java +++ b/core/java/android/os/VintfObject.java @@ -31,6 +31,10 @@ public class VintfObject { private static final String LOG_TAG = "VintfObject"; + static { + System.loadLibrary("vintf_jni"); + } + /** * Slurps all device information (both manifests and both matrices) * and report them. diff --git a/core/java/android/os/VintfRuntimeInfo.java b/core/java/android/os/VintfRuntimeInfo.java index f17039ba9bf4..e729063d6763 100644 --- a/core/java/android/os/VintfRuntimeInfo.java +++ b/core/java/android/os/VintfRuntimeInfo.java @@ -28,6 +28,10 @@ public class VintfRuntimeInfo { private VintfRuntimeInfo() {} + static { + System.loadLibrary("vintf_jni"); + } + /** * @return /sys/fs/selinux/policyvers, via security_policyvers() native call * diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 39b6aeb814f6..8c7050176506 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -86,3 +86,11 @@ flag { description: "This flag is used to enabled the Wallet Role for all users on the device" bug: "283989236" } + +flag { + name: "signature_permission_allowlist_enabled" + is_fixed_read_only: true + namespace: "permissions" + description: "Enable signature permission allowlist" + bug: "308573169" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ab98c94cba73..d946430f18af 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3243,9 +3243,17 @@ public final class Settings { private static final String NAME_EQ_PLACEHOLDER = "name=?"; + // Cached values of queried settings. + // Key is the setting's name, value is the setting's value. // Must synchronize on 'this' to access mValues and mValuesVersion. private final ArrayMap<String, String> mValues = new ArrayMap<>(); + // Cached values for queried prefixes. + // Key is the prefix, value is all of the settings under the prefix, mapped from a setting's + // name to a setting's value. The name string doesn't include the prefix. + // Must synchronize on 'this' to access. + private final ArrayMap<String, ArrayMap<String, String>> mPrefixToValues = new ArrayMap<>(); + private final Uri mUri; @UnsupportedAppUsage private final ContentProviderHolder mProviderHolder; @@ -3592,15 +3600,13 @@ public final class Settings { || applicationInfo.isSignedWithPlatformKey(); } - private ArrayMap<String, String> getStringsForPrefixStripPrefix( - ContentResolver cr, String prefix, String[] names) { + private Map<String, String> getStringsForPrefixStripPrefix( + ContentResolver cr, String prefix, List<String> names) { String namespace = prefix.substring(0, prefix.length() - 1); ArrayMap<String, String> keyValues = new ArrayMap<>(); int substringLength = prefix.length(); - int currentGeneration = -1; boolean needsGenerationTracker = false; - synchronized (NameValueCache.this) { final GenerationTracker generationTracker = mGenerationTrackers.get(prefix); if (generationTracker != null) { @@ -3614,40 +3620,24 @@ public final class Settings { // generation tracker and request a new one generationTracker.destroy(); mGenerationTrackers.remove(prefix); - for (int i = mValues.size() - 1; i >= 0; i--) { - String key = mValues.keyAt(i); - if (key.startsWith(prefix)) { - mValues.remove(key); - } - } + mPrefixToValues.remove(prefix); needsGenerationTracker = true; } else { - boolean prefixCached = mValues.containsKey(prefix); - if (prefixCached) { - if (DEBUG) { - Log.i(TAG, "Cache hit for prefix:" + prefix); - } - if (names.length > 0) { + final ArrayMap<String, String> cachedSettings = mPrefixToValues.get(prefix); + if (cachedSettings != null) { + if (!names.isEmpty()) { for (String name : names) { - // mValues can contain "null" values, need to use containsKey. - if (mValues.containsKey(name)) { + // The cache can contain "null" values, need to use containsKey. + if (cachedSettings.containsKey(name)) { keyValues.put( - name.substring(substringLength), - mValues.get(name)); + name, + cachedSettings.get(name)); } } } else { - for (int i = 0; i < mValues.size(); ++i) { - String key = mValues.keyAt(i); - // Explicitly exclude the prefix as it is only there to - // signal that the prefix has been cached. - if (key.startsWith(prefix) && !key.equals(prefix)) { - String value = mValues.valueAt(i); - keyValues.put( - key.substring(substringLength), - value); - } - } + keyValues.putAll(cachedSettings); + // Remove the hack added for the legacy behavior. + keyValues.remove(""); } return keyValues; } @@ -3657,7 +3647,6 @@ public final class Settings { needsGenerationTracker = true; } } - if (mCallListCommand == null) { // No list command specified, return empty map return keyValues; @@ -3702,20 +3691,23 @@ public final class Settings { } // All flags for the namespace - Map<String, String> flagsToValues = + HashMap<String, String> flagsToValues = (HashMap) b.getSerializable(Settings.NameValueTable.VALUE, java.util.HashMap.class); + if (flagsToValues == null) { + return keyValues; + } // Only the flags requested by the caller - if (names.length > 0) { + if (!names.isEmpty()) { for (String name : names) { // flagsToValues can contain "null" values, need to use containsKey. - if (flagsToValues.containsKey(name)) { + final String key = Config.createCompositeName(namespace, name); + if (flagsToValues.containsKey(key)) { keyValues.put( - name.substring(substringLength), - flagsToValues.get(name)); + name, + flagsToValues.get(key)); } } } else { - keyValues.ensureCapacity(keyValues.size() + flagsToValues.size()); for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { keyValues.put( flag.getKey().substring(substringLength), @@ -3751,10 +3743,18 @@ public final class Settings { if (DEBUG) { Log.i(TAG, "Updating cache for prefix:" + prefix); } - // cache the complete list of flags for the namespace - mValues.putAll(flagsToValues); - // Adding the prefix as a signal that the prefix is cached. - mValues.put(prefix, null); + // Cache the complete list of flags for the namespace for bulk queries. + // In this cached list, the setting's name doesn't include the prefix. + ArrayMap<String, String> namesToValues = + new ArrayMap<>(flagsToValues.size() + 1); + for (Map.Entry<String, String> flag : flagsToValues.entrySet()) { + namesToValues.put( + flag.getKey().substring(substringLength), + flag.getValue()); + } + // Legacy behavior, we return <"", null> when queried with name = "" + namesToValues.put("", null); + mPrefixToValues.put(prefix, namesToValues); } } return keyValues; @@ -19945,16 +19945,9 @@ public final class Settings { @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) public static Map<String, String> getStrings(@NonNull ContentResolver resolver, @NonNull String namespace, @NonNull List<String> names) { - String[] compositeNames = new String[names.size()]; - for (int i = 0, size = names.size(); i < size; ++i) { - compositeNames[i] = createCompositeName(namespace, names.get(i)); - } - String prefix = createPrefix(namespace); - ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix( - resolver, prefix, compositeNames); - return keyValues; + return sNameValueCache.getStringsForPrefixStripPrefix(resolver, prefix, names); } /** @@ -20276,7 +20269,7 @@ public final class Settings { } } - private static String createCompositeName(@NonNull String namespace, @NonNull String name) { + static String createCompositeName(@NonNull String namespace, @NonNull String name) { Preconditions.checkNotNull(namespace); Preconditions.checkNotNull(name); var sb = new StringBuilder(namespace.length() + 1 + name.length()); diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index c479877fe98e..9895551a8672 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -185,7 +185,13 @@ public class ZenModeConfig implements Parcelable { SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT | SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT; - public static final int XML_VERSION = 8; + // ZenModeConfig XML versions distinguishing key changes. + public static final int XML_VERSION_ZEN_UPGRADE = 8; + public static final int XML_VERSION_MODES_API = 11; + + // TODO: b/310620812 - Update XML_VERSION and update default_zen_config.xml accordingly when + // modes_api is inlined. + private static final int XML_VERSION = 10; public static final String ZEN_TAG = "zen"; private static final String ZEN_ATT_VERSION = "version"; private static final String ZEN_ATT_USER = "user"; @@ -586,6 +592,10 @@ public class ZenModeConfig implements Parcelable { } } + public static int getCurrentXmlVersion() { + return Flags.modesApi() ? XML_VERSION_MODES_API : XML_VERSION; + } + public static ZenModeConfig readXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { int type = parser.getEventType(); @@ -593,7 +603,7 @@ public class ZenModeConfig implements Parcelable { String tag = parser.getName(); if (!ZEN_TAG.equals(tag)) return null; final ZenModeConfig rt = new ZenModeConfig(); - rt.version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION); + rt.version = safeInt(parser, ZEN_ATT_VERSION, getCurrentXmlVersion()); rt.user = safeInt(parser, ZEN_ATT_USER, rt.user); boolean readSuppressedEffects = false; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { @@ -707,14 +717,16 @@ public class ZenModeConfig implements Parcelable { /** * Writes XML of current ZenModeConfig * @param out serializer - * @param version uses XML_VERSION if version is null + * @param version uses the current XML version if version is null * @throws IOException */ + public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup) throws IOException { + int xmlVersion = getCurrentXmlVersion(); out.startTag(null, ZEN_TAG); out.attribute(null, ZEN_ATT_VERSION, version == null - ? Integer.toString(XML_VERSION) : Integer.toString(version)); + ? Integer.toString(xmlVersion) : Integer.toString(version)); out.attributeInt(null, ZEN_ATT_USER, user); out.startTag(null, ALLOW_TAG); out.attributeBoolean(null, ALLOW_ATT_CALLS, allowCalls); diff --git a/core/java/android/service/notification/ZenPolicy.aidl b/core/java/android/service/notification/ZenPolicy.aidl new file mode 100644 index 000000000000..b56f5c6989bf --- /dev/null +++ b/core/java/android/service/notification/ZenPolicy.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.notification; + +parcelable ZenPolicy;
\ No newline at end of file diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index fb491d010f54..d8318a6bee7c 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -1422,6 +1422,54 @@ public final class ZenPolicy implements Parcelable { } /** + * Overwrites any policy values in this ZenPolicy with set values from newPolicy and + * returns a copy of the resulting ZenPolicy. + * Unlike apply(), values set in newPolicy will always be kept over pre-existing + * fields. Any values in newPolicy that are not set keep their currently set values. + * + * @hide + */ + @TestApi + @FlaggedApi(Flags.FLAG_MODES_API) + public @NonNull ZenPolicy overwrittenWith(@Nullable ZenPolicy newPolicy) { + ZenPolicy result = this.copy(); + + if (newPolicy == null) { + return result; + } + + // set priority categories + for (int category = 0; category < mPriorityCategories.size(); category++) { + @State int newState = newPolicy.mPriorityCategories.get(category); + if (newState != STATE_UNSET) { + result.mPriorityCategories.set(category, newState); + + if (category == PRIORITY_CATEGORY_MESSAGES) { + result.mPriorityMessages = newPolicy.mPriorityMessages; + } else if (category == PRIORITY_CATEGORY_CALLS) { + result.mPriorityCalls = newPolicy.mPriorityCalls; + } else if (category == PRIORITY_CATEGORY_CONVERSATIONS) { + result.mConversationSenders = newPolicy.mConversationSenders; + } + } + } + + // set visual effects + for (int visualEffect = 0; visualEffect < mVisualEffects.size(); visualEffect++) { + if (newPolicy.mVisualEffects.get(visualEffect) != STATE_UNSET) { + result.mVisualEffects.set(visualEffect, newPolicy.mVisualEffects.get(visualEffect)); + } + } + + // set allowed channels + if (newPolicy.mAllowChannels != CHANNEL_POLICY_UNSET) { + result.mAllowChannels = newPolicy.mAllowChannels; + } + + return result; + } + + /** * @hide */ public void dumpDebug(ProtoOutputStream proto, long fieldId) { diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 1a2be15b2e8d..76e0c259b38f 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -16,6 +16,7 @@ package android.service.wallpaper; +import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WallpaperManager.SetWallpaperFlags; @@ -153,6 +154,7 @@ public abstract class WallpaperService extends Service { static final boolean DEBUG = false; static final float MIN_PAGE_ALLOWED_MARGIN = .05f; private static final int MIN_BITMAP_SCREENSHOT_WIDTH = 64; + private static final long PRESERVE_VISIBLE_TIMEOUT_MS = 1000; private static final long DEFAULT_UPDATE_SCREENSHOT_DURATION = 60 * 1000; //Once per minute private static final @NonNull RectF LOCAL_COLOR_BOUNDS = new RectF(0, 0, 1, 1); @@ -165,6 +167,7 @@ public abstract class WallpaperService extends Service { private static final int MSG_UPDATE_SURFACE = 10000; private static final int MSG_VISIBILITY_CHANGED = 10010; + private static final int MSG_REFRESH_VISIBILITY = 10011; private static final int MSG_WALLPAPER_OFFSETS = 10020; private static final int MSG_WALLPAPER_COMMAND = 10025; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -248,6 +251,11 @@ public abstract class WallpaperService extends Service { */ private boolean mIsScreenTurningOn; boolean mReportedVisible; + /** + * This is used with {@link #PRESERVE_VISIBLE_TIMEOUT_MS} to avoid intermediate visibility + * changes if the display may be toggled in a short time, e.g. display switch. + */ + boolean mPreserveVisible; boolean mDestroyed; // Set to true after receiving WallpaperManager#COMMAND_FREEZE. It's reset back to false // after receiving WallpaperManager#COMMAND_UNFREEZE. COMMAND_FREEZE is fully applied once @@ -1084,6 +1092,9 @@ public abstract class WallpaperService extends Service { if (pendingCount != 0) { out.print(prefix); out.print("mPendingResizeCount="); out.println(pendingCount); } + if (mPreserveVisible) { + out.print(prefix); out.print("mPreserveVisible=true"); + } synchronized (mLock) { out.print(prefix); out.print("mPendingXOffset="); out.print(mPendingXOffset); out.print(" mPendingXOffset="); out.println(mPendingXOffset); @@ -1643,7 +1654,8 @@ public abstract class WallpaperService extends Service { ? false : mIWallpaperEngine.mInfo.supportsAmbientMode(); // Report visibility only if display is fully on or wallpaper supports ambient mode. - boolean visible = mVisible && (displayFullyOn || supportsAmbientMode); + final boolean visible = (mVisible && (displayFullyOn || supportsAmbientMode)) + || mPreserveVisible; if (DEBUG) { Log.v( TAG, @@ -2080,6 +2092,9 @@ public abstract class WallpaperService extends Service { if (!mDestroyed) { if (COMMAND_FREEZE.equals(cmd.action) || COMMAND_UNFREEZE.equals(cmd.action)) { updateFrozenState(/* frozenRequested= */ !COMMAND_UNFREEZE.equals(cmd.action)); + } else if (COMMAND_DISPLAY_SWITCH.equals(cmd.action)) { + handleDisplaySwitch(cmd.z == 1 /* startToSwitch */); + return; } result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z, cmd.extras, cmd.sync); @@ -2095,6 +2110,23 @@ public abstract class WallpaperService extends Service { } } + private void handleDisplaySwitch(boolean startToSwitch) { + if (startToSwitch && mReportedVisible) { + // The display may be off/on in a short time when the display is switching. + // Keep the visible state until onScreenTurnedOn or !startToSwitch is received, so + // the rendering thread can be active to redraw in time when receiving size change. + mPreserveVisible = true; + mCaller.removeMessages(MSG_REFRESH_VISIBILITY); + mCaller.sendMessageDelayed(mCaller.obtainMessage(MSG_REFRESH_VISIBILITY), + PRESERVE_VISIBLE_TIMEOUT_MS); + } else if (!startToSwitch && mPreserveVisible) { + // The switch is finished, so restore to actual visibility. + mPreserveVisible = false; + mCaller.removeMessages(MSG_REFRESH_VISIBILITY); + reportVisibility(false /* forceReport */); + } + } + private void updateFrozenState(boolean frozenRequested) { if (mIWallpaperEngine.mInfo == null // Procees the unfreeze command in case the wallaper became static while @@ -2638,6 +2670,10 @@ public abstract class WallpaperService extends Service { + ": " + message.arg1); mEngine.doVisibilityChanged(message.arg1 != 0); break; + case MSG_REFRESH_VISIBILITY: + mEngine.mPreserveVisible = false; + mEngine.reportVisibility(false /* forceReport */); + break; case MSG_UPDATE_SCREEN_TURNING_ON: if (DEBUG) { Log.v(TAG, diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 8935ab38451a..0a813a3edb1f 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -38,6 +38,7 @@ import android.telephony.Annotation.PreciseDisconnectCauses; import android.telephony.Annotation.RadioPowerState; import android.telephony.Annotation.SimActivationState; import android.telephony.Annotation.SrvccState; +import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; import android.telephony.TelephonyManager.CarrierPrivilegesCallback; import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsCallSession; @@ -116,14 +117,15 @@ public class TelephonyRegistryManager { } /** - * Register for changes to the list of active {@link SubscriptionInfo} records or to the - * individual records themselves. When a change occurs the onSubscriptionsChanged method of - * the listener will be invoked immediately if there has been a notification. The - * onSubscriptionChanged method will also be triggered once initially when calling this - * function. + * Register for changes to the list of {@link SubscriptionInfo} records or to the + * individual records (active or inactive) themselves. When a change occurs, the + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of + * the listener will be invoked immediately. The + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked + * once initially when calling this method. * - * @param listener an instance of {@link SubscriptionManager.OnSubscriptionsChangedListener} - * with onSubscriptionsChanged overridden. + * @param listener an instance of {@link OnSubscriptionsChangedListener} with + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden. * @param executor the executor that will execute callbacks. * @hide */ diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index eca848ae1ea3..1ea80f146b0b 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -152,7 +152,8 @@ public abstract class Layout { /** @hide */ @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = { LineBreaker.JUSTIFICATION_MODE_NONE, - LineBreaker.JUSTIFICATION_MODE_INTER_WORD + LineBreaker.JUSTIFICATION_MODE_INTER_WORD, + LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER, }) @Retention(RetentionPolicy.SOURCE) public @interface JustificationMode {} @@ -168,6 +169,13 @@ public abstract class Layout { public static final int JUSTIFICATION_MODE_INTER_WORD = LineBreaker.JUSTIFICATION_MODE_INTER_WORD; + /** + * Value for justification mode indicating the text is justified by stretching letter spacing. + */ + @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION) + public static final int JUSTIFICATION_MODE_INTER_CHARACTER = + LineBreaker.JUSTIFICATION_MODE_INTER_CHARACTER; + /* * Line spacing multiplier for default line spacing. */ @@ -809,7 +817,7 @@ public abstract class Layout { getEllipsisStart(lineNum) + getEllipsisCount(lineNum), isFallbackLineSpacingEnabled()); if (justify) { - tl.justify(right - left - indentWidth); + tl.justify(mJustificationMode, right - left - indentWidth); } tl.draw(canvas, x, ltop, lbaseline, lbottom); } @@ -1058,7 +1066,7 @@ public abstract class Layout { getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), isFallbackLineSpacingEnabled()); if (isJustificationRequired(line)) { - tl.justify(getJustifyWidth(line)); + tl.justify(mJustificationMode, getJustifyWidth(line)); } tl.metrics(null, rectF, false, null); @@ -1794,7 +1802,7 @@ public abstract class Layout { getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), isFallbackLineSpacingEnabled()); if (isJustificationRequired(line)) { - tl.justify(getJustifyWidth(line)); + tl.justify(mJustificationMode, getJustifyWidth(line)); } final float width = tl.metrics(null, null, mUseBoundsForWidth, null); TextLine.recycle(tl); @@ -1882,7 +1890,7 @@ public abstract class Layout { getEllipsisStart(line), getEllipsisStart(line) + getEllipsisCount(line), isFallbackLineSpacingEnabled()); if (isJustificationRequired(line)) { - tl.justify(getJustifyWidth(line)); + tl.justify(mJustificationMode, getJustifyWidth(line)); } final float width = tl.metrics(null, null, mUseBoundsForWidth, null); TextLine.recycle(tl); diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 2175b47e149e..224e5d8f7712 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -100,9 +100,25 @@ public class TextLine { // Additional width of whitespace for justification. This value is per whitespace, thus // the line width will increase by mAddedWidthForJustify x (number of stretchable whitespaces). - private float mAddedWidthForJustify; + private float mAddedWordSpacingInPx; + private float mAddedLetterSpacingInPx; private boolean mIsJustifying; + @VisibleForTesting + public float getAddedWordSpacingInPx() { + return mAddedWordSpacingInPx; + } + + @VisibleForTesting + public float getAddedLetterSpacingInPx() { + return mAddedLetterSpacingInPx; + } + + @VisibleForTesting + public boolean isJustifying() { + return mIsJustifying; + } + private final TextPaint mWorkPaint = new TextPaint(); private final TextPaint mActivePaint = new TextPaint(); @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -259,7 +275,7 @@ public class TextLine { } } mTabs = tabStops; - mAddedWidthForJustify = 0; + mAddedWordSpacingInPx = 0; mIsJustifying = false; mEllipsisStart = ellipsisStart != ellipsisEnd ? ellipsisStart : 0; @@ -274,19 +290,42 @@ public class TextLine { * Justify the line to the given width. */ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) - public void justify(float justifyWidth) { + public void justify(@Layout.JustificationMode int justificationMode, float justifyWidth) { int end = mLen; while (end > 0 && isLineEndSpace(mText.charAt(mStart + end - 1))) { end--; } - final int spaces = countStretchableSpaces(0, end); - if (spaces == 0) { - // There are no stretchable spaces, so we can't help the justification by adding any - // width. - return; + if (justificationMode == Layout.JUSTIFICATION_MODE_INTER_WORD) { + float width = Math.abs(measure(end, false, null, null, null)); + final int spaces = countStretchableSpaces(0, end); + if (spaces == 0) { + // There are no stretchable spaces, so we can't help the justification by adding any + // width. + return; + } + mAddedWordSpacingInPx = (justifyWidth - width) / spaces; + mAddedLetterSpacingInPx = 0; + } else { // justificationMode == Layout.JUSTIFICATION_MODE_INTER_CHARACTER + LineInfo lineInfo = new LineInfo(); + float width = Math.abs(measure(end, false, null, null, lineInfo)); + + int lettersCount = lineInfo.getClusterCount(); + if (lettersCount < 2) { + return; + } + mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1); + if (mAddedLetterSpacingInPx > 0.03) { + // If the letter spacing is more than 0.03em, the ligatures are automatically + // disabled, so re-calculate everything without ligatures. + final String oldFontFeatures = mPaint.getFontFeatureSettings(); + mPaint.setFontFeatureSettings(oldFontFeatures + ", \"liga\" off, \"cliga\" off"); + width = Math.abs(measure(end, false, null, null, lineInfo)); + lettersCount = lineInfo.getClusterCount(); + mAddedLetterSpacingInPx = (justifyWidth - width) / (lettersCount - 1); + mPaint.setFontFeatureSettings(oldFontFeatures); + } + mAddedWordSpacingInPx = 0; } - final float width = Math.abs(measure(end, false, null, null, null)); - mAddedWidthForJustify = (justifyWidth - width) / spaces; mIsJustifying = true; } @@ -529,6 +568,9 @@ public class TextLine { throw new IndexOutOfBoundsException( "offset(" + offset + ") should be less than line limit(" + mLen + ")"); } + if (lineInfo != null) { + lineInfo.setClusterCount(0); + } final int target = trailing ? offset - 1 : offset; if (target < 0) { return 0; @@ -1076,7 +1118,8 @@ public class TextLine { TextPaint wp = mWorkPaint; wp.set(mPaint); if (mIsJustifying) { - wp.setWordSpacing(mAddedWidthForJustify); + wp.setWordSpacing(mAddedWordSpacingInPx); + wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize()); // Convert to Em } int spanStart = runStart; @@ -1277,7 +1320,8 @@ public class TextLine { @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, int runFlag) { if (mIsJustifying) { - wp.setWordSpacing(mAddedWidthForJustify); + wp.setWordSpacing(mAddedWordSpacingInPx); + wp.setLetterSpacing(mAddedLetterSpacingInPx / wp.getTextSize()); // Convert to Em } // Get metrics first (even for empty strings or "0" width runs) if (drawBounds != null && fmi == null) { diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e0bda9181380..3c36227eda0a 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -26,7 +26,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.InputDevice.SOURCE_CLASS_NONE; import static android.view.InsetsSource.ID_IME; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; -import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; @@ -1012,10 +1011,8 @@ public final class ViewRootImpl implements ViewParent, // Used to check if there were any view invalidations in // the previous time frame (FRAME_RATE_IDLENESS_REEVALUATE_TIME). private boolean mHasInvalidation = false; - // Used to check if it is in the frame rate boosting period. + // Used to check if it is in the touch boosting period. private boolean mIsFrameRateBoosting = false; - // Used to check if it is in touch boosting period. - private boolean mIsTouchBoosting = false; // Used to check if there is a message in the message queue // for idleness handling. private boolean mHasIdledMessage = false; @@ -6424,12 +6421,11 @@ public final class ViewRootImpl implements ViewParent, * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME). */ mIsFrameRateBoosting = false; - mIsTouchBoosting = false; setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory, mLastPreferredFrameRateCategory)); break; case MSG_CHECK_INVALIDATION_IDLE: - if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) { + if (!mHasInvalidation && !mIsFrameRateBoosting) { mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; setPreferredFrameRateCategory(mPreferredFrameRateCategory); mHasIdledMessage = false; @@ -7452,7 +7448,7 @@ public final class ViewRootImpl implements ViewParent, // For the variable refresh rate project if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { // set the frame rate to the maximum value. - mIsTouchBoosting = true; + mIsFrameRateBoosting = true; setPreferredFrameRateCategory(mPreferredFrameRateCategory); } /** @@ -12204,16 +12200,8 @@ public final class ViewRootImpl implements ViewParent, return; } - int frameRateCategory = mIsTouchBoosting - ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory; - - // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT - // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction. - // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction - // (e.g., Window Initialization). - if (mIsFrameRateBoosting || mInsetsAnimationRunning) { - frameRateCategory = FRAME_RATE_CATEGORY_HIGH; - } + int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning + ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory; try { if (mLastPreferredFrameRateCategory != frameRateCategory) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index efae57c9946c..a11ac7cb48ad 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -94,6 +94,13 @@ flag { } flag { + name: "skip_accessibility_warning_dialog_for_trusted_services" + namespace: "accessibility" + description: "Skips showing the accessibility warning dialog for trusted services." + bug: "303511250" +} + +flag { namespace: "accessibility" name: "update_always_on_a11y_service" description: "Updates the Always-On A11yService state when the user changes the enablement of the shortcut." diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index c20b278f7eaa..7f5331b936e9 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -167,6 +167,11 @@ public class WindowTokenClient extends Binder { + ", reported config=" + currentConfig + ", updated config=" + newConfig); } + // Update display first. In case callers want to obtain display information( + // ex: DisplayMetrics) in #onConfigurationChanged callback. + if (displayChanged) { + context.updateDisplay(newDisplayId); + } if (shouldUpdateResources) { // TODO(ag/9789103): update resource manager logic to track non-activity tokens mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId); @@ -195,9 +200,6 @@ public class WindowTokenClient extends Binder { } } } - if (displayChanged) { - context.updateDisplay(newDisplayId); - } } /** diff --git a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java index ce9ab82614d5..2ff62251d786 100644 --- a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java +++ b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java @@ -21,6 +21,7 @@ import android.accounts.AccountManager; import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.app.backup.BackupHelper; +import android.app.backup.BackupHelperWithLogger; import android.content.ContentResolver; import android.content.Context; import android.content.SyncAdapterType; @@ -56,7 +57,7 @@ import java.util.Set; * sync settings are backed up as a JSON object containing all the necessary information for * restoring the sync settings later. */ -public class AccountSyncSettingsBackupHelper implements BackupHelper { +public class AccountSyncSettingsBackupHelper extends BackupHelperWithLogger { private static final String TAG = "AccountSyncSettingsBackupHelper"; private static final boolean DEBUG = false; diff --git a/core/jni/Android.bp b/core/jni/Android.bp index c8fd246a255b..656cc3e2af71 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -188,8 +188,6 @@ cc_library_shared_for_libandroid_runtime { "android_os_SharedMemory.cpp", "android_os_storage_StorageManager.cpp", "android_os_UEventObserver.cpp", - "android_os_VintfObject.cpp", - "android_os_VintfRuntimeInfo.cpp", "android_os_incremental_IncrementalManager.cpp", "android_net_LocalSocketImpl.cpp", "android_service_DataLoaderService.cpp", @@ -280,6 +278,7 @@ cc_library_shared_for_libandroid_runtime { "libdmabufinfo", "libgif", "libgui_window_info_static", + "libkernelconfigs", "libseccomp_policy", "libgrallocusage", "libscrypt_static", @@ -350,7 +349,6 @@ cc_library_shared_for_libandroid_runtime { "libnativeloader_lazy", "libmemunreachable", "libhidlbase", - "libvintf", "libnativedisplay", "libnativewindow", "libdl", @@ -458,8 +456,25 @@ cc_library_shared_for_libandroid_runtime { // (e.g. gDefaultServiceManager) "libbinder", "libhidlbase", // libhwbinder is in here - "libvintf", ], }, }, } + +cc_library_shared { + name: "libvintf_jni", + + cpp_std: "gnu++20", + + srcs: [ + "android_os_VintfObject.cpp", + "android_os_VintfRuntimeInfo.cpp", + ], + + shared_libs: [ + "libbase", + "liblog", + "libnativehelper", + "libvintf", + ], +} diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 7a16318f3276..aa63f4fa03d4 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -152,8 +152,6 @@ extern int register_android_os_MessageQueue(JNIEnv* env); extern int register_android_os_Parcel(JNIEnv* env); extern int register_android_os_PerformanceHintManager(JNIEnv* env); extern int register_android_os_SELinux(JNIEnv* env); -extern int register_android_os_VintfObject(JNIEnv *env); -extern int register_android_os_VintfRuntimeInfo(JNIEnv *env); extern int register_android_os_storage_StorageManager(JNIEnv* env); extern int register_android_os_SystemProperties(JNIEnv *env); extern int register_android_os_SystemClock(JNIEnv* env); @@ -1545,8 +1543,6 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_os_NativeHandle), REG_JNI(register_android_os_ServiceManager), REG_JNI(register_android_os_storage_StorageManager), - REG_JNI(register_android_os_VintfObject), - REG_JNI(register_android_os_VintfRuntimeInfo), REG_JNI(register_android_service_DataLoaderService), REG_JNI(register_android_view_DisplayEventReceiver), REG_JNI(register_android_view_Surface), diff --git a/core/jni/android_os_Debug.cpp b/core/jni/android_os_Debug.cpp index de1ce4e29198..1504a00f972c 100644 --- a/core/jni/android_os_Debug.cpp +++ b/core/jni/android_os_Debug.cpp @@ -52,7 +52,7 @@ #include <memunreachable/memunreachable.h> #include <android-base/strings.h> #include "android_os_Debug.h" -#include <vintf/VintfObject.h> +#include <vintf/KernelConfigs.h> namespace android { @@ -1004,10 +1004,9 @@ static jboolean android_os_Debug_isVmapStack(JNIEnv *env, jobject clazz) } cfg_state = CONFIG_UNKNOWN; if (cfg_state == CONFIG_UNKNOWN) { - auto runtime_info = vintf::VintfObject::GetInstance()->getRuntimeInfo( - vintf::RuntimeInfo::FetchFlag::CONFIG_GZ); - CHECK(runtime_info != nullptr) << "Kernel configs cannot be fetched. b/151092221"; - const std::map<std::string, std::string>& configs = runtime_info->kernelConfigs(); + std::map<std::string, std::string> configs; + const status_t result = android::kernelconfigs::LoadKernelConfigs(&configs); + CHECK(result == OK) << "Kernel configs could not be fetched. b/151092221"; std::map<std::string, std::string>::const_iterator it = configs.find("CONFIG_VMAP_STACK"); cfg_state = (it != configs.end() && it->second == "y") ? CONFIG_SET : CONFIG_UNSET; } diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp index 477bd096b11a..734b5f497e2e 100644 --- a/core/jni/android_os_HwBinder.cpp +++ b/core/jni/android_os_HwBinder.cpp @@ -39,7 +39,6 @@ #include <hwbinder/ProcessState.h> #include <nativehelper/ScopedLocalRef.h> #include <nativehelper/ScopedUtfChars.h> -#include <vintf/parse_string.h> #include <utils/misc.h> #include "core_jni_helpers.h" diff --git a/core/jni/android_os_VintfObject.cpp b/core/jni/android_os_VintfObject.cpp index a5b2f65eafc7..ce4a33735c6d 100644 --- a/core/jni/android_os_VintfObject.cpp +++ b/core/jni/android_os_VintfObject.cpp @@ -17,16 +17,14 @@ #define LOG_TAG "VintfObject" //#define LOG_NDEBUG 0 #include <android-base/logging.h> - -#include <vector> -#include <string> - -#include <nativehelper/JNIHelp.h> #include <vintf/VintfObject.h> #include <vintf/parse_string.h> #include <vintf/parse_xml.h> -#include "core_jni_helpers.h" +#include <vector> +#include <string> + +#include "jni_wrappers.h" static jclass gString; static jclass gHashMapClazz; @@ -94,7 +92,7 @@ static jobjectArray android_os_VintfObject_report(JNIEnv* env, jclass) return toJavaStringArray(env, cStrings); } -static jint android_os_VintfObject_verifyBuildAtBoot(JNIEnv* env, jclass) { +static jint android_os_VintfObject_verifyBuildAtBoot(JNIEnv*, jclass) { std::string error; // Use temporary VintfObject, not the shared instance, to release memory // after check. @@ -204,4 +202,23 @@ int register_android_os_VintfObject(JNIEnv* env) NELEM(gVintfObjectMethods)); } -}; +extern int register_android_os_VintfRuntimeInfo(JNIEnv* env); + +} // namespace android + +jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { + JNIEnv* env = NULL; + if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) { + return JNI_ERR; + } + + if (android::register_android_os_VintfObject(env) < 0) { + return JNI_ERR; + } + + if (android::register_android_os_VintfRuntimeInfo(env) < 0) { + return JNI_ERR; + } + + return JNI_VERSION_1_6; +} diff --git a/core/jni/android_os_VintfRuntimeInfo.cpp b/core/jni/android_os_VintfRuntimeInfo.cpp index b0271b9e92af..7c2f58829446 100644 --- a/core/jni/android_os_VintfRuntimeInfo.cpp +++ b/core/jni/android_os_VintfRuntimeInfo.cpp @@ -17,23 +17,22 @@ #define LOG_TAG "VintfRuntimeInfo" //#define LOG_NDEBUG 0 -#include <nativehelper/JNIHelp.h> #include <vintf/VintfObject.h> #include <vintf/parse_string.h> #include <vintf/parse_xml.h> -#include "core_jni_helpers.h" +#include "jni_wrappers.h" namespace android { using vintf::RuntimeInfo; using vintf::VintfObject; -#define MAP_STRING_METHOD(javaMethod, cppString, flags) \ - static jstring android_os_VintfRuntimeInfo_##javaMethod(JNIEnv* env, jclass clazz) { \ - std::shared_ptr<const RuntimeInfo> info = VintfObject::GetRuntimeInfo(flags); \ - if (info == nullptr) return nullptr; \ - return env->NewStringUTF((cppString).c_str()); \ +#define MAP_STRING_METHOD(javaMethod, cppString, flags) \ + static jstring android_os_VintfRuntimeInfo_##javaMethod(JNIEnv* env, jclass) { \ + std::shared_ptr<const RuntimeInfo> info = VintfObject::GetRuntimeInfo(flags); \ + if (info == nullptr) return nullptr; \ + return env->NewStringUTF((cppString).c_str()); \ } MAP_STRING_METHOD(getCpuInfo, info->cpuInfo(), RuntimeInfo::FetchFlag::CPU_INFO); @@ -49,9 +48,7 @@ MAP_STRING_METHOD(getBootAvbVersion, vintf::to_string(info->bootAvbVersion()), MAP_STRING_METHOD(getBootVbmetaAvbVersion, vintf::to_string(info->bootVbmetaAvbVersion()), RuntimeInfo::FetchFlag::AVB); - -static jlong android_os_VintfRuntimeInfo_getKernelSepolicyVersion(JNIEnv *env, jclass clazz) -{ +static jlong android_os_VintfRuntimeInfo_getKernelSepolicyVersion(JNIEnv*, jclass) { std::shared_ptr<const RuntimeInfo> info = VintfObject::GetRuntimeInfo(RuntimeInfo::FetchFlag::POLICYVERS); if (info == nullptr) return 0; diff --git a/core/jni/core_jni_helpers.h b/core/jni/core_jni_helpers.h index 210dc895d674..769fa723c96e 100644 --- a/core/jni/core_jni_helpers.h +++ b/core/jni/core_jni_helpers.h @@ -22,6 +22,8 @@ #include <nativehelper/scoped_utf_chars.h> #include <android_runtime/AndroidRuntime.h> +#include "jni_wrappers.h" + // Host targets (layoutlib) do not differentiate between regular and critical native methods, // and they need all the JNI methods to have JNIEnv* and jclass/jobject as their first two arguments. // The following macro allows to have those arguments when compiling for host while omitting them when @@ -36,60 +38,6 @@ namespace android { -// Defines some helpful functions. - -static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) { - jclass clazz = env->FindClass(class_name); - LOG_ALWAYS_FATAL_IF(clazz == NULL, "Unable to find class %s", class_name); - return clazz; -} - -static inline jfieldID GetFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name, - const char* field_signature) { - jfieldID res = env->GetFieldID(clazz, field_name, field_signature); - LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find field %s with signature %s", field_name, - field_signature); - return res; -} - -static inline jmethodID GetMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name, - const char* method_signature) { - jmethodID res = env->GetMethodID(clazz, method_name, method_signature); - LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find method %s with signature %s", method_name, - method_signature); - return res; -} - -static inline jfieldID GetStaticFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name, - const char* field_signature) { - jfieldID res = env->GetStaticFieldID(clazz, field_name, field_signature); - LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static field %s with signature %s", field_name, - field_signature); - return res; -} - -static inline jmethodID GetStaticMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name, - const char* method_signature) { - jmethodID res = env->GetStaticMethodID(clazz, method_name, method_signature); - LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static method %s with signature %s", - method_name, method_signature); - return res; -} - -template <typename T> -static inline T MakeGlobalRefOrDie(JNIEnv* env, T in) { - jobject res = env->NewGlobalRef(in); - LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to create global reference."); - return static_cast<T>(res); -} - -static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className, - const JNINativeMethod* gMethods, int numMethods) { - int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods); - LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods."); - return res; -} - /** * Returns the result of invoking java.lang.ref.Reference.get() on a Reference object. */ diff --git a/core/jni/jni_wrappers.h b/core/jni/jni_wrappers.h new file mode 100644 index 000000000000..3b29e305e410 --- /dev/null +++ b/core/jni/jni_wrappers.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +// JNI wrappers for better logging + +#include <jni.h> +#include <log/log.h> +#include <nativehelper/JNIHelp.h> + +namespace android { + +static inline jclass FindClassOrDie(JNIEnv* env, const char* class_name) { + jclass clazz = env->FindClass(class_name); + LOG_ALWAYS_FATAL_IF(clazz == NULL, "Unable to find class %s", class_name); + return clazz; +} + +static inline jfieldID GetFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name, + const char* field_signature) { + jfieldID res = env->GetFieldID(clazz, field_name, field_signature); + LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find field %s with signature %s", field_name, + field_signature); + return res; +} + +static inline jmethodID GetMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name, + const char* method_signature) { + jmethodID res = env->GetMethodID(clazz, method_name, method_signature); + LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find method %s with signature %s", method_name, + method_signature); + return res; +} + +static inline jfieldID GetStaticFieldIDOrDie(JNIEnv* env, jclass clazz, const char* field_name, + const char* field_signature) { + jfieldID res = env->GetStaticFieldID(clazz, field_name, field_signature); + LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static field %s with signature %s", field_name, + field_signature); + return res; +} + +static inline jmethodID GetStaticMethodIDOrDie(JNIEnv* env, jclass clazz, const char* method_name, + const char* method_signature) { + jmethodID res = env->GetStaticMethodID(clazz, method_name, method_signature); + LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to find static method %s with signature %s", + method_name, method_signature); + return res; +} + +template <typename T> +static inline T MakeGlobalRefOrDie(JNIEnv* env, T in) { + jobject res = env->NewGlobalRef(in); + LOG_ALWAYS_FATAL_IF(res == NULL, "Unable to create global reference."); + return static_cast<T>(res); +} + +static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className, + const JNINativeMethod* gMethods, int numMethods) { + int res = jniRegisterNativeMethods(env, className, gMethods, numMethods); + LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods."); + return res; +} + +} // namespace android diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index b199420537d5..52cf67981bb3 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3569,6 +3569,13 @@ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_NEARBY_COMMUNICATION" android:protectionLevel="internal|role" /> + <!-- Allows an application to set policy related to <a + href="https://www.threadgroup.org">Thread</a> network. + @FlaggedApi("com.android.net.thread.flags.thread_user_restriction_enabled") + --> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK" + android:protectionLevel="internal|role" /> + <!-- Allows an application to set policy related to windows. <p>{@link Manifest.permission#MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL} is required to call APIs protected by this permission on users different to the calling user. @@ -4961,7 +4968,7 @@ <p>Protection level: signature --> <permission android:name="android.permission.BIND_NFC_SERVICE" - android:protectionLevel="signature" /> + android:protectionLevel="signature|module" /> <!-- Must be required by a {@link android.service.quickaccesswallet.QuickAccessWalletService} to ensure that only the system can bind to it. diff --git a/core/res/res/drawable/autofill_half_sheet_divider.xml b/core/res/res/drawable/autofill_half_sheet_divider.xml new file mode 100644 index 000000000000..1a96c7dc263e --- /dev/null +++ b/core/res/res/drawable/autofill_half_sheet_divider.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copied from //frameworks/base/core/res/res/drawable/list_divider_material.xml. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="@color/foreground_material_light"> + <solid android:color="#1f000000" /> + <size + android:height="1dp" + android:width="1dp"/> +</shape>
\ No newline at end of file diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml index 27f8138ac5e3..ddedca2865c4 100644 --- a/core/res/res/layout/autofill_save.xml +++ b/core/res/res/layout/autofill_save.xml @@ -31,11 +31,11 @@ android:gravity="center_horizontal" android:orientation="vertical"> <ScrollView + android:id="@+id/autofill_sheet_scroll_view" android:layout_width="fill_parent" android:layout_height="0dp" android:fillViewport="true" - android:layout_weight="1" - android:layout_marginBottom="8dp"> + android:layout_weight="1"> <LinearLayout android:layout_marginStart="@dimen/autofill_save_outer_margin" android:layout_marginEnd="@dimen/autofill_save_outer_margin" @@ -66,16 +66,25 @@ android:layout_height="wrap_content" android:minHeight="0dp" android:visibility="gone"/> - + <View + android:id="@+id/autofill_sheet_scroll_view_space" + android:layout_width="match_parent" + android:layout_height="16dp"/> </LinearLayout> </ScrollView> + <View + android:id="@+id/autofill_sheet_divider" + android:layout_width="match_parent" + android:layout_height="1dp" + style="@style/AutofillHalfSheetDivider" /> + <com.android.internal.widget.ButtonBarLayout android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="end" android:clipToPadding="false" - android:layout_marginTop="16dp" + android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton" android:orientation="horizontal" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 5be29a6d68b8..5e2aacdb24af 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -57,6 +57,7 @@ <item><xliff:g id="id">@string/status_bar_screen_record</xliff:g></item> <item><xliff:g id="id">@string/status_bar_cast</xliff:g></item> <item><xliff:g id="id">@string/status_bar_ethernet</xliff:g></item> + <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item> <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item> <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item> <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item> @@ -102,6 +103,7 @@ <string translatable="false" name="status_bar_call_strength">call_strength</string> <string translatable="false" name="status_bar_sensors_off">sensors_off</string> <string translatable="false" name="status_bar_screen_record">screen_record</string> + <string translatable="false" name="status_bar_oem_satellite">satellite</string> <!-- Flag indicating whether the surface flinger has limited alpha compositing functionality in hardware. If set, the window @@ -4495,6 +4497,16 @@ <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. --> <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string> + <!-- Array of component names, each flattened to a string, for accessibility services that + can be enabled by the user without showing a warning prompt. These services must be + preinstalled. --> + <string-array translatable="false" name="config_trustedAccessibilityServices"> + <!-- + <item>com.example.package.first/com.example.class.FirstService</item> + <item>com.example.package.second/com.example.class.SecondService</item> + --> + </string-array> + <!-- Warning: This API can be dangerous when not implemented properly. In particular, escrow token must NOT be retrievable from device storage. In other words, either escrow token is not stored on device or its ciphertext is stored on device while @@ -5334,20 +5346,19 @@ and a second time clipped to the fill level to indicate charge --> <bool name="config_batterymeterDualTone">false</bool> - <!-- The default refresh rate for a given device. Change this value to set a higher default - refresh rate. If the hardware composer on the device supports display modes with a higher - refresh rate than the default value specified here, the framework may use those higher - refresh rate modes if an app chooses one by setting preferredDisplayModeId or calling - setFrameRate(). - If a non-zero value is set for config_defaultPeakRefreshRate, then - config_defaultRefreshRate may be set to 0, in which case the value set for - config_defaultPeakRefreshRate will act as the default frame rate. --> - <integer name="config_defaultRefreshRate">60</integer> - - <!-- The default peak refresh rate for a given device. Change this value if you want to prevent - the framework from using higher refresh rates, even if display modes with higher refresh - rates are available from hardware composer. Only has an effect if the value is - non-zero. --> + <!-- The default refresh rate for a given device. This value is used to set the + global refresh rate vote, and when set to zero it has no effect on the vote. + If this value is non-zero but the hardware composer on the device supports + display modes with higher refresh rates, the framework may use those higher + refresh rate modes if an app chooses one by setting preferredDisplayModeId + or calling setFrameRate().--> + <integer name="config_defaultRefreshRate">0</integer> + + <!-- The default peak refresh rate for a given device. This value is used to set the + global peak refresh rate vote, and when set to zero it has no effect on the vote. + Change this value to non-zero if you want to prevent the framework from using higher + refresh rates, even if display modes with higher refresh rates are available from + hardware composer. --> <integer name="config_defaultPeakRefreshRate">0</integer> <!-- External display peak refresh rate for the given device. Change this value if you want to @@ -6890,4 +6901,8 @@ <!-- Defines suitability of the built-in speaker route. Refer to {@link MediaRoute2Info} to see supported values. --> <integer name="config_mediaRouter_builtInSpeakerSuitability">0</integer> + + <!-- Whether to show a percentage text next to the progressbar while preparing to update the + device --> + <bool name="config_showPercentageTextDuringRebootToUpdate">true</bool> </resources> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 619ec31e37bc..22d028cb079e 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1515,6 +1515,11 @@ please see styles_device_defaults.xml. <item name="background">@drawable/btn_outlined</item> </style> + <!-- @hide Divider for Autofill half screen dialog --> + <style name="AutofillHalfSheetDivider"> + <item name="android:background">@drawable/autofill_half_sheet_divider</item> + </style> + <!-- @hide Autofill background for popup window (not for fullscreen) --> <style name="AutofillDatasetPicker"> <item name="elevation">4dp</item> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index d12ef2b95f06..33ea02ac8172 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3198,6 +3198,7 @@ <java-symbol type="string" name="status_bar_camera" /> <java-symbol type="string" name="status_bar_sensors_off" /> <java-symbol type="string" name="status_bar_screen_record" /> + <java-symbol type="string" name="status_bar_oem_satellite" /> <!-- Locale picker --> <java-symbol type="id" name="locale_search_menu" /> @@ -3629,6 +3630,7 @@ <java-symbol type="string" name="config_defaultAccessibilityService" /> <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> + <java-symbol type="array" name="config_trustedAccessibilityServices" /> <java-symbol type="string" name="accessibility_select_shortcut_menu_title" /> <java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" /> @@ -3702,6 +3704,10 @@ <java-symbol type="id" name="autofill_dataset_list"/> <java-symbol type="id" name="autofill_dataset_picker"/> <java-symbol type="id" name="autofill_dataset_title" /> + <java-symbol type="id" name="autofill_sheet_divider"/> + <java-symbol type="id" name="autofill_sheet_scroll_view"/> + <java-symbol type="id" name="autofill_sheet_scroll_view_space"/> + <java-symbol type="id" name="autofill_save_custom_subtitle" /> <java-symbol type="id" name="autofill_save_icon" /> <java-symbol type="id" name="autofill_save_no" /> @@ -5311,4 +5317,7 @@ <!-- Android MediaRouter framework configs. --> <java-symbol type="integer" name="config_mediaRouter_builtInSpeakerSuitability" /> + + <!-- Shutdown thread config flags --> + <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" /> </resources> diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java index bbac69f35e81..4f9b2697ee89 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioManagerTest.java @@ -23,6 +23,7 @@ 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.doThrow; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -728,10 +729,10 @@ public final class RadioManagerTest { @Test public void equals_withFmBandConfigsOfDifferentAfSupportValues() { - RadioManager.FmBandConfig fmBandConfigCompared = new RadioManager.FmBandConfig( - new RadioManager.FmBandDescriptor(REGION, RadioManager.BAND_FM, FM_LOWER_LIMIT, - FM_UPPER_LIMIT, FM_SPACING, STEREO_SUPPORTED, RDS_SUPPORTED, TA_SUPPORTED, - !AF_SUPPORTED, EA_SUPPORTED)); + RadioManager.FmBandConfig.Builder builder = new RadioManager.FmBandConfig.Builder( + createFmBandDescriptor()).setStereo(STEREO_SUPPORTED).setRds(RDS_SUPPORTED) + .setTa(TA_SUPPORTED).setAf(!AF_SUPPORTED).setEa(EA_SUPPORTED); + RadioManager.FmBandConfig fmBandConfigCompared = builder.build(); assertWithMessage("FM Band Config of different af support value") .that(FM_BAND_CONFIG).isNotEqualTo(fmBandConfigCompared); @@ -1300,6 +1301,18 @@ public final class RadioManagerTest { } @Test + public void addAnnouncementListener_withListenerAddedBeforeAndCloseException_throws() + throws Exception { + createRadioManager(); + Set<Integer> enableTypeSet = createAnnouncementTypeSet(EVENT_ANNOUNCEMENT_TYPE); + mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener); + doThrow(new RemoteException()).when(mCloseHandleMock).close(); + + assertThrows(RuntimeException.class, + () -> mRadioManager.addAnnouncementListener(enableTypeSet, mEventListener)); + } + + @Test public void addAnnouncementListener_whenServiceDied_throwException() throws Exception { createRadioManager(); String exceptionMessage = "service is dead"; diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java index 4841711f712d..4cda26de2906 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java @@ -125,6 +125,16 @@ public final class TunerAdapterTest { } @Test + public void close_forTunerAdapterCalledTwice() throws Exception { + mRadioTuner.close(); + verify(mTunerMock).close(); + + mRadioTuner.close(); + + verify(mTunerMock).close(); + } + + @Test public void setConfiguration_forTunerAdapter() throws Exception { int status = mRadioTuner.setConfiguration(TEST_BAND_CONFIG); @@ -134,6 +144,12 @@ public final class TunerAdapterTest { } @Test + public void setConfiguration_withNull_fails() throws Exception { + assertWithMessage("Status for setting configuration with null") + .that(mRadioTuner.setConfiguration(null)).isEqualTo(RadioManager.STATUS_BAD_VALUE); + } + + @Test public void setConfiguration_withInvalidParameters_fails() throws Exception { doThrow(new IllegalArgumentException()).when(mTunerMock).setConfiguration(any()); @@ -840,6 +856,15 @@ public final class TunerAdapterTest { } @Test + public void onTuneFailed_withDeadService() throws Exception { + mTunerCallback.onTuneFailed(RadioManager.STATUS_DEAD_OBJECT, FM_SELECTOR); + + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onTuneFailed( + RadioManager.STATUS_DEAD_OBJECT, FM_SELECTOR); + verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SERVER_DIED); + } + + @Test public void onProgramListChanged_forTunerCallbackAdapter() throws Exception { mTunerCallback.onProgramListChanged(); diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java index e118c98dd4da..3ee565f8e025 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -403,4 +403,41 @@ public class SQLiteDatabaseTest { } assertFalse(allowed); } + + /** Return true if the path is in the list of strings. */ + private boolean isConcurrent(String path) throws Exception { + path = new File(path).toPath().toRealPath().toString(); + return SQLiteDatabase.getConcurrentDatabasePaths().contains(path); + } + + @Test + public void testDuplicateDatabases() throws Exception { + // The two database paths in this test are assumed not to have been opened earlier in this + // process. + + // A database path that will be opened twice. + final String dbName = "never-used-db.db"; + final File dbFile = mContext.getDatabasePath(dbName); + final String dbPath = dbFile.getPath(); + + // A database path that will be opened only once. + final String okName = "never-used-ok.db"; + final File okFile = mContext.getDatabasePath(okName); + final String okPath = okFile.getPath(); + + SQLiteDatabase db1 = SQLiteDatabase.openOrCreateDatabase(dbFile, null); + assertFalse(isConcurrent(dbPath)); + SQLiteDatabase db2 = SQLiteDatabase.openOrCreateDatabase(dbFile, null); + assertTrue(isConcurrent(dbPath)); + db1.close(); + assertTrue(isConcurrent(dbPath)); + db2.close(); + assertTrue(isConcurrent(dbPath)); + + SQLiteDatabase db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null); + db3.close(); + db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null); + assertFalse(isConcurrent(okPath)); + db3.close(); + } } diff --git a/core/tests/coretests/src/android/text/TextLineJustificationTest.kt b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt new file mode 100644 index 000000000000..a52561557523 --- /dev/null +++ b/core/tests/coretests/src/android/text/TextLineJustificationTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.text + +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TextLineJustificationTest { + + @Rule + @JvmField + val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + private val PAINT = TextPaint().apply { + textSize = 10f // make 1em = 10px + } + + private fun makeTextLine(cs: CharSequence, paint: TextPaint) = TextLine.obtain().apply { + set(paint, cs, 0, cs.length, Layout.DIR_LEFT_TO_RIGHT, Layout.DIRS_ALL_LEFT_TO_RIGHT, false, + null, 0, 0, false) + } + + private fun getClusterCount(cs: CharSequence, paint: TextPaint) = TextLine.LineInfo().apply { + makeTextLine(cs, paint).also { + it.metrics(null, null, false, this) + TextLine.recycle(it) + } + }.clusterCount + + fun justifyTest_WithoutJustify() { + val line = "Hello, World." + val tl = makeTextLine(line, PAINT) + + // Without calling justify method, justifying should be false and all added spaces should + // be zeros. + assertThat(tl.isJustifying).isFalse() + assertThat(tl.addedWordSpacingInPx).isEqualTo(0) + assertThat(tl.addedLetterSpacingInPx).isEqualTo(0) + } + + @Test + fun justifyTest_IntrCharacter_Latin() { + val line = "Hello, World." + val clusterCount = getClusterCount(line, PAINT) + val originalWidth = Layout.getDesiredWidth(line, PAINT) + val extraWidth = 100f + + val tl = makeTextLine(line, PAINT) + tl.justify(Layout.JUSTIFICATION_MODE_INTER_CHARACTER, originalWidth + extraWidth) + + assertThat(tl.isJustifying).isTrue() + assertThat(tl.addedWordSpacingInPx).isEqualTo(0) + assertThat(tl.addedLetterSpacingInPx).isEqualTo(extraWidth / (clusterCount - 1)) + + TextLine.recycle(tl) + } + + @Test + fun justifyTest_IntrCharacter_Japanese() { + val line = "\u672C\u65E5\u306F\u6674\u5929\u306A\u308A\u3002" + val clusterCount = getClusterCount(line, PAINT) + val originalWidth = Layout.getDesiredWidth(line, PAINT) + val extraWidth = 100f + + val tl = makeTextLine(line, PAINT) + tl.justify(Layout.JUSTIFICATION_MODE_INTER_CHARACTER, originalWidth + extraWidth) + + assertThat(tl.isJustifying).isTrue() + assertThat(tl.addedWordSpacingInPx).isEqualTo(0) + assertThat(tl.addedLetterSpacingInPx).isEqualTo(extraWidth / (clusterCount - 1)) + + TextLine.recycle(tl) + } + + @Test + fun justifyTest_IntrWord_Latin() { + val line = "Hello, World." + val originalWidth = Layout.getDesiredWidth(line, PAINT) + val extraWidth = 100f + + val tl = makeTextLine(line, PAINT) + tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, originalWidth + extraWidth) + + assertThat(tl.isJustifying).isTrue() + // This text contains only one whitespace, so word spacing should be same to the extraWidth. + assertThat(tl.addedWordSpacingInPx).isEqualTo(extraWidth) + assertThat(tl.addedLetterSpacingInPx).isEqualTo(0) + + TextLine.recycle(tl) + } + + @Test + fun justifyTest_IntrWord_Japanese() { + val line = "\u672C\u65E5\u306F\u6674\u0020\u5929\u306A\u308A\u3002" + val originalWidth = Layout.getDesiredWidth(line, PAINT) + val extraWidth = 100f + + val tl = makeTextLine(line, PAINT) + tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, originalWidth + extraWidth) + + assertThat(tl.isJustifying).isTrue() + // This text contains only one whitespace, so word spacing should be same to the extraWidth. + assertThat(tl.addedWordSpacingInPx).isEqualTo(extraWidth) + assertThat(tl.addedLetterSpacingInPx).isEqualTo(0) + + TextLine.recycle(tl) + } +}
\ No newline at end of file diff --git a/core/tests/coretests/src/android/text/TextLineTest.java b/core/tests/coretests/src/android/text/TextLineTest.java index a31992c8cfa1..8ae5669de55f 100644 --- a/core/tests/coretests/src/android/text/TextLineTest.java +++ b/core/tests/coretests/src/android/text/TextLineTest.java @@ -53,7 +53,7 @@ public class TextLineTest { final float originalWidth = tl.metrics(null, null, false, null); final float expandedWidth = 2 * originalWidth; - tl.justify(expandedWidth); + tl.justify(Layout.JUSTIFICATION_MODE_INTER_WORD, expandedWidth); final float newWidth = tl.metrics(null, null, false, null); TextLine.recycle(tl); return Math.abs(newWidth - expandedWidth) < 0.5; diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index cfbda84f24e1..cf3eb12498ca 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -19,7 +19,6 @@ package android.view; import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; -import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; @@ -576,13 +575,8 @@ public class ViewRootImplTest { assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL); - viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), - FRAME_RATE_CATEGORY_HIGH_HINT); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); - viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT); - assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL); assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW); diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 607b4bdf71ca..294b8ae0f6f5 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -23,40 +23,48 @@ applications that come with the platform <!-- Needed for Build.getSerial(), which is used to send a unique number for serial, per HUIG. --> <privapp-permissions package="android.car.usb.handler"> <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.angle"> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.apps.tag"> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.backupconfirm"> <permission name="android.permission.BACKUP"/> <permission name="android.permission.CRYPT_KEEPER"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.externalstorage"> <permission name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <permission name="android.permission.WRITE_MEDIA_STORAGE"/> <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.imsserviceentitlement"> <permission name="android.permission.MODIFY_PHONE_STATE" /> <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE" /> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.launcher3"> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.location.fused"> <permission name="android.permission.INSTALL_LOCATION_PROVIDER"/> <permission name="android.permission.UPDATE_DEVICE_STATS"/> <permission name="android.permission.UPDATE_APP_OPS_STATS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.managedprovisioning"> @@ -79,12 +87,14 @@ applications that come with the platform <permission name="android.permission.WRITE_SECURE_SETTINGS"/> <permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND"/> <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.mms.service"> <permission name="android.permission.BIND_CARRIER_MESSAGING_SERVICE"/> <permission name="android.permission.BIND_CARRIER_SERVICES"/> <permission name="android.permission.INTERACT_ACROSS_USERS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.mtp"> @@ -94,16 +104,19 @@ applications that come with the platform <permission name="android.permission.INTERACT_ACROSS_USERS"/> <permission name="android.permission.WRITE_MEDIA_STORAGE"/> <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.musicfx"> <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.networkrecommendation"> <permission name="android.permission.SCORE_NETWORKS"/> <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/> <permission name="android.permission.WRITE_SECURE_SETTINGS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.packageinstaller"> @@ -114,6 +127,7 @@ applications that come with the platform <permission name="android.permission.UPDATE_APP_OPS_STATS"/> <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/> <permission name="android.permission.PACKAGE_USAGE_STATS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.phone"> @@ -170,6 +184,7 @@ applications that come with the platform <permission name="android.permission.LOG_COMPAT_CHANGE"/> <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG"/> <permission name="android.permission.UWB_PRIVILEGED"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.providers.calendar"> @@ -180,6 +195,7 @@ applications that come with the platform <permission name="android.permission.USE_RESERVED_DISK"/> <permission name="android.permission.LOG_COMPAT_CHANGE" /> <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.providers.contacts"> @@ -193,6 +209,7 @@ applications that come with the platform <permission name="android.permission.USE_RESERVED_DISK"/> <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> <permission name="android.permission.LOG_COMPAT_CHANGE" /> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.providers.downloads"> @@ -205,6 +222,7 @@ applications that come with the platform <permission name="android.permission.UPDATE_APP_OPS_STATS"/> <permission name="android.permission.UPDATE_DEVICE_STATS"/> <permission name="android.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.providers.telephony"> @@ -214,6 +232,7 @@ applications that come with the platform <!-- Permissions required for reading and logging compat changes --> <permission name="android.permission.LOG_COMPAT_CHANGE" /> <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.server.telecom"> @@ -229,11 +248,13 @@ applications that come with the platform <permission name="android.permission.MODIFY_PHONE_STATE"/> <permission name="android.permission.STOP_APP_SWITCHES"/> <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.sharedstoragebackup"> <permission name="android.permission.WRITE_MEDIA_STORAGE"/> <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.shell"> @@ -547,16 +568,19 @@ applications that come with the platform <permission name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" /> <!-- Permission required for BinaryTransparencyService shell API and host test --> <permission name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> <permission name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/> <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/> <permission name="android.permission.INTERACT_ACROSS_USERS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.soundpicker"> <permission name="android.permission.INTERACT_ACROSS_USERS" /> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.tv"> @@ -568,15 +592,18 @@ applications that come with the platform <permission name="android.permission.READ_CONTENT_RATING_SYSTEMS"/> <permission name="com.android.providers.tv.permission.ACCESS_ALL_EPG_DATA"/> <permission name="com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.vpndialogs"> <permission name="android.permission.CONTROL_VPN"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.wallpaper.livepicker"> <permission name="android.permission.SET_WALLPAPER_COMPONENT"/> <permission name="android.permission.BIND_WALLPAPER"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.wallpaper"> @@ -584,25 +611,30 @@ applications that come with the platform <permission name="android.permission.BIND_WALLPAPER"/> <permission name="android.permission.CUSTOMIZE_SYSTEM_UI"/> <permission name="android.permission.SET_WALLPAPER_DIM_AMOUNT"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.dynsystem"> <permission name="android.permission.REBOOT"/> <permission name="android.permission.MANAGE_DYNAMIC_SYSTEM"/> <permission name="android.permission.READ_OEM_UNLOCK_STATE"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.settings"> <permission name="android.permission.INSTALL_DYNAMIC_SYSTEM"/> <permission name="android.permission.BIND_CELL_BROADCAST_SERVICE"/> <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> <permission name="android.permission.SEND_SAFETY_CENTER_UPDATE" /> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.bips"> <permission name="android.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> <privapp-permissions package="com.android.calllogbackup"> <permission name="com.android.voicemail.permission.READ_VOICEMAIL"/> + <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> </permissions> diff --git a/graphics/java/android/graphics/text/LineBreaker.java b/graphics/java/android/graphics/text/LineBreaker.java index 0e3fb163ef75..97071260402c 100644 --- a/graphics/java/android/graphics/text/LineBreaker.java +++ b/graphics/java/android/graphics/text/LineBreaker.java @@ -17,6 +17,7 @@ package android.graphics.text; import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH; +import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION; import android.annotation.FlaggedApi; import android.annotation.FloatRange; @@ -163,7 +164,8 @@ public class LineBreaker { /** @hide */ @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = { JUSTIFICATION_MODE_NONE, - JUSTIFICATION_MODE_INTER_WORD + JUSTIFICATION_MODE_INTER_WORD, + JUSTIFICATION_MODE_INTER_CHARACTER, }) @Retention(RetentionPolicy.SOURCE) public @interface JustificationMode {} @@ -179,6 +181,12 @@ public class LineBreaker { public static final int JUSTIFICATION_MODE_INTER_WORD = 1; /** + * Value for justification mode indicating the text is justified by stretching letter spacing. + */ + @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION) + public static final int JUSTIFICATION_MODE_INTER_CHARACTER = 2; + + /** * Helper class for creating a {@link LineBreaker}. */ public static final class Builder { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 83d555cbdecd..14a46778810c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -17,6 +17,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_SUCCESS; +import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; @@ -110,6 +111,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen static final boolean ENABLE_SHELL_TRANSITIONS = SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); + // TODO(b/295993745): remove after prebuilt library is updated. + private static final String KEY_ACTIVITY_STACK_TOKEN = + "androidx.window.extensions.embedding.ActivityStackToken"; + @VisibleForTesting @GuardedBy("mLock") final SplitPresenter mPresenter; @@ -2779,8 +2784,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // TODO(b/232042367): Consolidate the activity create handling so that we can handle // cross-process the same as normal. + IBinder activityStackToken = options.getBinder(KEY_ACTIVITY_STACK_TOKEN); + if (activityStackToken != null) { + // Put activityStack token to #KEY_LAUNCH_TASK_FRAGMENT_TOKEN to launch the activity + // into the taskFragment associated with the token. + options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, activityStackToken); + } + // Early return if the launching taskfragment is already been set. - if (options.getBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) { + // TODO(b/295993745): Use KEY_LAUNCH_TASK_FRAGMENT_TOKEN after WM Jetpack migrates to + // bundle. This is still needed to support #setLaunchingActivityStack. + if (options.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) { synchronized (mLock) { mCurrentIntent = intent; } @@ -2837,7 +2851,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Amend the request to let the WM know that the activity should be placed in // the dedicated container. // TODO(b/229680885): skip override launching TaskFragment token by split-rule - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); mCurrentIntent = intent; } else { @@ -2855,8 +2869,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (mCurrentIntent != null && result != START_SUCCESS) { // Clear the pending appeared intent if the activity was not started // successfully. - final IBinder token = bOptions.getBinder( - ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN); + final IBinder token = bOptions.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN); if (token != null) { final TaskFragmentContainer container = getContainer(token); if (container != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java index 30d5edb59c85..160f922dd928 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java @@ -199,6 +199,10 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { } private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) { + if (leash == null || !leash.isValid()) { + return; + } + final float scale = targetRect.width() / mStartTaskRect.width(); mTransformMatrix.reset(); mTransformMatrix.setScale(scale, scale); @@ -211,12 +215,16 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { private void finishAnimation() { if (mEnteringTarget != null) { - mTransaction.setCornerRadius(mEnteringTarget.leash, 0); - mEnteringTarget.leash.release(); + if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) { + mTransaction.setCornerRadius(mEnteringTarget.leash, 0); + mEnteringTarget.leash.release(); + } mEnteringTarget = null; } if (mClosingTarget != null) { - mClosingTarget.leash.release(); + if (mClosingTarget.leash != null) { + mClosingTarget.leash.release(); + } mClosingTarget = null; } if (mBackground != null) { @@ -260,7 +268,9 @@ public class CrossActivityBackAnimation extends ShellBackAnimation { } private void onGestureCommitted() { - if (mEnteringTarget == null || mClosingTarget == null) { + if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null + || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid() + || !mClosingTarget.leash.isValid()) { finishAnimation(); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index ac2a1c867462..adc78391f033 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -208,7 +208,9 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { float top = mapRange(progress, mClosingStartRect.top, targetTop); float width = mapRange(progress, mClosingStartRect.width(), targetWidth); float height = mapRange(progress, mClosingStartRect.height(), targetHeight); - mTransaction.setLayer(mClosingTarget.leash, 0); + if (mClosingTarget.leash != null && mClosingTarget.leash.isValid()) { + mTransaction.setLayer(mClosingTarget.leash, 0); + } mClosingCurrentRect.set(left, top, left + width, top + height); applyTransform(mClosingTarget.leash, mClosingCurrentRect, mCornerRadius); @@ -226,7 +228,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { /** Transform the target window to match the target rect. */ private void applyTransform(SurfaceControl leash, RectF targetRect, float cornerRadius) { - if (leash == null) { + if (leash == null || !leash.isValid()) { return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index d4a92894a397..a5f7880cfd41 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -137,7 +137,7 @@ import java.util.function.IntConsumer; * The controller manages addition, removal, and visible state of bubbles on screen. */ public class BubbleController implements ConfigurationChangeListener, - RemoteCallable<BubbleController> { + RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -706,6 +706,7 @@ public class BubbleController implements ConfigurationChangeListener, return mBubbleIconFactory; } + @Override public Bubbles.SysuiProxy getSysuiProxy() { return mSysuiProxy; } @@ -732,8 +733,7 @@ public class BubbleController implements ConfigurationChangeListener, if (mStackView == null) { mStackView = new BubbleStackView( mContext, this, mBubbleData, mSurfaceSynchronizer, - mFloatingContentCoordinator, - mMainExecutor); + mFloatingContentCoordinator, this, mMainExecutor); mStackView.onOrientationChanged(); if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index c25d41275f2b..a619401301aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -206,6 +206,7 @@ public class BubbleStackView extends FrameLayout }; private final BubbleController mBubbleController; private final BubbleData mBubbleData; + private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider; private StackViewState mStackViewState = new StackViewState(); private final ValueAnimator mDismissBubbleAnimator; @@ -875,12 +876,14 @@ public class BubbleStackView extends FrameLayout public BubbleStackView(Context context, BubbleController bubbleController, BubbleData data, @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, + Bubbles.SysuiProxy.Provider sysuiProxyProvider, ShellExecutor mainExecutor) { super(context); mMainExecutor = mainExecutor; mBubbleController = bubbleController; mBubbleData = data; + mSysuiProxyProvider = sysuiProxyProvider; Resources res = getResources(); mBubbleSize = res.getDimensionPixelSize(R.dimen.bubble_size); @@ -2090,7 +2093,7 @@ public class BubbleStackView extends FrameLayout hideCurrentInputMethod(); - mBubbleController.getSysuiProxy().onStackExpandChanged(shouldExpand); + mSysuiProxyProvider.getSysuiProxy().onStackExpandChanged(shouldExpand); if (wasExpanded) { stopMonitoringSwipeUpGesture(); @@ -3034,7 +3037,7 @@ public class BubbleStackView extends FrameLayout if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) { mManageMenu.setVisibility(View.INVISIBLE); mManageMenuScrim.setVisibility(INVISIBLE); - mBubbleController.getSysuiProxy().onManageMenuExpandChanged(false /* show */); + mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(false /* show */); return; } if (show) { @@ -3048,7 +3051,7 @@ public class BubbleStackView extends FrameLayout } }; - mBubbleController.getSysuiProxy().onManageMenuExpandChanged(show); + mSysuiProxyProvider.getSysuiProxy().onManageMenuExpandChanged(show); mManageMenuScrim.animate() .setInterpolator(show ? ALPHA_IN : ALPHA_OUT) .alpha(show ? BUBBLE_EXPANDED_SCRIM_ALPHA : 0f) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 759246eb285d..28af0ca6ac6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -321,6 +321,13 @@ public interface Bubbles { /** Callback to tell SysUi components execute some methods. */ interface SysuiProxy { + + /** Provider interface for {@link SysuiProxy}. */ + interface Provider { + /** Returns {@link SysuiProxy}. */ + SysuiProxy getSysuiProxy(); + } + void isNotificationPanelExpand(Consumer<Boolean> callback); void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index 9ce46d69815b..e9cd73b0df5e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -95,8 +95,8 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " - + "entering PIP from an Activity Embedding window"); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for entering PIP from" + + " an Activity Embedding window #%d", info.getDebugId()); // Split into two transitions (wct) TransitionInfo.Change pipChange = null; final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */); @@ -146,6 +146,8 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for opening an intent" + + " with a remote transition and PIP #%d", info.getDebugId()); boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip( info, startTransaction, finishTransaction, finishCallback); // Consume the transition on remote handler if the leftover handler already handle this @@ -192,8 +194,9 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { } return false; } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate" - + " animation because remote-animation likely doesn't support it"); + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate" + + " animation because remote-animation likely doesn't support it #%d", + info.getDebugId()); // Split the transition into 2 parts: the pip part and the rest. mInFlightSubAnimations = 2; // make a new startTransaction because pip's startEnterAnimation "consumes" it so @@ -218,6 +221,9 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for unfolding #%d", + info.getDebugId()); + final Transitions.TransitionFinishCallback finishCB = (wct) -> { mInFlightSubAnimations--; if (mInFlightSubAnimations > 0) return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 643e0266d7df..4ea71490798c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -30,9 +30,11 @@ import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.WindowContainerTransaction; +import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.splitscreen.StageCoordinator; @@ -77,6 +79,9 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition for Recents during" + + " Desktop #%d", info.getDebugId()); + if (mInfo == null) { mInfo = info; mFinishT = finishTransaction; @@ -109,6 +114,9 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for Recents during" + + " Keyguard #%d", info.getDebugId()); + if (mInfo == null) { mInfo = info; mFinishT = finishTransaction; @@ -122,6 +130,9 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for Recents during" + + " split screen #%d", info.getDebugId()); + for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); // Pip auto-entering info might be appended to recent transition like pressing diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index f58332198696..4878df806f82 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -125,7 +125,7 @@ class BubbleViewInfoTest : ShellTestCase() { mock<BubbleProperties>()) bubbleStackView = BubbleStackView(context, bubbleController, bubbleData, - surfaceSynchronizer, FloatingContentCoordinator(), mainExecutor) + surfaceSynchronizer, FloatingContentCoordinator(), bubbleController, mainExecutor) bubbleBarLayerView = BubbleBarLayerView(context, bubbleController) } diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp index 4d020c567972..5f5ffe97e953 100644 --- a/libs/hwui/AutoBackendTextureRelease.cpp +++ b/libs/hwui/AutoBackendTextureRelease.cpp @@ -21,6 +21,7 @@ #include <include/gpu/GrDirectContext.h> #include <include/gpu/GrBackendSurface.h> #include <include/gpu/MutableTextureState.h> +#include <include/gpu/vk/VulkanMutableTextureState.h> #include "renderthread/RenderThread.h" #include "utils/Color.h" #include "utils/PaintUtils.h" @@ -142,8 +143,9 @@ void AutoBackendTextureRelease::releaseQueueOwnership(GrDirectContext* context) LOG_ALWAYS_FATAL_IF(Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan); if (mBackendTexture.isValid()) { // Passing in VK_IMAGE_LAYOUT_UNDEFINED means we keep the old layout. - skgpu::MutableTextureState newState(VK_IMAGE_LAYOUT_UNDEFINED, - VK_QUEUE_FAMILY_FOREIGN_EXT); + skgpu::MutableTextureState newState = skgpu::MutableTextureStates::MakeVulkan( + VK_IMAGE_LAYOUT_UNDEFINED, + VK_QUEUE_FAMILY_FOREIGN_EXT); // The unref for this ref happens in the releaseProc passed into setBackendTextureState. The // releaseProc callback will be made when the work to set the new state has finished on the diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 0b42c88aa448..f526a280b113 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -230,7 +230,7 @@ void RenderNode::pushLayerUpdate(TreeInfo& info) { * stencil buffer may be needed. Views that use a functor to draw will be forced onto a layer. */ void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) { - if (mDamageGenerationId == info.damageGenerationId) { + if (mDamageGenerationId == info.damageGenerationId && mDamageGenerationId != 0) { // We hit the same node a second time in the same tree. We don't know the minimal // damage rect anymore, so just push the biggest we can onto our parent's transform // We push directly onto parent in case we are clipped to bounds but have moved position. diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index 1f3834be5bef..c9045427bd42 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -262,7 +262,7 @@ private: DisplayList mDisplayList; DisplayList mStagingDisplayList; - int64_t mDamageGenerationId; + int64_t mDamageGenerationId = 0; friend class AnimatorManager; AnimatorManager mAnimatorManager; diff --git a/libs/hwui/jni/PathMeasure.cpp b/libs/hwui/jni/PathMeasure.cpp index acf893e9544c..79acb6cc35e5 100644 --- a/libs/hwui/jni/PathMeasure.cpp +++ b/libs/hwui/jni/PathMeasure.cpp @@ -17,7 +17,11 @@ #include "GraphicsJNI.h" +#include "SkMatrix.h" +#include "SkPath.h" #include "SkPathMeasure.h" +#include "SkPoint.h" +#include "SkScalar.h" /* We declare an explicit pair, so that we don't have to rely on the java client to be sure not to edit the path while we have an active measure diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/gnss.aconfig index 5f1279ecceea..8c7c8716b2dc 100644 --- a/location/java/android/location/flags/gnss.aconfig +++ b/location/java/android/location/flags/gnss.aconfig @@ -36,15 +36,15 @@ flag { } flag { - name: "gnss_configuration_from_resource" + name: "replace_future_elapsed_realtime_jni" namespace: "location" - description: "Flag for GNSS configuration from resource" - bug: "317734846" + description: "Flag for replacing future elapsedRealtime in JNI" + bug: "314328533" } flag { - name: "replace_future_elapsed_realtime_jni" + name: "gnss_configuration_from_resource" namespace: "location" - description: "Flag for replacing future elapsedRealtime in JNI" - bug: "314328533" + description: "Flag for GNSS configuration from resource" + bug: "317734846" } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 2eec9b3d4a09..8dfa6be0fd11 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -754,15 +754,16 @@ interface IAudioService { void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher); - oneway void startLoudnessCodecUpdates(int piid, in List<LoudnessCodecInfo> codecInfoSet); + oneway void startLoudnessCodecUpdates(int sessionId); - oneway void stopLoudnessCodecUpdates(int piid); + oneway void stopLoudnessCodecUpdates(int sessionId); - oneway void addLoudnessCodecInfo(int piid, int mediaCodecHash, in LoudnessCodecInfo codecInfo); + oneway void addLoudnessCodecInfo(int sessionId, int mediaCodecHash, + in LoudnessCodecInfo codecInfo); - oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo); + oneway void removeLoudnessCodecInfo(int sessionId, in LoudnessCodecInfo codecInfo); - PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo); + PersistableBundle getLoudnessParams(in LoudnessCodecInfo codecInfo); @EnforcePermission("MODIFY_AUDIO_SETTINGS_PRIVILEGED") @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED)") diff --git a/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl b/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl index 16eaaea38b7f..202242755a97 100644 --- a/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl +++ b/media/java/android/media/ILoudnessCodecUpdatesDispatcher.aidl @@ -16,6 +16,7 @@ package android.media; +import android.media.AudioAttributes; import android.os.PersistableBundle; /** @@ -26,6 +27,6 @@ import android.os.PersistableBundle; */ oneway interface ILoudnessCodecUpdatesDispatcher { - void dispatchLoudnessCodecParameterChange(int piid, in PersistableBundle params); + void dispatchLoudnessCodecParameterChange(int sessionId, in PersistableBundle params); }
\ No newline at end of file diff --git a/media/java/android/media/LoudnessCodecConfigurator.java b/media/java/android/media/LoudnessCodecController.java index aadd78328d68..b3e5c52b27b3 100644 --- a/media/java/android/media/LoudnessCodecConfigurator.java +++ b/media/java/android/media/LoudnessCodecController.java @@ -16,13 +16,13 @@ package android.media; -import static android.media.AudioPlaybackConfiguration.PLAYER_PIID_INVALID; import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4; import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D; import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; +import android.media.permission.SafeCloseable; import android.os.Bundle; import android.util.Log; @@ -32,7 +32,6 @@ import androidx.annotation.Nullable; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -41,16 +40,21 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; /** - * Class for getting recommended loudness parameter updates for audio decoders, according to the - * encoded format and current audio routing. Those updates can be automatically applied to the - * {@link MediaCodec} instance(s), or be provided to the user. The codec loudness management - * parameter updates are defined by the CTA-2075 standard. - * <p>A new object should be instantiated for each {@link AudioTrack} with the help - * of {@link #create()} or {@link #create(Executor, OnLoudnessCodecUpdateListener)}. + * Class for getting recommended loudness parameter updates for audio decoders as they are used + * to play back media content according to the encoded format and current audio routing. These + * audio decoder updates leverage loudness metadata present in compressed audio streams. They + * ensure the loudness and dynamic range of the content is optimized to the physical + * characteristics of the audio output device (e.g. phone microspeakers vs headphones vs TV + * speakers).Those updates can be automatically applied to the {@link MediaCodec} instance(s), or + * be provided to the user. The codec loudness management parameter updates are computed in + * accordance to the CTA-2075 standard. + * <p>A new object should be instantiated for each audio session + * (see {@link AudioManager#generateAudioSessionId()}) using creator methods {@link #create(int)} or + * {@link #create(int, Executor, OnLoudnessCodecUpdateListener)}. */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) -public class LoudnessCodecConfigurator { - private static final String TAG = "LoudnessCodecConfigurator"; +public class LoudnessCodecController implements SafeCloseable { + private static final String TAG = "LoudnessCodecController"; /** * Listener used for receiving asynchronous loudness metadata updates. @@ -67,6 +71,7 @@ public class LoudnessCodecConfigurator { * directly on the mediaCodec. The listener can modify * these values with their own edits which will be * returned for the mediaCodec configuration + * * @return a Bundle which contains the original computed codecValues * aggregated with user edits. The platform will configure the associated * MediaCodecs with the returned Bundle params. @@ -74,159 +79,116 @@ public class LoudnessCodecConfigurator { @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) @NonNull default Bundle onLoudnessCodecUpdate(@NonNull MediaCodec mediaCodec, - @NonNull Bundle codecValues) { + @NonNull Bundle codecValues) { return codecValues; } } - @NonNull private final LoudnessCodecDispatcher mLcDispatcher; - - private final Object mConfiguratorLock = new Object(); - - @GuardedBy("mConfiguratorLock") - private AudioTrack mAudioTrack; + @NonNull + private final LoudnessCodecDispatcher mLcDispatcher; - @GuardedBy("mConfiguratorLock") - private final Executor mExecutor; + private final Object mControllerLock = new Object(); - @GuardedBy("mConfiguratorLock") - private final OnLoudnessCodecUpdateListener mListener; + private final int mSessionId; - @GuardedBy("mConfiguratorLock") + @GuardedBy("mControllerLock") private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>(); /** - * Creates a new instance of {@link LoudnessCodecConfigurator} + * Creates a new instance of {@link LoudnessCodecController} * * <p>This method should be used when the client does not need to alter the * codec loudness parameters before they are applied to the audio decoders. - * Otherwise, use {@link #create(Executor, OnLoudnessCodecUpdateListener)}. + * Otherwise, use {@link #create(int, Executor, OnLoudnessCodecUpdateListener)}. * - * @return the {@link LoudnessCodecConfigurator} instance + * @param sessionId the session ID of the track that will receive data + * from the added {@link MediaCodec}'s + * + * @return the {@link LoudnessCodecController} instance */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) - public static @NonNull LoudnessCodecConfigurator create() { - return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()), - Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {}); + public static @NonNull LoudnessCodecController create(int sessionId) { + final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher( + AudioManager.getService()); + final LoudnessCodecController controller = new LoudnessCodecController(dispatcher, + sessionId); + dispatcher.addLoudnessCodecListener(controller, Executors.newSingleThreadExecutor(), + new OnLoudnessCodecUpdateListener() {}); + dispatcher.startLoudnessCodecUpdates(sessionId); + return controller; } /** - * Creates a new instance of {@link LoudnessCodecConfigurator} + * Creates a new instance of {@link LoudnessCodecController} * * <p>This method should be used when the client wants to alter the codec * loudness parameters before they are applied to the audio decoders. - * Otherwise, use {@link #create()}. + * Otherwise, use {@link #create( int)}. * - * @param executor {@link Executor} to handle the callbacks - * @param listener used for receiving updates + * @param sessionId the session ID of the track that will receive data + * from the added {@link MediaCodec}'s + * @param executor {@link Executor} to handle the callbacks + * @param listener used for receiving updates * - * @return the {@link LoudnessCodecConfigurator} instance + * @return the {@link LoudnessCodecController} instance */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) - public static @NonNull LoudnessCodecConfigurator create( + public static @NonNull LoudnessCodecController create( + int sessionId, @NonNull @CallbackExecutor Executor executor, @NonNull OnLoudnessCodecUpdateListener listener) { Objects.requireNonNull(executor, "Executor cannot be null"); Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null"); - return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()), - executor, listener); + final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher( + AudioManager.getService()); + final LoudnessCodecController controller = new LoudnessCodecController(dispatcher, + sessionId); + dispatcher.addLoudnessCodecListener(controller, executor, listener); + dispatcher.startLoudnessCodecUpdates(sessionId); + return controller; } /** - * Creates a new instance of {@link LoudnessCodecConfigurator} + * Creates a new instance of {@link LoudnessCodecController} * * <p>This method should be used only in testing * - * @param service interface for communicating with AudioService + * @param sessionId the session ID of the track that will receive data + * from the added {@link MediaCodec}'s * @param executor {@link Executor} to handle the callbacks * @param listener used for receiving updates + * @param service interface for communicating with AudioService * - * @return the {@link LoudnessCodecConfigurator} instance - * + * @return the {@link LoudnessCodecController} instance * @hide */ - public static @NonNull LoudnessCodecConfigurator createForTesting( - @NonNull IAudioService service, + public static @NonNull LoudnessCodecController createForTesting( + int sessionId, @NonNull @CallbackExecutor Executor executor, - @NonNull OnLoudnessCodecUpdateListener listener) { + @NonNull OnLoudnessCodecUpdateListener listener, + @NonNull IAudioService service) { Objects.requireNonNull(service, "IAudioService cannot be null"); Objects.requireNonNull(executor, "Executor cannot be null"); Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null"); - return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(service), - executor, listener); + final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(service); + final LoudnessCodecController controller = new LoudnessCodecController(dispatcher, + sessionId); + dispatcher.addLoudnessCodecListener(controller, executor, listener); + dispatcher.startLoudnessCodecUpdates(sessionId); + return controller; } /** @hide */ - private LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher, - @NonNull @CallbackExecutor Executor executor, - @NonNull OnLoudnessCodecUpdateListener listener) { + private LoudnessCodecController(@NonNull LoudnessCodecDispatcher lcDispatcher, int sessionId) { mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null"); - mExecutor = Objects.requireNonNull(executor, "Executor cannot be null"); - mListener = Objects.requireNonNull(listener, - "OnLoudnessCodecUpdateListener cannot be null"); - } - - /** - * Sets the {@link AudioTrack} and starts receiving asynchronous updates for - * the registered {@link MediaCodec}s (see {@link #addMediaCodec(MediaCodec)}) - * - * <p>The AudioTrack should be the one that receives audio data from the - * added audio decoders and is used to determine the device routing on which - * the audio streaming will take place. This will directly influence the - * loudness parameters. - * <p>After calling this method the framework will compute the initial set of - * parameters which will be applied to the registered codecs/returned to the - * listener for modification. - * - * @param audioTrack the track that will receive audio data from the provided - * audio decoders. In case this is {@code null} this - * method will have the effect of clearing the existing set - * {@link AudioTrack} and will stop receiving asynchronous - * loudness updates - */ - @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) - public void setAudioTrack(@Nullable AudioTrack audioTrack) { - List<LoudnessCodecInfo> codecInfos; - int piid = PLAYER_PIID_INVALID; - int oldPiid = PLAYER_PIID_INVALID; - synchronized (mConfiguratorLock) { - if (mAudioTrack != null && mAudioTrack == audioTrack) { - Log.v(TAG, "Loudness configurator already started for piid: " - + mAudioTrack.getPlayerIId()); - return; - } - - codecInfos = getLoudnessCodecInfoList_l(); - if (mAudioTrack != null) { - oldPiid = mAudioTrack.getPlayerIId(); - mLcDispatcher.removeLoudnessCodecListener(this); - } - if (audioTrack != null) { - piid = audioTrack.getPlayerIId(); - mLcDispatcher.addLoudnessCodecListener(this, mExecutor, mListener); - } - - mAudioTrack = audioTrack; - } - - if (oldPiid != PLAYER_PIID_INVALID) { - Log.v(TAG, "Loudness configurator stopping updates for piid: " + oldPiid); - mLcDispatcher.stopLoudnessCodecUpdates(oldPiid); - } - if (piid != PLAYER_PIID_INVALID) { - Log.v(TAG, "Loudness configurator starting updates for piid: " + piid); - mLcDispatcher.startLoudnessCodecUpdates(piid, codecInfos); - } + mSessionId = sessionId; } /** - * Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack} - * which the client sets - * (see {@link LoudnessCodecConfigurator#setAudioTrack(AudioTrack)}). - * - * <p>This method can be called while asynchronous updates are live. + * Adds a new {@link MediaCodec} that will stream data to a player + * which uses {@link #mSessionId}. * * <p>No new element will be added if the passed {@code mediaCodec} was * previously added. @@ -234,23 +196,22 @@ public class LoudnessCodecConfigurator { * @param mediaCodec the codec to start receiving asynchronous loudness * updates. The codec has to be in a configured or started * state in order to add it for loudness updates. + * @return {@code false} if the {@code mediaCodec} was not configured or does + * not contain loudness metadata, {@code true} otherwise. * @throws IllegalArgumentException if the same {@code mediaCodec} was already * added before. - * @return {@code false} if the {@code mediaCodec} was not configured or does - * not contain loudness metadata, {@code true} otherwise. */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) public boolean addMediaCodec(@NonNull MediaCodec mediaCodec) { final MediaCodec mc = Objects.requireNonNull(mediaCodec, "MediaCodec for addMediaCodec cannot be null"); - int piid = PLAYER_PIID_INVALID; final LoudnessCodecInfo mcInfo = getCodecInfo(mc); if (mcInfo == null) { Log.v(TAG, "Could not extract codec loudness information"); return false; } - synchronized (mConfiguratorLock) { + synchronized (mControllerLock) { final AtomicBoolean containsCodec = new AtomicBoolean(false); Set<MediaCodec> newSet = mMediaCodecs.computeIfPresent(mcInfo, (info, codecSet) -> { containsCodec.set(!codecSet.add(mc)); @@ -263,16 +224,12 @@ public class LoudnessCodecConfigurator { } if (containsCodec.get()) { throw new IllegalArgumentException( - "Loudness configurator already added " + mediaCodec); - } - if (mAudioTrack != null) { - piid = mAudioTrack.getPlayerIId(); + "Loudness controller already added " + mediaCodec); } } - if (piid != PLAYER_PIID_INVALID) { - mLcDispatcher.addLoudnessCodecInfo(piid, mediaCodec.hashCode(), mcInfo); - } + mLcDispatcher.addLoudnessCodecInfo(mSessionId, mediaCodec.hashCode(), + mcInfo); return true; } @@ -291,7 +248,6 @@ public class LoudnessCodecConfigurator { */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) public void removeMediaCodec(@NonNull MediaCodec mediaCodec) { - int piid = PLAYER_PIID_INVALID; LoudnessCodecInfo mcInfo; AtomicBoolean removedMc = new AtomicBoolean(false); AtomicBoolean removeInfo = new AtomicBoolean(false); @@ -302,10 +258,7 @@ public class LoudnessCodecConfigurator { if (mcInfo == null) { throw new IllegalArgumentException("Could not extract codec loudness information"); } - synchronized (mConfiguratorLock) { - if (mAudioTrack != null) { - piid = mAudioTrack.getPlayerIId(); - } + synchronized (mControllerLock) { mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> { removedMc.set(mcs.remove(mediaCodec)); if (mcs.isEmpty()) { @@ -317,68 +270,81 @@ public class LoudnessCodecConfigurator { }); if (!removedMc.get()) { throw new IllegalArgumentException( - "Loudness configurator does not contain " + mediaCodec); + "Loudness controller does not contain " + mediaCodec); } } - if (piid != PLAYER_PIID_INVALID && removeInfo.get()) { - mLcDispatcher.removeLoudnessCodecInfo(piid, mcInfo); + if (removeInfo.get()) { + mLcDispatcher.removeLoudnessCodecInfo(mSessionId, mcInfo); } } /** - * Gets synchronous loudness updates when no listener is required. The provided - * {@link MediaCodec} streams audio data to the passed {@link AudioTrack}. + * Returns the loudness parameters of the registered audio decoders + * + * <p>Those parameters may have been automatically applied if the + * {@code LoudnessCodecController} was created with {@link #create(int)}, or they are the + * parameters that have been sent to the {@link OnLoudnessCodecUpdateListener} if using a + * codec update listener. * - * @param audioTrack track that receives audio data from the passed - * {@link MediaCodec} - * @param mediaCodec codec that decodes loudness annotated data for the passed - * {@link AudioTrack} + * @param mediaCodec codec that decodes loudness annotated data. Has to be added + * with {@link #addMediaCodec(MediaCodec)} before calling this + * method + * @throws IllegalArgumentException if the passed {@link MediaCodec} was not + * added before calling this method * - * @return the {@link Bundle} containing the current loudness parameters. Caller is - * responsible to update the {@link MediaCodec} + * @return the {@link Bundle} containing the current loudness parameters. */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) @NonNull - public Bundle getLoudnessCodecParams(@NonNull AudioTrack audioTrack, - @NonNull MediaCodec mediaCodec) { - Objects.requireNonNull(audioTrack, "Passed audio track cannot be null"); + public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) { + Objects.requireNonNull(mediaCodec, "MediaCodec cannot be null"); LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec); if (codecInfo == null) { - return new Bundle(); + throw new IllegalArgumentException("MediaCodec does not have valid codec information"); } - return mLcDispatcher.getLoudnessCodecParams(audioTrack.getPlayerIId(), codecInfo); + synchronized (mControllerLock) { + final Set<MediaCodec> codecs = mMediaCodecs.get(codecInfo); + if (codecs == null || !codecs.contains(mediaCodec)) { + throw new IllegalArgumentException( + "MediaCodec was not added for loudness annotation"); + } + } + + return mLcDispatcher.getLoudnessCodecParams(codecInfo); } - /** @hide */ - /*package*/ int getAssignedTrackPiid() { - int piid = PLAYER_PIID_INVALID; + /** + * Stops any loudness updates and frees up the resources. + */ + @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) + public void release() { + close(); + } - synchronized (mConfiguratorLock) { - if (mAudioTrack == null) { - return piid; - } - piid = mAudioTrack.getPlayerIId(); + /** @hide */ + @Override + public void close() { + synchronized (mControllerLock) { + mMediaCodecs.clear(); } + mLcDispatcher.stopLoudnessCodecUpdates(mSessionId); + } - return piid; + /** @hide */ + /*package*/ int getSessionId() { + return mSessionId; } /** @hide */ /*package*/ Map<LoudnessCodecInfo, Set<MediaCodec>> getRegisteredMediaCodecs() { - synchronized (mConfiguratorLock) { + synchronized (mControllerLock) { return mMediaCodecs; } } - @GuardedBy("mConfiguratorLock") - private List<LoudnessCodecInfo> getLoudnessCodecInfoList_l() { - return mMediaCodecs.values().stream().flatMap(listMc -> listMc.stream().map( - LoudnessCodecConfigurator::getCodecInfo)).toList(); - } - @Nullable private static LoudnessCodecInfo getCodecInfo(@NonNull MediaCodec mediaCodec) { LoudnessCodecInfo lci = new LoudnessCodecInfo(); diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java index b546a81b0498..46be54be55ec 100644 --- a/media/java/android/media/LoudnessCodecDispatcher.java +++ b/media/java/android/media/LoudnessCodecDispatcher.java @@ -21,7 +21,7 @@ import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION; import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL; import android.annotation.CallbackExecutor; -import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener; +import android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener; import android.os.Bundle; import android.os.PersistableBundle; import android.os.RemoteException; @@ -32,7 +32,6 @@ import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -59,7 +58,7 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { private final Object mLock = new Object(); @GuardedBy("mLock") - private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> + private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecController> mConfiguratorListener = new HashMap<>(); public static synchronized LoudnessCodecUpdatesDispatcherStub getInstance() { @@ -72,16 +71,16 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { private LoudnessCodecUpdatesDispatcherStub() {} @Override - public void dispatchLoudnessCodecParameterChange(int piid, PersistableBundle params) { + public void dispatchLoudnessCodecParameterChange(int sessionId, PersistableBundle params) { if (DEBUG) { - Log.d(TAG, "dispatchLoudnessCodecParameterChange for piid " + piid + Log.d(TAG, "dispatchLoudnessCodecParameterChange for sessionId " + sessionId + " persistable bundle: " + params); } mLoudnessListenerMgr.callListeners(listener -> { synchronized (mLock) { mConfiguratorListener.computeIfPresent(listener, (l, lcConfig) -> { // send the appropriate bundle for the user to update - if (lcConfig.getAssignedTrackPiid() == piid) { + if (lcConfig.getSessionId() == sessionId) { final Map<LoudnessCodecInfo, Set<MediaCodec>> mediaCodecsMap = lcConfig.getRegisteredMediaCodecs(); for (LoudnessCodecInfo codecInfo : mediaCodecsMap.keySet()) { @@ -111,7 +110,12 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { bundle)); if (!bundle.isDefinitelyEmpty()) { - mediaCodec.setParameters(bundle); + try { + mediaCodec.setParameters(bundle); + } catch (IllegalStateException e) { + Log.w(TAG, "Cannot set loudness bundle on media codec " + + mediaCodec); + } } if (canBreak) { break; @@ -145,7 +149,7 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { } void addLoudnessCodecListener(@NonNull CallbackUtil.DispatcherStub dispatcher, - @NonNull LoudnessCodecConfigurator configurator, + @NonNull LoudnessCodecController configurator, @NonNull @CallbackExecutor Executor executor, @NonNull OnLoudnessCodecUpdateListener listener) { Objects.requireNonNull(configurator); @@ -160,15 +164,15 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { } } - void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) { + void removeLoudnessCodecListener(@NonNull LoudnessCodecController configurator) { Objects.requireNonNull(configurator); OnLoudnessCodecUpdateListener listenerToRemove = null; synchronized (mLock) { - Iterator<Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>> iterator = + Iterator<Entry<OnLoudnessCodecUpdateListener, LoudnessCodecController>> iterator = mConfiguratorListener.entrySet().iterator(); while (iterator.hasNext()) { - Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e = + Entry<OnLoudnessCodecUpdateListener, LoudnessCodecController> e = iterator.next(); if (e.getValue() == configurator) { final OnLoudnessCodecUpdateListener listener = e.getKey(); @@ -208,7 +212,7 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { } /** @hide */ - public void addLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator, + public void addLoudnessCodecListener(@NonNull LoudnessCodecController configurator, @NonNull @CallbackExecutor Executor executor, @NonNull OnLoudnessCodecUpdateListener listener) { LoudnessCodecUpdatesDispatcherStub.getInstance().addLoudnessCodecListener(this, @@ -216,52 +220,52 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { } /** @hide */ - public void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) { + public void removeLoudnessCodecListener(@NonNull LoudnessCodecController configurator) { LoudnessCodecUpdatesDispatcherStub.getInstance().removeLoudnessCodecListener(configurator); } /** @hide */ - public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) { + public void startLoudnessCodecUpdates(int sessionId) { try { - mAudioService.startLoudnessCodecUpdates(piid, codecInfoList); + mAudioService.startLoudnessCodecUpdates(sessionId); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** @hide */ - public void stopLoudnessCodecUpdates(int piid) { + public void stopLoudnessCodecUpdates(int sessionId) { try { - mAudioService.stopLoudnessCodecUpdates(piid); + mAudioService.stopLoudnessCodecUpdates(sessionId); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** @hide */ - public void addLoudnessCodecInfo(int piid, int mediaCodecHash, + public void addLoudnessCodecInfo(int sessionId, int mediaCodecHash, @NonNull LoudnessCodecInfo mcInfo) { try { - mAudioService.addLoudnessCodecInfo(piid, mediaCodecHash, mcInfo); + mAudioService.addLoudnessCodecInfo(sessionId, mediaCodecHash, mcInfo); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** @hide */ - public void removeLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) { + public void removeLoudnessCodecInfo(int sessionId, @NonNull LoudnessCodecInfo mcInfo) { try { - mAudioService.removeLoudnessCodecInfo(piid, mcInfo); + mAudioService.removeLoudnessCodecInfo(sessionId, mcInfo); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** @hide */ - public Bundle getLoudnessCodecParams(int piid, @NonNull LoudnessCodecInfo mcInfo) { + public Bundle getLoudnessCodecParams(@NonNull LoudnessCodecInfo mcInfo) { Bundle loudnessParams = null; try { - loudnessParams = new Bundle(mAudioService.getLoudnessParams(piid, mcInfo)); + loudnessParams = new Bundle(mAudioService.getLoudnessParams(mcInfo)); } catch (RemoteException e) { e.rethrowFromSystemServer(); } diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index 587e35b4b1fc..5e40eee26886 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -17,6 +17,7 @@ package android.media; import static com.android.media.codec.flags.Flags.FLAG_CODEC_IMPORTANCE; +import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -121,6 +122,10 @@ import java.util.stream.Collectors; * <tr><td>{@link #KEY_MPEGH_REFERENCE_CHANNEL_LAYOUT}</td> * <td>Integer</td><td><b>decoder-only</b>, optional, if content is MPEG-H audio, * specifies the preferred reference channel layout of the stream.</td></tr> + * <tr><td>{@link #KEY_MAX_BUFFER_BATCH_OUTPUT_SIZE}</td><td>Integer</td><td>optional, used with + * large audio frame support, specifies max size of output buffer in bytes.</td></tr> + * <tr><td>{@link #KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE}</td><td>Integer</td><td>optional, + * used with large audio frame support, specifies threshold output size in bytes.</td></tr> * </table> * * Subtitle formats have the following keys: @@ -459,6 +464,50 @@ public final class MediaFormat { public static final String KEY_MAX_INPUT_SIZE = "max-input-size"; /** + * A key describing the maximum output buffer size in bytes when using + * large buffer mode containing multiple access units. + * + * When not-set - codec functions with one access-unit per frame. + * When set less than the size of two access-units - will make codec + * operate in single access-unit per output frame. + * When set to a value too big - The component or the framework will + * override this value to a reasonable max size not exceeding typical + * 10 seconds of data (device dependent) when set to a value larger than + * that. The value final value used will be returned in the output format. + * + * The associated value is an integer + * + * @see FEATURE_MultipleFrames + */ + @FlaggedApi(FLAG_LARGE_AUDIO_FRAME) + public static final String KEY_BUFFER_BATCH_MAX_OUTPUT_SIZE = "buffer-batch-max-output-size"; + + /** + * A key describing the threshold output size in bytes when using large buffer + * mode containing multiple access units. + * + * This is an optional parameter. + * + * If not set - the component can set this to a reasonable value. + * If set larger than max size, the components will + * clip this setting to maximum buffer batching output size. + * + * The component will return a partial output buffer if the output buffer reaches or + * surpass this limit. + * + * Threshold size should be always less or equal to KEY_MAX_BUFFER_BATCH_OUTPUT_SIZE. + * The final setting of this value as determined by the component will be returned + * in the output format + * + * The associated value is an integer + * + * @see FEATURE_MultipleFrames + */ + @FlaggedApi(FLAG_LARGE_AUDIO_FRAME) + public static final String KEY_BUFFER_BATCH_THRESHOLD_OUTPUT_SIZE = + "buffer-batch-threshold-output-size"; + + /** * A key describing the pixel aspect ratio width. * The associated value is an integer */ diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index 8e9c07996f83..a0f8ae5defeb 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -1154,6 +1154,7 @@ public class MediaPlayer extends PlayerBase setDataSource(afd); return true; } catch (NullPointerException | SecurityException | IOException ex) { + Log.w(TAG, "Error setting data source via ContentResolver", ex); return false; } } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index a49782f1ed2a..691aa7784d7a 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -3105,9 +3105,8 @@ public final class MediaRouter2 { mStub, mDiscoveryPreference); } - if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) { - unregisterRouterStubLocked(); - } + unregisterRouterStubIfNeededLocked(); + } catch (RemoteException ex) { Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex); } @@ -3315,13 +3314,12 @@ public final class MediaRouter2 { obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller)); } - if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) { - try { - unregisterRouterStubLocked(); - } catch (RemoteException ex) { - ex.rethrowFromSystemServer(); - } + try { + unregisterRouterStubIfNeededLocked(); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); } + } } @@ -3335,8 +3333,10 @@ public final class MediaRouter2 { } @GuardedBy("mLock") - private void unregisterRouterStubLocked() throws RemoteException { - if (mStub != null) { + private void unregisterRouterStubIfNeededLocked() throws RemoteException { + if (mStub != null + && mRouteCallbackRecords.isEmpty() + && mNonSystemRoutingControllers.isEmpty()) { mMediaRouterService.unregisterRouter2(mStub); mStub = null; } diff --git a/media/java/android/media/audiofx/Virtualizer.java b/media/java/android/media/audiofx/Virtualizer.java index 74b6fc13ade4..71147f4becb5 100644 --- a/media/java/android/media/audiofx/Virtualizer.java +++ b/media/java/android/media/audiofx/Virtualizer.java @@ -46,6 +46,11 @@ import java.util.StringTokenizer; * <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling * audio effects. + * + * @deprecated use the {@link android.media.Spatializer} class to query the capabilities of the + * platform with regards to spatialization, a different name for audio channel virtualization, + * and the {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)} to + * characterize how you want your content to be played when spatialization is supported. */ public class Virtualizer extends AudioEffect { diff --git a/media/java/android/media/tv/BroadcastInfoRequest.java b/media/java/android/media/tv/BroadcastInfoRequest.java index cbd8c1fd53cd..694756c3f1a3 100644 --- a/media/java/android/media/tv/BroadcastInfoRequest.java +++ b/media/java/android/media/tv/BroadcastInfoRequest.java @@ -32,7 +32,8 @@ import java.lang.annotation.RetentionPolicy; public abstract class BroadcastInfoRequest implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE}) + @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE, + REQUEST_OPTION_ONEWAY, REQUEST_OPTION_ONESHOT}) public @interface RequestOption {} /** @@ -47,6 +48,18 @@ public abstract class BroadcastInfoRequest implements Parcelable { * first time, new values are detected. */ public static final int REQUEST_OPTION_AUTO_UPDATE = 1; + /** + * Request option: one-way + * <p> With this option, no response is expected after sending the request. + * @hide + */ + public static final int REQUEST_OPTION_ONEWAY = 2; + /** + * Request option: one-shot + * <p> With this option, only one response will be given per request. + * @hide + */ + public static final int REQUEST_OPTION_ONESHOT = 3; public static final @NonNull Parcelable.Creator<BroadcastInfoRequest> CREATOR = new Parcelable.Creator<BroadcastInfoRequest>() { diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl index 0f8a00a3c7d9..897827750d4e 100644 --- a/media/java/android/media/tv/ITvInputClient.aidl +++ b/media/java/android/media/tv/ITvInputClient.aidl @@ -44,6 +44,7 @@ oneway interface ITvInputClient { void onTrackSelected(int type, in String trackId, int seq); void onVideoAvailable(int seq); void onVideoUnavailable(int reason, int seq); + void onVideoFreezeUpdated(boolean isFrozen, int seq); void onContentAllowed(int seq); void onContentBlocked(in String rating, int seq); void onLayoutSurface(int left, int top, int right, int bottom, int seq); diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl index a52e9a572dca..8e2702a50662 100644 --- a/media/java/android/media/tv/ITvInputSessionCallback.aidl +++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl @@ -41,6 +41,7 @@ oneway interface ITvInputSessionCallback { void onTrackSelected(int type, in String trackId); void onVideoAvailable(); void onVideoUnavailable(int reason); + void onVideoFreezeUpdated(boolean isFrozen); void onContentAllowed(); void onContentBlocked(in String rating); void onLayoutSurface(int left, int top, int right, int bottom); diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 51b2542688a6..caddd8ad674f 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -740,6 +740,15 @@ public final class TvInputManager { } /** + * This is called when the video freeze state has been updated. + * If {@code true}, the video is frozen on the last frame while audio playback continues. + * @param session A {@link TvInputManager.Session} associated with this callback. + * @param isFrozen Whether the video is frozen + */ + public void onVideoFreezeUpdated(Session session, boolean isFrozen) { + } + + /** * This is called when the current program content turns out to be allowed to watch since * its content rating is not blocked by parental controls. * @@ -1030,6 +1039,19 @@ public final class TvInputManager { }); } + void postVideoFreezeUpdated(boolean isFrozen) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onVideoFreezeUpdated(mSession, isFrozen); + if (mSession.mIAppNotificationEnabled + && mSession.getInteractiveAppSession() != null) { + mSession.getInteractiveAppSession().notifyVideoFreezeUpdated(isFrozen); + } + } + }); + } + void postContentAllowed() { mHandler.post(new Runnable() { @Override @@ -1546,6 +1568,18 @@ public final class TvInputManager { } @Override + public void onVideoFreezeUpdated(boolean isFrozen, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postVideoFreezeUpdated(isFrozen); + } + } + + @Override public void onContentAllowed(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java index 76d8e50f94be..6301a27bac4d 100644 --- a/media/java/android/media/tv/TvInputService.java +++ b/media/java/android/media/tv/TvInputService.java @@ -763,6 +763,34 @@ public abstract class TvInputService extends Service { } /** + * Informs the application that the video freeze state has been updated. + * + * When {@code true}, the video is frozen on the last frame but audio playback remains + * active. + * + * @param isFrozen Whether or not the video is frozen + * @hide + */ + public void notifyVideoFreezeUpdated(boolean isFrozen) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "notifyVideoFreezeUpdated"); + } + if (mSessionCallback != null) { + mSessionCallback.onVideoFreezeUpdated(isFrozen); + } + } catch (RemoteException e) { + Log.e(TAG, "error in notifyVideoFreezeUpdated", e); + } + } + }); + } + + /** * Sends an updated list of all audio presentations available from a Next Generation Audio * service. This is used by the framework to maintain the audio presentation information for * a given track of {@link TvTrackInfo#TYPE_AUDIO}, which in turn is used by diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 4316d053a275..0f58b29247bb 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -88,6 +88,7 @@ interface ITvInteractiveAppManager { void notifyTracksChanged(in IBinder sessionToken, in List<TvTrackInfo> tracks, int userId); void notifyVideoAvailable(in IBinder sessionToken, int userId); void notifyVideoUnavailable(in IBinder sessionToken, int reason, int userId); + void notifyVideoFreezeUpdated(in IBinder sessionToken, boolean isFrozen, int userId); void notifyContentAllowed(in IBinder sessionToken, int userId); void notifyContentBlocked(in IBinder sessionToken, in String rating, int userId); void notifySignalStrength(in IBinder sessionToken, int stength, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index ba7cf13a7a1d..06808c9ff915 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -67,6 +67,7 @@ oneway interface ITvInteractiveAppSession { void notifyTracksChanged(in List<TvTrackInfo> tracks); void notifyVideoAvailable(); void notifyVideoUnavailable(int reason); + void notifyVideoFreezeUpdated(boolean isFrozen); void notifyContentAllowed(); void notifyContentBlocked(in String rating); void notifySignalStrength(int strength); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index 518b08a93f95..77730aa46d0a 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -103,6 +103,7 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_SEND_TIME_SHIFT_MODE = 46; private static final int DO_SEND_AVAILABLE_SPEEDS = 47; private static final int DO_SEND_SELECTED_TRACK_INFO = 48; + private static final int DO_NOTIFY_VIDEO_FREEZE_UPDATED = 49; private final HandlerCaller mCaller; private Session mSessionImpl; @@ -364,6 +365,10 @@ public class ITvInteractiveAppSessionWrapper args.recycle(); break; } + case DO_NOTIFY_VIDEO_FREEZE_UPDATED: { + mSessionImpl.notifyVideoFreezeUpdated((Boolean) msg.obj); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -552,6 +557,12 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void notifyVideoFreezeUpdated(boolean isFrozen) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_VIDEO_FREEZE_UPDATED, + isFrozen)); + } + + @Override public void notifyContentAllowed() { mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_NOTIFY_CONTENT_ALLOWED)); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index bf4379f470d8..8a340f6862bb 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -1731,6 +1731,22 @@ public final class TvInteractiveAppManager { } /** + * Notifies Interactive app session when the video freeze state is updated + * @param isFrozen Whether or not the video is frozen + */ + public void notifyVideoFreezeUpdated(boolean isFrozen) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.notifyVideoFreezeUpdated(mToken, isFrozen, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Notifies Interactive APP session when content is allowed. */ public void notifyContentAllowed() { diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 79364034ac2a..5247a0ebe6e0 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -882,6 +882,15 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Called when video becomes frozen or unfrozen. Audio playback will continue while + * video will be frozen to the last frame if {@code true}. + * @param isFrozen Whether or not the video is frozen. + * @hide + */ + public void onVideoFreezeUpdated(boolean isFrozen) { + } + + /** * Called when content is allowed. */ public void onContentAllowed() { @@ -1770,6 +1779,13 @@ public abstract class TvInteractiveAppService extends Service { onVideoUnavailable(reason); } + void notifyVideoFreezeUpdated(boolean isFrozen) { + if (DEBUG) { + Log.d(TAG, "notifyVideoFreezeUpdated (isFrozen=" + isFrozen + ")"); + } + onVideoFreezeUpdated(isFrozen); + } + void notifyContentAllowed() { if (DEBUG) { Log.d(TAG, "notifyContentAllowed"); diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 40a12e4db4cc..5bb61c261ae2 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -719,6 +719,22 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Alerts the TV Interactive app that the video freeze state has been updated. + * If {@code true}, the video is frozen on the last frame while audio playback continues. + * + * @param isFrozen Whether the video is frozen. + * @hide + */ + public void notifyVideoFreezeUpdated(boolean isFrozen) { + if (DEBUG) { + Log.d(TAG, "notifyVideoFreezeUpdated"); + } + if (mSession != null) { + mSession.notifyVideoFreezeUpdated(isFrozen); + } + } + + /** * Sends signing result to related TV interactive app. * * <p>This is used when the corresponding server of the broadcast-independent interactive diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java index 74e5612c0486..4f6ede508f7a 100644 --- a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java +++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java @@ -24,19 +24,16 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; import android.content.res.AssetFileDescriptor; -import android.media.AudioAttributes; -import android.media.AudioFormat; -import android.media.AudioTrack; +import android.media.AudioManager; import android.media.IAudioService; -import android.media.LoudnessCodecConfigurator; -import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener; +import android.media.LoudnessCodecController; +import android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener; import android.media.MediaCodec; import android.media.MediaExtractor; import android.media.MediaFormat; @@ -54,21 +51,19 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.util.List; import java.util.concurrent.Executors; /** - * Unit tests for {@link LoudnessCodecConfigurator} checking the internal interactions with a mocked + * Unit tests for {@link LoudnessCodecController} checking the internal interactions with a mocked * {@link IAudioService} without any real IPC interactions. */ @Presubmit @RunWith(AndroidJUnit4.class) -public class LoudnessCodecConfiguratorTest { +public class LoudnessCodecControllerTest { private static final String TAG = "LoudnessCodecConfiguratorTest"; private static final String TEST_MEDIA_AUDIO_CODEC_PREFIX = "audio/"; @@ -84,76 +79,41 @@ public class LoudnessCodecConfiguratorTest { @Mock private IAudioService mAudioService; - private LoudnessCodecConfigurator mLcc; + private LoudnessCodecController mLcc; + + private int mSessionId; @Before public void setUp() { - mLcc = LoudnessCodecConfigurator.createForTesting(mAudioService, - Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {}); - } - - @Test - @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void setAudioTrack_callsAudioServiceStart() throws Exception { - final AudioTrack track = createAudioTrack(); - final MediaCodec mediaCodec = createAndConfigureMediaCodec(); - - try { - mLcc.addMediaCodec(mediaCodec); - mLcc.setAudioTrack(track); - - verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), - anyList()); - } finally { - mediaCodec.release(); + final Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + final AudioManager audioManager = (AudioManager) context.getSystemService( + AudioManager.class); + mSessionId = 0; + if (audioManager != null) { + mSessionId = audioManager.generateAudioSessionId(); } + mLcc = LoudnessCodecController.createForTesting(mSessionId, + Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() { + }, mAudioService); } @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception { - when(mAudioService.getLoudnessParams(anyInt(), any())).thenReturn(new PersistableBundle()); - final AudioTrack track = createAudioTrack(); - final MediaCodec mediaCodec = createAndConfigureMediaCodec(); - - try { - mLcc.getLoudnessCodecParams(track, mediaCodec); - - verify(mAudioService).getLoudnessParams(eq(track.getPlayerIId()), any()); - } finally { - mediaCodec.release(); - } + public void createLcc_callsAudioServiceStart() throws Exception { + verify(mAudioService).startLoudnessCodecUpdates(eq(mSessionId)); } @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void setAudioTrack_addsAudioServicePiidCodecs() throws Exception { - final AudioTrack track = createAudioTrack(); - final MediaCodec mediaCodec = createAndConfigureMediaCodec(); - - try { - mLcc.addMediaCodec(mediaCodec); - mLcc.setAudioTrack(track); - - verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList()); - } finally { - mediaCodec.release(); - } - } - - @Test - @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void setAudioTrackTwice_ignoresSecondCall() throws Exception { - final AudioTrack track = createAudioTrack(); + public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception { + when(mAudioService.getLoudnessParams(any())).thenReturn(new PersistableBundle()); final MediaCodec mediaCodec = createAndConfigureMediaCodec(); try { mLcc.addMediaCodec(mediaCodec); - mLcc.setAudioTrack(track); - mLcc.setAudioTrack(track); + mLcc.getLoudnessCodecParams(mediaCodec); - verify(mAudioService, times(1)).startLoudnessCodecUpdates(eq(track.getPlayerIId()), - anyList()); + verify(mAudioService).getLoudnessParams(any()); } finally { mediaCodec.release(); } @@ -161,16 +121,14 @@ public class LoudnessCodecConfiguratorTest { @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void setTrackNull_stopCodecUpdates() throws Exception { - final AudioTrack track = createAudioTrack(); + public void release_stopCodecUpdates() throws Exception { final MediaCodec mediaCodec = createAndConfigureMediaCodec(); try { mLcc.addMediaCodec(mediaCodec); - mLcc.setAudioTrack(track); + mLcc.release(); // stops updats - mLcc.setAudioTrack(null); // stops updates - verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId())); + verify(mAudioService).stopLoudnessCodecUpdates(eq(mSessionId)); } finally { mediaCodec.release(); } @@ -204,40 +162,14 @@ public class LoudnessCodecConfiguratorTest { @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void setClearTrack_removeAllAudioServicePiidCodecs() throws Exception { - final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class); - final AudioTrack track = createAudioTrack(); - final MediaCodec mediaCodec1 = createAndConfigureMediaCodec(); - final MediaCodec mediaCodec2 = createAndConfigureMediaCodec(); - - try { - mLcc.addMediaCodec(mediaCodec1); - mLcc.setAudioTrack(track); - verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), - argument.capture()); - assertEquals(argument.getValue().size(), 1); - - mLcc.addMediaCodec(mediaCodec2); - mLcc.setAudioTrack(null); - verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId())); - } finally { - mediaCodec1.release(); - mediaCodec2.release(); - } - } - - @Test - @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) public void removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec() throws Exception { - final AudioTrack track = createAudioTrack(); final MediaCodec mediaCodec = createAndConfigureMediaCodec(); try { mLcc.addMediaCodec(mediaCodec); - mLcc.setAudioTrack(track); mLcc.removeMediaCodec(mediaCodec); - verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any()); + verify(mAudioService).removeLoudnessCodecInfo(eq(mSessionId), any()); } finally { mediaCodec.release(); } @@ -245,37 +177,28 @@ public class LoudnessCodecConfiguratorTest { @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void addMediaCodecAfterSetTrack_callsAudioServiceAdd() throws Exception { - final AudioTrack track = createAudioTrack(); - final MediaCodec mediaCodec1 = createAndConfigureMediaCodec(); - final MediaCodec mediaCodec2 = createAndConfigureMediaCodec(); + public void addMediaCodec_callsAudioServiceAdd() throws Exception { + final MediaCodec mediaCodec = createAndConfigureMediaCodec(); try { - mLcc.addMediaCodec(mediaCodec1); - mLcc.setAudioTrack(track); - verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList()); - - mLcc.addMediaCodec(mediaCodec2); - verify(mAudioService).addLoudnessCodecInfo(eq(track.getPlayerIId()), anyInt(), any()); + mLcc.addMediaCodec(mediaCodec); + verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any()); } finally { - mediaCodec1.release(); - mediaCodec2.release(); + mediaCodec.release(); } } @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void removeMediaCodecAfterSetTrack_callsAudioServiceRemove() throws Exception { - final AudioTrack track = createAudioTrack(); + public void removeMediaCodec_callsAudioServiceRemove() throws Exception { final MediaCodec mediaCodec = createAndConfigureMediaCodec(); try { mLcc.addMediaCodec(mediaCodec); - mLcc.setAudioTrack(track); - verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList()); + verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any()); mLcc.removeMediaCodec(mediaCodec); - verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any()); + verify(mAudioService).removeLoudnessCodecInfo(eq(mSessionId), any()); } finally { mediaCodec.release(); } @@ -283,15 +206,13 @@ public class LoudnessCodecConfiguratorTest { @Test @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API) - public void removeWrongMediaCodecAfterSetTrack_triggersIAE() throws Exception { - final AudioTrack track = createAudioTrack(); + public void removeWrongMediaCodec_triggersIAE() throws Exception { final MediaCodec mediaCodec1 = createAndConfigureMediaCodec(); final MediaCodec mediaCodec2 = createAndConfigureMediaCodec(); try { mLcc.addMediaCodec(mediaCodec1); - mLcc.setAudioTrack(track); - verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList()); + verify(mAudioService).addLoudnessCodecInfo(eq(mSessionId), anyInt(), any()); assertThrows(IllegalArgumentException.class, () -> mLcc.removeMediaCodec(mediaCodec2)); @@ -301,16 +222,6 @@ public class LoudnessCodecConfiguratorTest { } } - private static AudioTrack createAudioTrack() { - return new AudioTrack.Builder() - .setAudioAttributes(new AudioAttributes.Builder().build()) - .setBufferSizeInBytes(TEST_AUDIO_TRACK_BUFFER_SIZE) - .setAudioFormat(new AudioFormat.Builder() - .setChannelMask(TEST_AUDIO_TRACK_CHANNELS) - .setSampleRate(TEST_AUDIO_TRACK_SAMPLERATE).build()) - .build(); - } - private MediaCodec createAndConfigureMediaCodec() throws Exception { AssetFileDescriptor testFd = InstrumentationRegistry.getInstrumentation().getContext() .getResources() @@ -320,7 +231,7 @@ public class LoudnessCodecConfiguratorTest { extractor = new MediaExtractor(); try { extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(), - testFd.getLength()); + testFd.getLength()); assertEquals("wrong number of tracks", 1, extractor.getTrackCount()); MediaFormat format = extractor.getTrackFormat(0); String mime = format.getString(MediaFormat.KEY_MIME); diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 7573474f65b4..1046d8e9aebb 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -72,7 +72,7 @@ package android.nfc { method public void enableReaderMode(android.app.Activity, android.nfc.NfcAdapter.ReaderCallback, int, android.os.Bundle); method public static android.nfc.NfcAdapter getDefaultAdapter(android.content.Context); method @Nullable public android.nfc.NfcAntennaInfo getNfcAntennaInfo(); - method @FlaggedApi("android.nfc.enable_nfc_charging") @Nullable public android.nfc.WlcLDeviceInfo getWlcLDeviceInfo(); + method @FlaggedApi("android.nfc.enable_nfc_charging") @Nullable public android.nfc.WlcListenerDeviceInfo getWlcListenerDeviceInfo(); method public boolean ignore(android.nfc.Tag, int, android.nfc.NfcAdapter.OnTagRemovedListener, android.os.Handler); method public boolean isEnabled(); method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean isObserveModeSupported(); @@ -175,18 +175,18 @@ package android.nfc { ctor public TagLostException(String); } - @FlaggedApi("android.nfc.enable_nfc_charging") public final class WlcLDeviceInfo implements android.os.Parcelable { - ctor public WlcLDeviceInfo(double, double, double, int); + @FlaggedApi("android.nfc.enable_nfc_charging") public final class WlcListenerDeviceInfo implements android.os.Parcelable { + ctor public WlcListenerDeviceInfo(int, double, double, int); method public int describeContents(); - method public double getBatteryLevel(); - method public double getProductId(); + method @FloatRange(from=0.0, to=100.0) public double getBatteryLevel(); + method public int getProductId(); method public int getState(); method public double getTemperature(); method public void writeToParcel(@NonNull android.os.Parcel, int); - field public static final int CONNECTED_CHARGING = 2; // 0x2 - field public static final int CONNECTED_DISCHARGING = 3; // 0x3 - field @NonNull public static final android.os.Parcelable.Creator<android.nfc.WlcLDeviceInfo> CREATOR; - field public static final int DISCONNECTED = 1; // 0x1 + field @NonNull public static final android.os.Parcelable.Creator<android.nfc.WlcListenerDeviceInfo> CREATOR; + field public static final int STATE_CONNECTED_CHARGING = 2; // 0x2 + field public static final int STATE_CONNECTED_DISCHARGING = 3; // 0x3 + field public static final int STATE_DISCONNECTED = 1; // 0x1 } } diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index 40672a1adc15..3524f8cce04c 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -8,7 +8,6 @@ package android.nfc { method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable(); method @FlaggedApi("android.nfc.enable_nfc_reader_option") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableReaderOption(boolean); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableSecureNfc(boolean); - method @FlaggedApi("android.nfc.enable_nfc_charging") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enableWlc(boolean); method @FlaggedApi("android.nfc.enable_nfc_mainline") public int getAdapterState(); method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public java.util.Map<java.lang.String,java.lang.Boolean> getTagIntentAppPreferenceForUser(int); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOn(); @@ -20,6 +19,7 @@ package android.nfc { method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean setControllerAlwaysOn(boolean); method @FlaggedApi("android.nfc.enable_nfc_mainline") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setReaderMode(boolean); method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public int setTagIntentAppPreferenceForUser(int, @NonNull String, boolean); + method @FlaggedApi("android.nfc.enable_nfc_charging") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean setWlcEnabled(boolean); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void unregisterControllerAlwaysOnListener(@NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener); method @FlaggedApi("android.nfc.enable_nfc_charging") public void unregisterWlcStateListener(@NonNull android.nfc.NfcAdapter.WlcStateListener); field @FlaggedApi("android.nfc.enable_nfc_mainline") public static final String ACTION_REQUIRE_UNLOCK_FOR_NFC = "android.nfc.action.REQUIRE_UNLOCK_FOR_NFC"; @@ -37,7 +37,7 @@ package android.nfc { } @FlaggedApi("android.nfc.enable_nfc_charging") public static interface NfcAdapter.WlcStateListener { - method public void onWlcStateChanged(@NonNull android.nfc.WlcLDeviceInfo); + method public void onWlcStateChanged(@NonNull android.nfc.WlcListenerDeviceInfo); } } @@ -45,7 +45,7 @@ package android.nfc { package android.nfc.cardemulation { public final class CardEmulation { - method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public android.nfc.cardemulation.ApduServiceInfo getPreferredPaymentService(); + method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static android.content.ComponentName getPreferredPaymentService(@NonNull android.content.Context); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int); } diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl index bec62c5b1b82..63c3414acc36 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -32,8 +32,8 @@ import android.nfc.ITagRemovedCallback; import android.nfc.INfcDta; import android.nfc.INfcWlcStateListener; import android.nfc.NfcAntennaInfo; +import android.nfc.WlcListenerDeviceInfo; import android.os.Bundle; -import android.nfc.WlcLDeviceInfo; /** * @hide @@ -90,11 +90,11 @@ interface INfcAdapter boolean setObserveMode(boolean enabled); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)") - boolean enableWlc(boolean enable); + boolean setWlcEnabled(boolean enable); boolean isWlcEnabled(); void registerWlcStateListener(in INfcWlcStateListener listener); void unregisterWlcStateListener(in INfcWlcStateListener listener); - WlcLDeviceInfo getWlcLDeviceInfo(); + WlcListenerDeviceInfo getWlcListenerDeviceInfo(); void updateDiscoveryTechnology(IBinder b, int pollFlags, int listenFlags); diff --git a/nfc/java/android/nfc/INfcWlcStateListener.aidl b/nfc/java/android/nfc/INfcWlcStateListener.aidl index c2b7075bc6e4..584eb9a128b4 100644 --- a/nfc/java/android/nfc/INfcWlcStateListener.aidl +++ b/nfc/java/android/nfc/INfcWlcStateListener.aidl @@ -16,7 +16,7 @@ package android.nfc; -import android.nfc.WlcLDeviceInfo; +import android.nfc.WlcListenerDeviceInfo; /** * @hide */ @@ -24,7 +24,7 @@ oneway interface INfcWlcStateListener { /** * Called whenever NFC WLC state changes * - * @param wlcLDeviceInfo NFC wlc listener information + * @param wlcListenerDeviceInfo NFC wlc listener information */ - void onWlcStateChanged(in WlcLDeviceInfo wlcLDeviceInfo); + void onWlcStateChanged(in WlcListenerDeviceInfo wlcListenerDeviceInfo); } diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index 68c16e69f3af..11eb97b440bf 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -2820,13 +2820,12 @@ public final class NfcAdapter { @SystemApi @FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING) @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) - public boolean enableWlc(boolean enable) { + public boolean setWlcEnabled(boolean enable) { if (!sHasNfcWlcFeature) { throw new UnsupportedOperationException(); } try { - return sService.enableWlc(enable); - + return sService.setWlcEnabled(enable); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -2835,7 +2834,7 @@ public final class NfcAdapter { return false; } try { - return sService.enableWlc(enable); + return sService.setWlcEnabled(enable); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } @@ -2887,7 +2886,7 @@ public final class NfcAdapter { /** * Called on NFC WLC state changes */ - void onWlcStateChanged(@NonNull WlcLDeviceInfo wlcLDeviceInfo); + void onWlcStateChanged(@NonNull WlcListenerDeviceInfo wlcListenerDeviceInfo); } /** @@ -2945,12 +2944,12 @@ public final class NfcAdapter { */ @FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING) @Nullable - public WlcLDeviceInfo getWlcLDeviceInfo() { + public WlcListenerDeviceInfo getWlcListenerDeviceInfo() { if (!sHasNfcWlcFeature) { throw new UnsupportedOperationException(); } try { - return sService.getWlcLDeviceInfo(); + return sService.getWlcListenerDeviceInfo(); } catch (RemoteException e) { attemptDeadServiceRecovery(e); // Try one more time @@ -2959,7 +2958,7 @@ public final class NfcAdapter { return null; } try { - return sService.getWlcLDeviceInfo(); + return sService.getWlcListenerDeviceInfo(); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover NFC Service."); } diff --git a/nfc/java/android/nfc/NfcWlcStateListener.java b/nfc/java/android/nfc/NfcWlcStateListener.java index 8d793101f41f..890cb090f587 100644 --- a/nfc/java/android/nfc/NfcWlcStateListener.java +++ b/nfc/java/android/nfc/NfcWlcStateListener.java @@ -36,7 +36,7 @@ public class NfcWlcStateListener extends INfcWlcStateListener.Stub { private final Map<WlcStateListener, Executor> mListenerMap = new HashMap<>(); - private WlcLDeviceInfo mCurrentState = null; + private WlcListenerDeviceInfo mCurrentState = null; private boolean mIsRegistered = false; public NfcWlcStateListener(@NonNull INfcAdapter adapter) { @@ -98,8 +98,10 @@ public class NfcWlcStateListener extends INfcWlcStateListener.Stub { Executor executor = mListenerMap.get(listener); final long identity = Binder.clearCallingIdentity(); try { - executor.execute(() -> listener.onWlcStateChanged( - mCurrentState)); + if (Flags.enableNfcCharging()) { + executor.execute(() -> listener.onWlcStateChanged( + mCurrentState)); + } } finally { Binder.restoreCallingIdentity(identity); } @@ -107,9 +109,9 @@ public class NfcWlcStateListener extends INfcWlcStateListener.Stub { } @Override - public void onWlcStateChanged(@NonNull WlcLDeviceInfo wlcLDeviceInfo) { + public void onWlcStateChanged(@NonNull WlcListenerDeviceInfo wlcListenerDeviceInfo) { synchronized (this) { - mCurrentState = wlcLDeviceInfo; + mCurrentState = wlcListenerDeviceInfo; for (WlcStateListener cb : mListenerMap.keySet()) { sendCurrentState(cb); diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.java b/nfc/java/android/nfc/WlcLDeviceInfo.java deleted file mode 100644 index 016431e90d8e..000000000000 --- a/nfc/java/android/nfc/WlcLDeviceInfo.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.nfc; - -import android.annotation.FlaggedApi; -import android.annotation.NonNull; -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Contains information of the nfc wireless charging listener device information. - */ -@FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING) -public final class WlcLDeviceInfo implements Parcelable { - public static final int DISCONNECTED = 1; - - public static final int CONNECTED_CHARGING = 2; - - public static final int CONNECTED_DISCHARGING = 3; - - private double mProductId; - private double mTemperature; - private double mBatteryLevel; - private int mState; - - public WlcLDeviceInfo(double productId, double temperature, double batteryLevel, int state) { - this.mProductId = productId; - this.mTemperature = temperature; - this.mBatteryLevel = batteryLevel; - this.mState = state; - } - - /** - * ProductId of the WLC listener device. - */ - public double getProductId() { - return mProductId; - } - - /** - * Temperature of the WLC listener device. - */ - public double getTemperature() { - return mTemperature; - } - - /** - * BatteryLevel of the WLC listener device. - */ - public double getBatteryLevel() { - return mBatteryLevel; - } - - /** - * State of the WLC listener device. - */ - public int getState() { - return mState; - } - - private WlcLDeviceInfo(Parcel in) { - this.mProductId = in.readDouble(); - this.mTemperature = in.readDouble(); - this.mBatteryLevel = in.readDouble(); - this.mState = in.readInt(); - } - - public static final @NonNull Parcelable.Creator<WlcLDeviceInfo> CREATOR = - new Parcelable.Creator<WlcLDeviceInfo>() { - @Override - public WlcLDeviceInfo createFromParcel(Parcel in) { - return new WlcLDeviceInfo(in); - } - - @Override - public WlcLDeviceInfo[] newArray(int size) { - return new WlcLDeviceInfo[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeDouble(mProductId); - dest.writeDouble(mTemperature); - dest.writeDouble(mBatteryLevel); - dest.writeDouble(mState); - } -} diff --git a/nfc/java/android/nfc/WlcLDeviceInfo.aidl b/nfc/java/android/nfc/WlcListenerDeviceInfo.aidl index 33143fe81162..7f2ca545007b 100644 --- a/nfc/java/android/nfc/WlcLDeviceInfo.aidl +++ b/nfc/java/android/nfc/WlcListenerDeviceInfo.aidl @@ -16,4 +16,4 @@ package android.nfc; -parcelable WlcLDeviceInfo; +parcelable WlcListenerDeviceInfo; diff --git a/nfc/java/android/nfc/WlcListenerDeviceInfo.java b/nfc/java/android/nfc/WlcListenerDeviceInfo.java new file mode 100644 index 000000000000..45315f812250 --- /dev/null +++ b/nfc/java/android/nfc/WlcListenerDeviceInfo.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc; + +import android.annotation.FlaggedApi; +import android.annotation.FloatRange; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Contains information of the nfc wireless charging listener device information. + */ +@FlaggedApi(Flags.FLAG_ENABLE_NFC_CHARGING) +public final class WlcListenerDeviceInfo implements Parcelable { + /** + * Device is currently not connected with any WlcListenerDevice. + */ + public static final int STATE_DISCONNECTED = 1; + + /** + * Device is currently connected with a WlcListenerDevice and is charging it. + */ + public static final int STATE_CONNECTED_CHARGING = 2; + + /** + * Device is currently connected with a WlcListenerDevice without charging it. + */ + public static final int STATE_CONNECTED_DISCHARGING = 3; + + /** + * Possible states from {@link #getState}. + * @hide + */ + @IntDef(prefix = { "STATE_" }, value = { + STATE_DISCONNECTED, + STATE_CONNECTED_CHARGING, + STATE_CONNECTED_DISCHARGING + }) + @Retention(RetentionPolicy.SOURCE) + public @interface WlcListenerState{} + + private int mProductId; + private double mTemperature; + private double mBatteryLevel; + private int mState; + + /** + * Create a new object containing wlc listener information. + * + * @param productId code for the device vendor + * @param temperature current temperature + * @param batteryLevel current battery level + * @param state current state + */ + public WlcListenerDeviceInfo(int productId, double temperature, double batteryLevel, + @WlcListenerState int state) { + this.mProductId = productId; + this.mTemperature = temperature; + this.mBatteryLevel = batteryLevel; + this.mState = state; + } + + /** + * ProductId of the WLC listener device. + * @return integer that is converted from USI Stylus VendorID[11:0]. + */ + public int getProductId() { + return mProductId; + } + + /** + * Temperature of the WLC listener device. + * @return the value represents the temperature in °C. + */ + public double getTemperature() { + return mTemperature; + } + + /** + * BatteryLevel of the WLC listener device. + * @return battery level in percentage [0-100] + */ + public @FloatRange(from = 0.0, to = 100.0) double getBatteryLevel() { + return mBatteryLevel; + } + + /** + * State of the WLC listener device. + */ + public @WlcListenerState int getState() { + return mState; + } + + private WlcListenerDeviceInfo(Parcel in) { + this.mProductId = in.readInt(); + this.mTemperature = in.readDouble(); + this.mBatteryLevel = in.readDouble(); + this.mState = in.readInt(); + } + + public static final @NonNull Parcelable.Creator<WlcListenerDeviceInfo> CREATOR = + new Parcelable.Creator<WlcListenerDeviceInfo>() { + @Override + public WlcListenerDeviceInfo createFromParcel(Parcel in) { + return new WlcListenerDeviceInfo(in); + } + + @Override + public WlcListenerDeviceInfo[] newArray(int size) { + return new WlcListenerDeviceInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mProductId); + dest.writeDouble(mTemperature); + dest.writeDouble(mBatteryLevel); + dest.writeInt(mState); + } +} diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 0943392a68ad..9d38e4c5b297 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -16,6 +16,7 @@ package android.nfc.cardemulation; +import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -23,6 +24,7 @@ import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; +import android.annotation.UserHandleAware; import android.annotation.UserIdInt; import android.app.Activity; import android.content.ComponentName; @@ -1138,31 +1140,28 @@ public final class CardEmulation { } /** - * Returns the {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT} for the given user. + * Returns the value of {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT}. + * + * @param context A context + * @return A ComponentName for the setting value, or null. * * @hide */ @SystemApi + @UserHandleAware @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @SuppressWarnings("AndroidFrameworkClientSidePermissionCheck") @FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ENABLED) @Nullable - public ApduServiceInfo getPreferredPaymentService() { - try { - return sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return null; - } - try { - return sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return null; - } + public static ComponentName getPreferredPaymentService(@NonNull Context context) { + context.checkCallingOrSelfPermission(Manifest.permission.NFC_PREFERRED_PAYMENT_INFO); + String defaultPaymentComponent = Settings.Secure.getString(context.getContentResolver(), + Constants.SETTINGS_SECURE_NFC_PAYMENT_DEFAULT_COMPONENT); + + if (defaultPaymentComponent == null) { + return null; } - } + return ComponentName.unflattenFromString(defaultPaymentComponent); + } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt index ba8e354fa0c6..b34c310cd30f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/editor/SettingsExposedDropdownMenuCheckBox.kt @@ -54,16 +54,18 @@ fun SettingsExposedDropdownMenuCheckBox( selectedOptionsState: SnapshotStateList<Int>, emptyVal: String = "", enabled: Boolean, + errorMessage: String? = null, onSelectedOptionStateChange: () -> Unit, ) { var dropDownWidth by remember { mutableIntStateOf(0) } var expanded by remember { mutableStateOf(false) } + val allIndex = options.indexOf("*") ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = it }, modifier = Modifier .width(350.dp) - .padding(SettingsDimension.menuFieldPadding) + .padding(SettingsDimension.textFieldPadding) .onSizeChanged { dropDownWidth = it.width }, ) { OutlinedTextField( @@ -72,7 +74,8 @@ fun SettingsExposedDropdownMenuCheckBox( .menuAnchor() .fillMaxWidth(), value = if (selectedOptionsState.size == 0) emptyVal - else selectedOptionsState.joinToString { options[it] }, + else if (selectedOptionsState.contains(allIndex)) "*" + else selectedOptionsState.joinToString { options[it] }, onValueChange = {}, label = { Text(text = label) }, trailingIcon = { @@ -81,7 +84,13 @@ fun SettingsExposedDropdownMenuCheckBox( ) }, readOnly = true, - enabled = enabled + enabled = enabled, + isError = errorMessage != null, + supportingText = { + if (errorMessage != null) { + Text(text = errorMessage) + } + } ) if (options.isNotEmpty()) { ExposedDropdownMenu( @@ -98,9 +107,17 @@ fun SettingsExposedDropdownMenuCheckBox( .fillMaxWidth(), onClick = { if (selectedOptionsState.contains(index)) { - selectedOptionsState.remove( - index - ) + if (index == allIndex) + selectedOptionsState.clear() + else { + selectedOptionsState.remove( + index + ) + if (selectedOptionsState.contains(allIndex)) + selectedOptionsState.remove( + allIndex + ) + } } else { selectedOptionsState.add( index diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java index 5d520ce5d81f..7e2d0af5c075 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java @@ -21,6 +21,8 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import androidx.annotation.NonNull; + /** * A class for applying config changes and determing if doing so resulting in any "interesting" * changes. @@ -48,8 +50,15 @@ public class InterestingConfigChanges { */ @SuppressLint("NewApi") public boolean applyNewConfig(Resources res) { + return applyNewConfig(res.getConfiguration()); + } + + /** + * Applies the given config change and returns whether an "interesting" change happened. + */ + public boolean applyNewConfig(@NonNull Configuration configuration) { int configChanges = mLastConfiguration.updateFrom( - Configuration.generateDelta(mLastConfiguration, res.getConfiguration())); + Configuration.generateDelta(mLastConfiguration, configuration)); return (configChanges & (mFlags)) != 0; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 57012aabb123..931a6f149b84 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -3,17 +3,17 @@ */ /* 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. -*/ + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.settingslib.bluetooth; @@ -23,6 +23,7 @@ import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; +import android.bluetooth.BluetoothCsipSetCoordinator; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; @@ -30,6 +31,7 @@ import android.content.Context; import android.os.Build; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -57,13 +59,12 @@ public class LeAudioProfile implements LocalBluetoothProfile { private static final int ORDINAL = 1; // These callbacks run on the main thread. - private final class LeAudioServiceListener - implements BluetoothProfile.ServiceListener { + private final class LeAudioServiceListener implements BluetoothProfile.ServiceListener { @RequiresApi(Build.VERSION_CODES.S) public void onServiceConnected(int profile, BluetoothProfile proxy) { if (DEBUG) { - Log.d(TAG,"Bluetooth service connected"); + Log.d(TAG, "Bluetooth service connected"); } mService = (BluetoothLeAudio) proxy; // We just bound to the service, so refresh the UI for any connected LeAudio devices. @@ -78,8 +79,7 @@ public class LeAudioProfile implements LocalBluetoothProfile { } device = mDeviceManager.addDevice(nextDevice); } - device.onProfileStateChanged(LeAudioProfile.this, - BluetoothProfile.STATE_CONNECTED); + device.onProfileStateChanged(LeAudioProfile.this, BluetoothProfile.STATE_CONNECTED); device.refresh(); } @@ -89,7 +89,7 @@ public class LeAudioProfile implements LocalBluetoothProfile { public void onServiceDisconnected(int profile) { if (DEBUG) { - Log.d(TAG,"Bluetooth service disconnected"); + Log.d(TAG, "Bluetooth service disconnected"); } mProfileManager.callServiceDisconnectedListeners(); mIsProfileReady = false; @@ -105,7 +105,9 @@ public class LeAudioProfile implements LocalBluetoothProfile { return BluetoothProfile.LE_AUDIO; } - LeAudioProfile(Context context, CachedBluetoothDeviceManager deviceManager, + LeAudioProfile( + Context context, + CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { mContext = context; mDeviceManager = deviceManager; @@ -113,8 +115,7 @@ public class LeAudioProfile implements LocalBluetoothProfile { mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mBluetoothAdapter.getProfileProxy( - context, new LeAudioServiceListener(), - BluetoothProfile.LE_AUDIO); + context, new LeAudioServiceListener(), BluetoothProfile.LE_AUDIO); } public boolean accessProfileEnabled() { @@ -126,18 +127,22 @@ public class LeAudioProfile implements LocalBluetoothProfile { } public List<BluetoothDevice> getConnectedDevices() { - return getDevicesByStates(new int[] { - BluetoothProfile.STATE_CONNECTED, - BluetoothProfile.STATE_CONNECTING, - BluetoothProfile.STATE_DISCONNECTING}); + return getDevicesByStates( + new int[] { + BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_CONNECTING, + BluetoothProfile.STATE_DISCONNECTING + }); } public List<BluetoothDevice> getConnectableDevices() { - return getDevicesByStates(new int[] { - BluetoothProfile.STATE_DISCONNECTED, - BluetoothProfile.STATE_CONNECTED, - BluetoothProfile.STATE_CONNECTING, - BluetoothProfile.STATE_DISCONNECTING}); + return getDevicesByStates( + new int[] { + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.STATE_CONNECTED, + BluetoothProfile.STATE_CONNECTING, + BluetoothProfile.STATE_DISCONNECTING + }); } private List<BluetoothDevice> getDevicesByStates(int[] states) { @@ -148,8 +153,8 @@ public class LeAudioProfile implements LocalBluetoothProfile { } /* - * @hide - */ + * @hide + */ public boolean connect(BluetoothDevice device) { if (mService == null) { return false; @@ -158,8 +163,8 @@ public class LeAudioProfile implements LocalBluetoothProfile { } /* - * @hide - */ + * @hide + */ public boolean disconnect(BluetoothDevice device) { if (mService == null) { return false; @@ -174,6 +179,14 @@ public class LeAudioProfile implements LocalBluetoothProfile { return mService.getConnectionState(device); } + /** Get group id for {@link BluetoothDevice}. */ + public int getGroupId(@NonNull BluetoothDevice device) { + if (mService == null) { + return BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + } + return mService.getGroupId(device); + } + public boolean setActiveDevice(BluetoothDevice device) { if (mBluetoothAdapter == null) { return false; @@ -193,29 +206,28 @@ public class LeAudioProfile implements LocalBluetoothProfile { /** * Get Lead device for the group. * - * Lead device is the device that can be used as an active device in the system. - * Active devices points to the Audio Device for the Le Audio group. - * This method returns the Lead devices for the connected LE Audio - * group and this device should be used in the setActiveDevice() method by other parts - * of the system, which wants to set to active a particular Le Audio group. + * <p>Lead device is the device that can be used as an active device in the system. Active + * devices points to the Audio Device for the Le Audio group. This method returns the Lead + * devices for the connected LE Audio group and this device should be used in the + * setActiveDevice() method by other parts of the system, which wants to set to active a + * particular Le Audio group. * - * Note: getActiveDevice() returns the Lead device for the currently active LE Audio group. + * <p>Note: getActiveDevice() returns the Lead device for the currently active LE Audio group. * Note: When Lead device gets disconnected while Le Audio group is active and has more devices - * in the group, then Lead device will not change. If Lead device gets disconnected, for the - * Le Audio group which is not active, a new Lead device will be chosen + * in the group, then Lead device will not change. If Lead device gets disconnected, for the Le + * Audio group which is not active, a new Lead device will be chosen * * @param groupId The group id. * @return group lead device. - * * @hide */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) { if (DEBUG) { - Log.d(TAG,"getConnectedGroupLeadDevice"); + Log.d(TAG, "getConnectedGroupLeadDevice"); } if (mService == null) { - Log.e(TAG,"No service."); + Log.e(TAG, "No service."); return null; } return mService.getConnectedGroupLeadDevice(groupId); @@ -310,10 +322,10 @@ public class LeAudioProfile implements LocalBluetoothProfile { } if (mService != null) { try { - BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.LE_AUDIO, - mService); + BluetoothAdapter.getDefaultAdapter() + .closeProfileProxy(BluetoothProfile.LE_AUDIO, mService); mService = null; - }catch (Throwable t) { + } catch (Throwable t) { Log.w(TAG, "Error cleaning up LeAudio proxy", t); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index de21c541c7e4..934870507a20 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -84,6 +84,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO), Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE), Settings.Secure.getUriFor(Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME), + Settings.Secure.getUriFor( + Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY), }; private BluetoothLeBroadcast mServiceBroadcast; @@ -96,6 +98,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { private String mNewAppSourceName = ""; private boolean mIsBroadcastProfileReady = false; private boolean mIsBroadcastAssistantProfileReady = false; + private boolean mImproveCompatibility = false; private String mProgramInfo; private byte[] mBroadcastCode; private Executor mExecutor; @@ -391,6 +394,52 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { * <p>If the system started the LE Broadcast, then the system calls the corresponding callback * {@link BluetoothLeBroadcast.Callback}. */ + public void startPrivateBroadcast() { + mNewAppSourceName = "Sharing audio"; + if (mServiceBroadcast == null) { + Log.d(TAG, "The BluetoothLeBroadcast is null when starting the private broadcast."); + return; + } + if (mServiceBroadcast.getAllBroadcastMetadata().size() + >= mServiceBroadcast.getMaximumNumberOfBroadcasts()) { + Log.d(TAG, "Skip starting the broadcast due to number limit."); + return; + } + String programInfo = getProgramInfo(); + boolean improveCompatibility = getImproveCompatibility(); + if (DEBUG) { + Log.d( + TAG, + "startBroadcast: language = null , programInfo = " + + programInfo + + ", improveCompatibility = " + + improveCompatibility); + } + // Current broadcast framework only support one subgroup + BluetoothLeBroadcastSubgroupSettings subgroupSettings = + buildBroadcastSubgroupSettings( + /* language= */ null, programInfo, improveCompatibility); + BluetoothLeBroadcastSettings settings = + buildBroadcastSettings( + true, // TODO: set to false after framework fix + TextUtils.isEmpty(programInfo) ? null : programInfo, + (mBroadcastCode != null && mBroadcastCode.length > 0) + ? mBroadcastCode + : null, + ImmutableList.of(subgroupSettings)); + mServiceBroadcast.startBroadcast(settings); + } + + /** + * Start the private Broadcast for personal audio sharing or qr code sharing. + * + * <p>The broadcast will use random string for both broadcast name and subgroup program info; + * The broadcast will use random string for broadcast code; The broadcast will only have one + * subgroup due to system limitation; The subgroup language will be null. + * + * <p>If the system started the LE Broadcast, then the system calls the corresponding callback + * {@link BluetoothLeBroadcast.Callback}. + */ public void startPrivateBroadcast(int quality) { mNewAppSourceName = "Sharing audio"; if (mServiceBroadcast == null) { @@ -408,7 +457,11 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } // Current broadcast framework only support one subgroup BluetoothLeBroadcastSubgroupSettings subgroupSettings = - buildBroadcastSubgroupSettings(/* language= */ null, programInfo, quality); + buildBroadcastSubgroupSettings( + /* language= */ null, + programInfo, + /* improveCompatibility= */ + BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD == quality); BluetoothLeBroadcastSettings settings = buildBroadcastSettings( true, // TODO: set to false after framework fix @@ -437,7 +490,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } private BluetoothLeBroadcastSubgroupSettings buildBroadcastSubgroupSettings( - @Nullable String language, @Nullable String programInfo, int quality) { + @Nullable String language, @Nullable String programInfo, boolean improveCompatibility) { BluetoothLeAudioContentMetadata metadata = new BluetoothLeAudioContentMetadata.Builder() .setLanguage(language) @@ -447,7 +500,10 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { // metadata to keep legacy UI working. mBluetoothLeAudioContentMetadata = metadata; return new BluetoothLeBroadcastSubgroupSettings.Builder() - .setPreferredQuality(quality) + .setPreferredQuality( + improveCompatibility + ? BluetoothLeBroadcastSubgroupSettings.QUALITY_STANDARD + : BluetoothLeBroadcastSubgroupSettings.QUALITY_HIGH) .setContentMetadata(mBluetoothLeAudioContentMetadata) .build(); } @@ -513,6 +569,36 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } } + /** Get compatibility config for broadcast. */ + public boolean getImproveCompatibility() { + return mImproveCompatibility; + } + + /** Set compatibility config for broadcast. */ + public void setImproveCompatibility(boolean improveCompatibility) { + setImproveCompatibility(improveCompatibility, /* updateContentResolver= */ true); + } + + private void setImproveCompatibility( + boolean improveCompatibility, boolean updateContentResolver) { + if (mImproveCompatibility == improveCompatibility) { + Log.d(TAG, "setImproveCompatibility: improveCompatibility is not changed"); + return; + } + mImproveCompatibility = improveCompatibility; + if (updateContentResolver) { + if (mContentResolver == null) { + Log.d(TAG, "mContentResolver is null"); + return; + } + Log.d(TAG, "Set improveCompatibility to: " + improveCompatibility); + Settings.Secure.putString( + mContentResolver, + Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, + improveCompatibility ? "1" : "0"); + } + } + private void setLatestBroadcastId(int broadcastId) { Log.d(TAG, "setLatestBroadcastId: mBroadcastId is " + broadcastId); mBroadcastId = broadcastId; @@ -600,6 +686,14 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Settings.Secure.getString( mContentResolver, Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME); setAppSourceName(appSourceName, /* updateContentResolver= */ false); + + String improveCompatibility = + Settings.Secure.getString( + mContentResolver, + Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY); + setImproveCompatibility( + improveCompatibility == null ? false : improveCompatibility.equals("1"), + /* updateContentResolver= */ false); } private void updateBroadcastInfoFromBroadcastMetadata( diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java index d6f1eab442fa..15f33d2cff42 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -229,6 +229,17 @@ public class PhoneMediaDevice extends MediaDevice { @SuppressWarnings("NewApi") @Override public String getId() { + if (com.android.media.flags.Flags.enableAudioPoliciesDeviceAndBluetoothController()) { + // Note: be careful when removing this flag. Instead of just removing it, you might want + // to replace it with SDK_INT >= 35. Explanation: The presence of SDK checks in settings + // lib suggests that a mainline component may depend on this code. Which means removing + // this "if" (and using always the route info id) could mean a regression on mainline + // code running on a device that's running API 34 or older. Unfortunately, we cannot + // check the API level at the moment of writing this code because the API level has not + // been bumped, yet. + return mRouteInfo.getId(); + } + String id; switch (mRouteInfo.getType()) { case TYPE_WIRED_HEADSET: diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java index 1746befbfa4d..ceba9be70487 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java @@ -31,10 +31,15 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.media.MediaRoute2Info; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import com.android.media.flags.Flags; import com.android.settingslib.R; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -45,6 +50,8 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class PhoneMediaDeviceTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private MediaRoute2Info mInfo; @@ -110,8 +117,18 @@ public class PhoneMediaDeviceTest { .isEqualTo(mContext.getString(R.string.media_transfer_this_device_name)); } + @EnableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) + @Test + public void getId_whenAdvancedWiredRoutingEnabled_returnCorrectId() { + String fakeId = "foo"; + when(mInfo.getId()).thenReturn(fakeId); + + assertThat(mPhoneMediaDevice.getId()).isEqualTo(fakeId); + } + + @DisableFlags(Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER) @Test - public void getId_returnCorrectId() { + public void getId_whenAdvancedWiredRoutingDisabled_returnCorrectId() { when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES); assertThat(mPhoneMediaDevice.getId()) diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 2d442f4c0e6e..3a46f4e96ccb 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -406,7 +406,7 @@ public class SettingsProvider extends ContentProvider { Process.THREAD_PRIORITY_BACKGROUND); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); - mSettingsRegistry = new SettingsRegistry(); + mSettingsRegistry = new SettingsRegistry(mHandlerThread.getLooper()); } SettingsState.cacheSystemPackageNamesAndSystemSignature(getContext()); synchronized (mLock) { @@ -2896,8 +2896,8 @@ public class SettingsProvider extends ContentProvider { private String mSettingsCreationBuildId; - public SettingsRegistry() { - mHandler = new MyHandler(getContext().getMainLooper()); + SettingsRegistry(Looper looper) { + mHandler = new MyHandler(looper); mGenerationRegistry = new GenerationRegistry(UserManager.getMaxSupportedUsers()); mBackupManager = new BackupManager(getContext()); } diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index d61ae7eccc42..80656e9253db 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -361,6 +361,7 @@ android_library { "androidx.test.ext.junit", "androidx.test.ext.truth", "kotlin-test", + "SystemUICustomizationTestUtils", ], libs: [ "android.test.runner", @@ -439,6 +440,7 @@ android_robolectric_test { "androidx.test.ext.junit", "inline-mockito-robolectric-prebuilt", "platform-parametric-runner-lib", + "SystemUICustomizationTestUtils", ], libs: [ "android.test.runner", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 323613077a70..2c35c777ab12 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -89,6 +89,13 @@ flag { } flag { + name: "notification_avalanche_suppression" + namespace: "systemui" + description: "After notification avalanche floodgate event, suppress HUNs completely." + bug: "321089634" +} + +flag { name: "notification_background_tint_optimization" namespace: "systemui" description: "Re-enable the codepath that removed tinting of notifications when the" diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 2052e2c01410..a7557d8880e3 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -17,6 +17,7 @@ package com.android.systemui.compose +import android.app.Dialog import android.content.Context import android.view.View import android.view.WindowInsets @@ -26,11 +27,13 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow @@ -78,6 +81,13 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } + override fun createStickyKeysDialog( + dialogFactory: SystemUIDialogFactory, + viewModel: StickyKeysIndicatorViewModel + ): Dialog { + throwComposeUnavailableError() + } + override fun createCommunalView( context: Context, viewModel: BaseCommunalViewModel, diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index b607d596390d..d63939d3b699 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -16,6 +16,7 @@ package com.android.systemui.compose +import android.app.Dialog import android.content.Context import android.graphics.Point import android.view.View @@ -38,6 +39,8 @@ import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalHub import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyboard.stickykeys.ui.view.StickyKeysIndicator +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -47,6 +50,8 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.statusbar.phone.create import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -120,6 +125,13 @@ object ComposeFacade : BaseComposeFacade { } } + override fun createStickyKeysDialog( + dialogFactory: SystemUIDialogFactory, + viewModel: StickyKeysIndicatorViewModel + ): Dialog { + return dialogFactory.create { StickyKeysIndicator(viewModel) } + } + override fun createCommunalView( context: Context, viewModel: BaseCommunalViewModel, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt new file mode 100644 index 000000000000..68e57b5d51b8 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.ui.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel + +@Composable +fun StickyKeysIndicator(viewModel: StickyKeysIndicatorViewModel) { + val stickyKeys by viewModel.indicatorContent.collectAsState(emptyMap()) + StickyKeysIndicator(stickyKeys) +} + +@Composable +fun StickyKeysIndicator(stickyKeys: Map<ModifierKey, Locked>, modifier: Modifier = Modifier) { + Surface( + color = MaterialTheme.colorScheme.surface, + shape = MaterialTheme.shapes.medium, + modifier = modifier + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + stickyKeys.forEach { (key, isLocked) -> + key(key) { + Text( + text = key.text, + fontWeight = if (isLocked.locked) FontWeight.Bold else FontWeight.Normal + ) + } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index 9778e53d8f69..c027c499c0b7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -16,17 +16,16 @@ package com.android.systemui.qs.ui.composable -import android.view.ContextThemeWrapper import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -53,14 +52,6 @@ object QuickSettings { } } -@Composable -private fun QuickSettingsTheme(content: @Composable () -> Unit) { - val context = LocalContext.current - val themedContext = - remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) } - CompositionLocalProvider(LocalContext provides themedContext) { content() } -} - private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State { return when (val transitionState = layoutState.transitionState) { is TransitionState.Idle -> { @@ -115,6 +106,7 @@ private fun QuickSettingsContent( modifier: Modifier = Modifier, ) { val qsView by qsSceneAdapter.qsView.collectAsState(null) + val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState() QuickSettingsTheme { val context = LocalContext.current @@ -124,14 +116,27 @@ private fun QuickSettingsContent( } } qsView?.let { view -> - AndroidView( - modifier = modifier.fillMaxSize().background(colorAttr(R.attr.underSurface)), - factory = { _ -> - qsSceneAdapter.setState(state) - view - }, - update = { qsSceneAdapter.setState(state) } - ) + Box( + modifier = + modifier + .fillMaxWidth() + .then( + if (isCustomizing) { + Modifier.fillMaxHeight() + } else { + Modifier.wrapContentHeight() + } + ) + ) { + AndroidView( + modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)), + factory = { _ -> + qsSceneAdapter.setState(state) + view + }, + update = { qsSceneAdapter.setState(state) } + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index d8c7290b76b8..bbfe0fda049a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -24,31 +24,44 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background +import androidx.compose.foundation.clipScrollableContainer +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.TransitionState import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.battery.BatteryMeterViewController +import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace +import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.toTransitionSceneKey import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.Shade @@ -105,57 +118,120 @@ private fun SceneScope.QuickSettingsScene( ) { // TODO(b/280887232): implement the real UI. Box(modifier = modifier.fillMaxSize()) { - Box(modifier = Modifier.fillMaxSize()) { - val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() - val collapsedHeaderHeight = - with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() } - Spacer( - modifier = - Modifier.element(Shade.Elements.ScrimBackground) - .fillMaxSize() - .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim) - ) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 48.dp) - ) { - when (LocalWindowSizeClass.current.widthSizeClass) { - WindowWidthSizeClass.Compact -> - AnimatedVisibility( - visible = !isCustomizing, - enter = - expandVertically( - animationSpec = tween(1000), - initialHeight = { collapsedHeaderHeight }, - ) + fadeIn(tween(1000)), - exit = - shrinkVertically( - animationSpec = tween(1000), - targetHeight = { collapsedHeaderHeight }, - shrinkTowards = Alignment.Top, - ) + fadeOut(tween(1000)), - ) { - ExpandedShadeHeader( + val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() + val collapsedHeaderHeight = + with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() } + val lifecycleOwner = LocalLifecycleOwner.current + val footerActionsViewModel = + remember(lifecycleOwner, viewModel) { + viewModel.getFooterActionsViewModel(lifecycleOwner) + } + val scrollState = rememberScrollState() + // When animating into the scene, we don't want it to be able to scroll, as it could mess + // up with the expansion animation. + val isScrollable = + when (val state = layoutState.transitionState) { + is TransitionState.Idle -> true + is TransitionState.Transition -> { + state.fromScene == SceneKey.QuickSettings.toTransitionSceneKey() + } + } + + LaunchedEffect(isCustomizing, scrollState) { + if (isCustomizing) { + scrollState.scrollTo(0) + } + } + + // This is the background for the whole scene, as the elements don't necessarily provide + // a background that extends to the edges. + Spacer( + modifier = + Modifier.element(Shade.Elements.ScrimBackground) + .fillMaxSize() + .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim) + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier.fillMaxSize() + // bottom should be tied to insets + .padding(bottom = 16.dp) + ) { + Box(modifier = Modifier.fillMaxSize().weight(1f)) { + val shadeHeaderAndQuickSettingsModifier = + if (isCustomizing) { + Modifier.fillMaxHeight().align(Alignment.TopCenter) + } else { + Modifier.verticalNestedScrollToScene() + .verticalScroll( + scrollState, + enabled = isScrollable, + ) + .clipScrollableContainer(Orientation.Horizontal) + .fillMaxWidth() + .wrapContentHeight(unbounded = true) + .align(Alignment.TopCenter) + } + + Column( + modifier = shadeHeaderAndQuickSettingsModifier, + ) { + when (LocalWindowSizeClass.current.widthSizeClass) { + WindowWidthSizeClass.Compact -> + AnimatedVisibility( + visible = !isCustomizing, + enter = + expandVertically( + animationSpec = tween(100), + initialHeight = { collapsedHeaderHeight }, + ) + fadeIn(tween(100)), + exit = + shrinkVertically( + animationSpec = tween(100), + targetHeight = { collapsedHeaderHeight }, + shrinkTowards = Alignment.Top, + ) + fadeOut(tween(100)), + ) { + ExpandedShadeHeader( + viewModel = viewModel.shadeHeaderViewModel, + createTintedIconManager = createTintedIconManager, + createBatteryMeterViewController = + createBatteryMeterViewController, + statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + else -> + CollapsedShadeHeader( viewModel = viewModel.shadeHeaderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = 16.dp), ) - } - else -> - CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, - ) + } + Spacer(modifier = Modifier.height(16.dp)) + // This view has its own horizontal padding + QuickSettings( + modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"), + viewModel.qsSceneAdapter, + ) + } + } + AnimatedVisibility( + visible = !isCustomizing, + modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth() + ) { + QuickSettingsTheme { + // This view has its own horizontal padding + // TODO(b/321716470) This should use a lifecycle tied to the scene. + FooterActions( + viewModel = footerActionsViewModel, + qsVisibilityLifecycleOwner = lifecycleOwner, + modifier = Modifier.element(QuickSettings.Elements.FooterActions) + ) } - Spacer(modifier = Modifier.height(16.dp)) - QuickSettings( - modifier = Modifier.fillMaxHeight(), - viewModel.qsSceneAdapter, - ) } } HeadsUpNotificationSpace( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt new file mode 100644 index 000000000000..87b6f95b0ae6 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.composable + +import android.view.ContextThemeWrapper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.android.systemui.res.R + +@Composable +fun QuickSettingsTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + val themedContext = + remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) } + CompositionLocalProvider(LocalContext provides themedContext) { content() } +} diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp index 1d1849680040..81b5bd43bdbd 100644 --- a/packages/SystemUI/customization/Android.bp +++ b/packages/SystemUI/customization/Android.bp @@ -34,15 +34,19 @@ android_library { "PluginCoreLib", "SystemUIPluginLib", "SystemUIUnfoldLib", - "androidx.dynamicanimation_dynamicanimation", + "kotlinx_coroutines", + "dagger2", + "jsr330", + ], + libs: [ + // Keep android-specific libraries as libs instead of static_libs, so that they don't break + // things when included as transitive dependencies in robolectric targets. "androidx.concurrent_concurrent-futures", + "androidx.dynamicanimation_dynamicanimation", "androidx.lifecycle_lifecycle-runtime-ktx", "androidx.lifecycle_lifecycle-viewmodel-ktx", "androidx.recyclerview_recyclerview", "kotlinx_coroutines_android", - "kotlinx_coroutines", - "dagger2", - "jsr330", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt index ccf119ab3088..97c407cb9b16 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/FingerprintPropertyInteractorTest.kt @@ -41,10 +41,10 @@ import org.junit.runner.RunWith class FingerprintPropertyInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest = kosmos.fingerprintPropertyInteractor - private val repository = kosmos.fingerprintPropertyRepository - private val configurationRepository = kosmos.fakeConfigurationRepository - private val displayRepository = kosmos.displayRepository + private val underTest by lazy { kosmos.fingerprintPropertyInteractor } + private val repository by lazy { kosmos.fingerprintPropertyRepository } + private val configurationRepository by lazy { kosmos.fakeConfigurationRepository } + private val displayRepository by lazy { kosmos.displayRepository } @Test fun sensorLocation_resolution1f() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt index c8560c31cdf1..c300e0a905b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt @@ -55,7 +55,9 @@ class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() { @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor - private val mainHandler = FakeHandler(Looper.getMainLooper()) + private val mainHandler by lazy { + FakeHandler(Looper.getMainLooper()) + } private lateinit var underTest: PrimaryBouncerInteractor @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index 27b84b2ffabc..d30e33332926 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -39,8 +39,8 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val bouncerInteractor = kosmos.bouncerInteractor - private val underTest = + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private val underTest by lazy { PinBouncerViewModel( applicationContext = context, viewModelScope = testScope.backgroundScope, @@ -49,6 +49,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, ) + } @Test fun animateFailure() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index cfe8c5d52c18..73db1757c06a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -58,8 +58,8 @@ class BouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val authenticationInteractor = kosmos.authenticationInteractor - private val bouncerInteractor = kosmos.bouncerInteractor + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private lateinit var underTest: BouncerViewModel @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt index a0c2acc31589..cddbd1f78dd5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt @@ -66,7 +66,9 @@ class KeyguardBouncerViewModelTest : SysuiTestCase() { @Mock private lateinit var faceAuthInteractor: DeviceEntryFaceAuthInteractor lateinit var bouncerInteractor: PrimaryBouncerInteractor - private val mainHandler = FakeHandler(Looper.getMainLooper()) + private val mainHandler by lazy { + FakeHandler(Looper.getMainLooper()) + } val repository = FakeKeyguardBouncerRepository() lateinit var underTest: KeyguardBouncerViewModel diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index b3b6457b46e7..c6d612d5dc79 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -52,17 +52,18 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val authenticationInteractor = kosmos.authenticationInteractor - private val sceneInteractor = kosmos.sceneInteractor - private val bouncerInteractor = kosmos.bouncerInteractor - private val bouncerViewModel = kosmos.bouncerViewModel + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private val isInputEnabled = MutableStateFlow(true) - private val underTest = + private val underTest by lazy { PasswordBouncerViewModel( viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled.asStateFlow(), ) + } @Before fun setUp() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index c2680bcc82a3..725bdbd43445 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -53,17 +53,18 @@ class PatternBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val authenticationInteractor = kosmos.authenticationInteractor - private val sceneInteractor = kosmos.sceneInteractor - private val bouncerInteractor = kosmos.bouncerInteractor - private val bouncerViewModel = kosmos.bouncerViewModel - private val underTest = + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private val bouncerViewModel by lazy { kosmos.bouncerViewModel } + private val underTest by lazy { PatternBouncerViewModel( applicationContext = context, viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), ) + } private val containerSize = 90 // px private val dotSize = 30 // px diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 1d660d63710d..06e12586d384 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -53,22 +53,24 @@ class PinBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor - private val authenticationInteractor = kosmos.authenticationInteractor - private val bouncerInteractor = kosmos.bouncerInteractor - private val bouncerViewModel = kosmos.bouncerViewModel - private val underTest = - PinBouncerViewModel( - applicationContext = context, - viewModelScope = testScope.backgroundScope, - interactor = bouncerInteractor, - isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = kosmos.simBouncerInteractor, - authenticationMethod = AuthenticationMethodModel.Pin, - ) + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private val bouncerViewModel by lazy { kosmos.bouncerViewModel } + private lateinit var underTest: PinBouncerViewModel @Before fun setUp() { + underTest = + PinBouncerViewModel( + applicationContext = context, + viewModelScope = testScope.backgroundScope, + interactor = bouncerInteractor, + isInputEnabled = MutableStateFlow(true).asStateFlow(), + simBouncerInteractor = kosmos.simBouncerInteractor, + authenticationMethod = AuthenticationMethodModel.Pin, + ) + overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN) overrideResource(R.string.kg_wrong_pin, WRONG_PIN) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt index ed29aa4ac202..e8216735fb5d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt @@ -27,7 +27,6 @@ import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepositor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.communalInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 21190608e002..86279ef24ca7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.domain.interactor import android.app.smartspace.SmartspaceTarget +import android.content.pm.UserInfo import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -42,12 +43,12 @@ import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.communalInteractor -import com.android.systemui.keyguard.domain.interactor.editWidgetsActivityStarter import com.android.systemui.kosmos.testScope import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -59,8 +60,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations /** * This class of test cases assume that communal is enabled. For disabled cases, see @@ -70,6 +73,9 @@ import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class CommunalInteractorTest : SysuiTestCase() { + @Mock private lateinit var mainUser: UserInfo + @Mock private lateinit var secondaryUser: UserInfo + private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -78,6 +84,7 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var mediaRepository: FakeCommunalMediaRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository + private lateinit var userRepository: FakeUserRepository private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter @@ -86,15 +93,22 @@ class CommunalInteractorTest : SysuiTestCase() { @Before fun setUp() { + MockitoAnnotations.initMocks(this) + tutorialRepository = kosmos.fakeCommunalTutorialRepository communalRepository = kosmos.fakeCommunalRepository mediaRepository = kosmos.fakeCommunalMediaRepository widgetRepository = kosmos.fakeCommunalWidgetRepository smartspaceRepository = kosmos.fakeSmartspaceRepository + userRepository = kosmos.fakeUserRepository keyguardRepository = kosmos.fakeKeyguardRepository editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter communalPrefsRepository = kosmos.fakeCommunalPrefsRepository + whenever(mainUser.isMain).thenReturn(true) + whenever(secondaryUser.isMain).thenReturn(false) + userRepository.setUserInfos(listOf(mainUser, secondaryUser)) + underTest = kosmos.communalInteractor } @@ -103,36 +117,52 @@ class CommunalInteractorTest : SysuiTestCase() { testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() } @Test - fun isCommunalAvailable_trueWhenStorageUnlock() = + fun isCommunalAvailable_storageUnlockedAndMainUser_true() = testScope.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(mainUser) runCurrent() assertThat(isAvailable).isTrue() } @Test - fun isCommunalAvailable_whenStorageUnlock_true() = + fun isCommunalAvailable_storageLockedAndMainUser_false() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(true) + userRepository.setSelectedUserInfo(mainUser) + runCurrent() + + assertThat(isAvailable).isFalse() + } + + @Test + fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() = testScope.runTest { val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(secondaryUser) runCurrent() - assertThat(isAvailable).isTrue() + assertThat(isAvailable).isFalse() } @Test - fun updateAppWidgetHostActive_uponStorageUnlock_true() = + fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = testScope.runTest { collectLastValue(underTest.isCommunalAvailable) assertThat(widgetRepository.isHostActive()).isFalse() keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(mainUser) runCurrent() assertThat(widgetRepository.isHostActive()).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 161356d1bed2..352463f5a198 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.communal.domain.interactor +import android.content.pm.UserInfo import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED @@ -30,9 +31,9 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.testScope -import com.android.systemui.settings.UserTracker import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.mock +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -45,15 +46,17 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalTutorialInteractorTest : SysuiTestCase() { + @Mock lateinit var user: UserInfo + private val kosmos = testKosmos() private val testScope = kosmos.testScope - @Mock private lateinit var userTracker: UserTracker - private lateinit var underTest: CommunalTutorialInteractor private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var communalTutorialRepository: FakeCommunalTutorialRepository private lateinit var communalRepository: FakeCommunalRepository + private lateinit var communalInteractor: CommunalInteractor + private lateinit var userRepository: FakeUserRepository @Before fun setUp() { @@ -62,16 +65,17 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { keyguardRepository = kosmos.fakeKeyguardRepository communalTutorialRepository = kosmos.fakeCommunalTutorialRepository communalRepository = kosmos.fakeCommunalRepository + communalInteractor = kosmos.communalInteractor + userRepository = kosmos.fakeUserRepository underTest = kosmos.communalTutorialInteractor - - whenever(userTracker.userHandle).thenReturn(mock()) } @Test fun tutorialUnavailable_whenKeyguardNotVisible() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) keyguardRepository.setKeyguardShowing(false) assertThat(isTutorialAvailable).isFalse() @@ -81,6 +85,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { fun tutorialUnavailable_whenTutorialIsCompleted() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) communalRepository.setIsCommunalHubShowing(false) @@ -89,9 +94,20 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { } @Test + fun tutorialUnavailable_whenCommunalNotAvailable() = + testScope.runTest { + val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(false) + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED) + keyguardRepository.setKeyguardShowing(true) + assertThat(isTutorialAvailable).isFalse() + } + + @Test fun tutorialAvailable_whenTutorialNotStarted() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) communalRepository.setIsCommunalHubShowing(false) @@ -103,6 +119,7 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { fun tutorialAvailable_whenTutorialIsStarted() = testScope.runTest { val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable) + setCommunalAvailable(true) keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) communalRepository.setIsCommunalHubShowing(true) @@ -183,4 +200,16 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED) } + + private suspend fun setCommunalAvailable(available: Boolean) { + if (available) { + communalRepository.setIsCommunalEnabled(true) + keyguardRepository.setIsEncryptedOrLockdown(false) + whenever(user.isMain).thenReturn(true) + userRepository.setUserInfos(listOf(user)) + userRepository.setSelectedUserInfo(user) + } else { + keyguardRepository.setIsEncryptedOrLockdown(true) + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt index 721fc4906aba..6b1b93777fbc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/log/CommunalLoggerStartableTest.kt @@ -21,14 +21,15 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -47,17 +48,16 @@ import org.mockito.MockitoAnnotations class CommunalLoggerStartableTest : SysuiTestCase() { @Mock private lateinit var uiEventLogger: UiEventLogger - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private lateinit var communalInteractor: CommunalInteractor private lateinit var underTest: CommunalLoggerStartable @Before fun setUp() { MockitoAnnotations.initMocks(this) - - val withDeps = CommunalInteractorFactory.create() - testScope = withDeps.testScope - communalInteractor = withDeps.communalInteractor + communalInteractor = kosmos.communalInteractor underTest = CommunalLoggerStartable( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 867a48dc3925..a2dec5ff8830 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -24,19 +24,21 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository +import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -59,8 +61,6 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var communalRepository: FakeCommunalRepository private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository @@ -72,17 +72,14 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - val withDeps = CommunalInteractorFactory.create(testScope) - keyguardRepository = withDeps.keyguardRepository - communalRepository = withDeps.communalRepository - tutorialRepository = withDeps.tutorialRepository - widgetRepository = withDeps.widgetRepository - smartspaceRepository = withDeps.smartspaceRepository - mediaRepository = withDeps.mediaRepository + tutorialRepository = kosmos.fakeCommunalTutorialRepository + widgetRepository = kosmos.fakeCommunalWidgetRepository + smartspaceRepository = kosmos.fakeSmartspaceRepository + mediaRepository = kosmos.fakeCommunalMediaRepository underTest = CommunalEditModeViewModel( - withDeps.communalInteractor, + kosmos.communalInteractor, mediaHost, uiEventLogger, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt index 9ac21dd69aab..c814f3f1db6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -17,31 +17,39 @@ package com.android.systemui.communal.view.viewmodel import android.app.smartspace.SmartspaceTarget +import android.content.pm.UserInfo import android.provider.Settings import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository -import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.data.repository.fakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.fakeCommunalTutorialRepository +import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalTutorialInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.POPUP_AUTO_HIDE_TIMEOUT_MS import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.Before @@ -57,16 +65,17 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class CommunalViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost + @Mock private lateinit var user: UserInfo - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var keyguardRepository: FakeKeyguardRepository - private lateinit var communalRepository: FakeCommunalRepository private lateinit var tutorialRepository: FakeCommunalTutorialRepository private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository - private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository + private lateinit var userRepository: FakeUserRepository private lateinit var underTest: CommunalViewModel @@ -74,22 +83,18 @@ class CommunalViewModelTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - testScope = TestScope() - - val withDeps = CommunalInteractorFactory.create() - keyguardRepository = withDeps.keyguardRepository - communalRepository = withDeps.communalRepository - tutorialRepository = withDeps.tutorialRepository - widgetRepository = withDeps.widgetRepository - smartspaceRepository = withDeps.smartspaceRepository - mediaRepository = withDeps.mediaRepository - communalPrefsRepository = withDeps.communalPrefsRepository + keyguardRepository = kosmos.fakeKeyguardRepository + tutorialRepository = kosmos.fakeCommunalTutorialRepository + widgetRepository = kosmos.fakeCommunalWidgetRepository + smartspaceRepository = kosmos.fakeSmartspaceRepository + mediaRepository = kosmos.fakeCommunalMediaRepository + userRepository = kosmos.fakeUserRepository underTest = CommunalViewModel( testScope, - withDeps.communalInteractor, - withDeps.tutorialInteractor, + kosmos.communalInteractor, + kosmos.communalTutorialInteractor, mediaHost, ) } @@ -104,9 +109,11 @@ class CommunalViewModelTest : SysuiTestCase() { @Test fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() = testScope.runTest { - // Keyguard showing, and tutorial not started. + // Keyguard showing, storage unlocked, main user, and tutorial not started. keyguardRepository.setKeyguardShowing(true) keyguardRepository.setKeyguardOccluded(false) + keyguardRepository.setIsEncryptedOrLockdown(false) + setIsMainUser(true) tutorialRepository.setTutorialSettingState( Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED ) @@ -202,4 +209,10 @@ class CommunalViewModelTest : SysuiTestCase() { underTest.onHidePopupAfterDismissCta() assertThat(isPopupOnDismissCtaShowing).isEqualTo(false) } + + private suspend fun setIsMainUser(isMainUser: Boolean) { + whenever(user.isMain).thenReturn(isMainUser) + userRepository.setUserInfos(listOf(user)) + userRepository.setSelectedUserInfo(user) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index 52305b1f5212..05b589126b08 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -49,10 +49,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository - private val trustRepository = kosmos.fakeTrustRepository - private val sceneInteractor = kosmos.sceneInteractor - private val authenticationInteractor = kosmos.authenticationInteractor + private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository } + private val trustRepository by lazy { kosmos.fakeTrustRepository } + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private lateinit var underTest: DeviceEntryInteractor @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java index 562f96c28b19..c14346899ede 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java @@ -93,7 +93,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { @Rule public final LeakCheckedTest.SysuiLeakCheck mLeakCheck = new LeakCheckedTest.SysuiLeakCheck(); - WindowManager.LayoutParams mWindowParams = new WindowManager.LayoutParams(); + WindowManager.LayoutParams mWindowParams; @Mock IDreamOverlayCallback mDreamOverlayCallback; @@ -184,6 +184,7 @@ public class DreamOverlayServiceTest extends SysuiTestCase { when(mDreamOverlayContainerViewController.getContainerView()) .thenReturn(mDreamOverlayContainerView); + mWindowParams = new WindowManager.LayoutParams(); mService = new DreamOverlayService( mContext, mLifecycleOwner, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt new file mode 100644 index 000000000000..ea766f8ea9bb --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.slider + +import android.widget.SeekBar +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.fakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class SeekableSliderHapticPluginTest : SysuiTestCase() { + + private val kosmos = Kosmos() + + @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule() + @Mock private lateinit var vibratorHelper: VibratorHelper + private val seekBar = SeekBar(mContext) + private lateinit var plugin: SeekableSliderHapticPlugin + + @Before + fun setup() { + whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0)) + } + + @Test + fun start_beginsTrackingSlider() = runOnStartedPlugin { assertThat(plugin.isTracking).isTrue() } + + @Test + fun stop_stopsTrackingSlider() = runOnStartedPlugin { + // WHEN called to stop + plugin.stop() + + // THEN stops tracking + assertThat(plugin.isTracking).isFalse() + } + + @Test + fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin { + // WHEN the plugin is restarted + plugin.stop() + plugin.start() + + // THEN the tracking begins again + assertThat(plugin.isTracking).isTrue() + } + + @Test + fun onKeyDown_startsWaiting() = runOnStartedPlugin { + // WHEN a keyDown event is recorded + plugin.onKeyDown() + + // THEN the timer starts waiting + assertThat(plugin.isKeyUpTimerWaiting).isTrue() + } + + @Test + fun keyUpWaitComplete_triggersOnArrowUp() = runOnStartedPlugin { + // GIVEN an onKeyDown that starts the wait and a program progress change that advances the + // slider state to ARROW_HANDLE_MOVED_ONCE + plugin.onKeyDown() + plugin.onProgressChanged(seekBar, 50, false) + testScheduler.runCurrent() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + + // WHEN the key-up wait completes after the timeout plus a small buffer + advanceTimeBy(KEY_UP_TIMEOUT + 10L) + + // THEN the onArrowUp event is delivered causing the slider tracker to move to IDLE + assertThat(plugin.trackerState).isEqualTo(SliderState.IDLE) + assertThat(plugin.isKeyUpTimerWaiting).isFalse() + } + + @Test + fun onKeyDown_whileWaiting_restartsWait() = runOnStartedPlugin { + // GIVEN an onKeyDown that starts the wait and a program progress change that advances the + // slider state to ARROW_HANDLE_MOVED_ONCE + plugin.onKeyDown() + plugin.onProgressChanged(seekBar, 50, false) + testScheduler.runCurrent() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + + // WHEN half the timeout period has elapsed and a new keyDown event occurs + advanceTimeBy(KEY_UP_TIMEOUT / 2) + plugin.onKeyDown() + + // AFTER advancing by a period of time that should have complete the original wait + advanceTimeBy(KEY_UP_TIMEOUT / 2 + 10L) + + // THEN the timer is still waiting and the slider tracker remains on ARROW_HANDLE_MOVED_ONCE + assertThat(plugin.isKeyUpTimerWaiting).isTrue() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + } + + private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) = + with(kosmos) { + testScope.runTest { + createPlugin(this, UnconfinedTestDispatcher(testScheduler)) + // GIVEN that the plugin is started + plugin.start() + + // THEN run the test + test() + } + } + + private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) { + plugin = + SeekableSliderHapticPlugin( + vibratorHelper, + kosmos.fakeSystemClock, + dispatcher, + scope, + ) + } + + companion object { + private const val KEY_UP_TIMEOUT = 100L + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index 6f62afc560fe..dc8b97abbfe8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -139,6 +139,18 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun topClippingBounds() = + testScope.runTest { + assertThat(underTest.topClippingBounds.value).isNull() + + underTest.topClippingBounds.value = 50 + assertThat(underTest.topClippingBounds.value).isEqualTo(50) + + underTest.topClippingBounds.value = 500 + assertThat(underTest.topClippingBounds.value).isEqualTo(500) + } + + @Test fun clockPosition() = testScope.runTest { assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 0)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index 2c3afb1b40a9..0b320a28b419 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -54,15 +54,17 @@ class KeyguardInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val repository = kosmos.fakeKeyguardRepository - private val sceneInteractor = kosmos.sceneInteractor - private val commandQueue = FakeCommandQueue() + private val repository by lazy { kosmos.fakeKeyguardRepository } + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val commandQueue by lazy { + FakeCommandQueue() + } private val bouncerRepository = FakeKeyguardBouncerRepository() private val shadeRepository = FakeShadeRepository() private val transitionState: MutableStateFlow<ObservableTransitionState> = MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Gone)) - private val underTest = + private val underTest by lazy { KeyguardInteractor( repository = repository, commandQueue = commandQueue, @@ -73,6 +75,7 @@ class KeyguardInteractorTest : SysuiTestCase() { shadeRepository = shadeRepository, sceneInteractorProvider = { sceneInteractor }, ) + } @Before fun setUp() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt index 9bccf4e26e87..ce43d4e14cc1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt @@ -38,12 +38,12 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import org.mockito.Spy @SmallTest @OptIn(ExperimentalCoroutinesApi::class) @@ -51,16 +51,19 @@ import org.mockito.Spy class LightRevealScrimInteractorTest : SysuiTestCase() { private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository() - @Spy private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository() + private val fakeLightRevealScrimRepository by lazy { + Mockito.spy(FakeLightRevealScrimRepository()) + } private val testScope = TestScope() - private val keyguardTransitionInteractor = + private val keyguardTransitionInteractor by lazy { KeyguardTransitionInteractorFactory.create( scope = testScope.backgroundScope, repository = fakeKeyguardTransitionRepository, ) .keyguardTransitionInteractor + } private lateinit var underTest: LightRevealScrimInteractor diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt index 9daf1860ebb8..199ffa6b87bd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt @@ -20,11 +20,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.TransitionState @@ -36,6 +39,7 @@ import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -45,10 +49,18 @@ import org.junit.runner.RunWith class AlternateBouncerToAodTransitionViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository - private val biometricSettingsRepository = kosmos.biometricSettingsRepository - private val underTest = kosmos.alternateBouncerToAodTransitionViewModel + private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository + private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository + private lateinit var underTest: AlternateBouncerToAodTransitionViewModel + + @Before + fun setUp() { + keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository + biometricSettingsRepository = kosmos.biometricSettingsRepository + underTest = kosmos.alternateBouncerToAodTransitionViewModel + } @Test fun deviceEntryParentViewAppear() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt index 3f7e0df427c6..d4438516a023 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt @@ -49,7 +49,9 @@ class AlternateBouncerToGoneTransitionViewModelTest : SysuiTestCase() { } private val testScope = kosmos.testScope private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val underTest = kosmos.alternateBouncerToGoneTransitionViewModel + private val underTest by lazy { + kosmos.alternateBouncerToGoneTransitionViewModel + } @Test fun deviceEntryParentViewDisappear() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt index c7ab52974852..ff41ea25f470 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModelTest.kt @@ -47,8 +47,8 @@ class AlternateBouncerToPrimaryBouncerTransitionViewModelTest : SysuiTestCase() } } private val testScope = kosmos.testScope - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val underTest = kosmos.alternateBouncerToPrimaryBouncerTransitionViewModel + private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + private val underTest by lazy { kosmos.alternateBouncerToPrimaryBouncerTransitionViewModel } @Test fun deviceEntryParentViewDisappear() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt index f1690dafe75a..89e29cf14451 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -20,10 +20,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING @@ -41,6 +43,7 @@ import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -50,9 +53,16 @@ import org.junit.runner.RunWith class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository - private val underTest = kosmos.dreamingToLockscreenTransitionViewModel + private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository + private lateinit var underTest: DreamingToLockscreenTransitionViewModel + + @Before + fun setUp() { + keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository + underTest = kosmos.dreamingToLockscreenTransitionViewModel + } @Test fun shortcutsAlpha_bothShortcutsReceiveLastValue() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt index f763a6790b51..36b26a490fdb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt @@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -29,6 +30,7 @@ import com.android.systemui.testKosmos import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -37,8 +39,14 @@ import org.junit.runner.RunWith class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val repository = kosmos.fakeKeyguardTransitionRepository - private val underTest = kosmos.goneToDreamingTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + private lateinit var underTest: GoneToDreamingTransitionViewModel + + @Before + fun setUp() { + repository = kosmos.fakeKeyguardTransitionRepository + underTest = kosmos.goneToDreamingTransitionViewModel + } @Test fun runTest() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index fd2fd2f04cde..6cc680baf938 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -25,9 +25,14 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags as AConfigFlags import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalRepository +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -42,6 +47,7 @@ import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -54,11 +60,14 @@ class KeyguardRootViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val keyguardInteractor = kosmos.keyguardInteractor + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val communalRepository = kosmos.communalRepository private val screenOffAnimationController = kosmos.screenOffAnimationController private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor private val dozeParameters = kosmos.dozeParameters - private val underTest = kosmos.keyguardRootViewModel + private val underTest by lazy { kosmos.keyguardRootViewModel } @Before fun setUp() { @@ -205,7 +214,38 @@ class KeyguardRootViewModelTest : SysuiTestCase() { } @Test - fun alpha_glanceableHubOpen_isZero() = + fun topClippingBounds() = + testScope.runTest { + val topClippingBounds by collectLastValue(underTest.topClippingBounds) + assertThat(topClippingBounds).isNull() + + keyguardRepository.topClippingBounds.value = 50 + assertThat(topClippingBounds).isEqualTo(50) + + keyguardRepository.topClippingBounds.value = 1000 + assertThat(topClippingBounds).isEqualTo(1000) + } + + @Test + fun alpha_idleOnHub_isZero() = + testScope.runTest { + val alpha by collectLastValue(underTest.alpha) + + // Hub transition state is idle with hub open. + communalRepository.setTransitionState( + flowOf(ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)) + ) + runCurrent() + + // Set keyguard alpha to 1.0f. + keyguardInteractor.setAlpha(1.0f) + + // Alpha property remains 0 regardless. + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun alpha_transitionToHub_isZero() = testScope.runTest { val alpha by collectLastValue(underTest.alpha) @@ -219,7 +259,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { } @Test - fun alpha_glanceableHubClosed_isOne() = + fun alpha_transitionFromHubToLockscreen_isOne() = testScope.runTest { val alpha by collectLastValue(underTest.alpha) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index aa15d0befa80..4595fbfab0d7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -47,9 +47,11 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } - private val underTest = createLockscreenSceneViewModel() + private val underTest by lazy { + createLockscreenSceneViewModel() + } @Test fun upTransitionSceneKey_canSwipeToUnlock_gone() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt index 74025fd6e100..8f04ec3814eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt @@ -27,18 +27,22 @@ import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.testKosmos import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -51,10 +55,18 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } } private val testScope = kosmos.testScope - private val repository = kosmos.fakeKeyguardTransitionRepository - private val shadeRepository = kosmos.shadeRepository - private val keyguardRepository = kosmos.fakeKeyguardRepository - private val underTest = kosmos.lockscreenToDreamingTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + private lateinit var shadeRepository: ShadeRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var underTest: LockscreenToDreamingTransitionViewModel + + @Before + fun setUp() { + repository = kosmos.fakeKeyguardTransitionRepository + shadeRepository = kosmos.shadeRepository + keyguardRepository = kosmos.fakeKeyguardRepository + underTest = kosmos.lockscreenToDreamingTransitionViewModel + } @Test fun lockscreenFadeOut() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt index 6fcb0c11edad..b120f8776d9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt @@ -22,12 +22,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState @@ -35,12 +38,14 @@ import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.testKosmos import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -52,11 +57,20 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } } private val testScope = kosmos.testScope - private val repository = kosmos.fakeKeyguardTransitionRepository - private val shadeRepository = kosmos.shadeRepository - private val keyguardRepository = kosmos.fakeKeyguardRepository - private val configurationRepository = kosmos.fakeConfigurationRepository - private val underTest = kosmos.lockscreenToOccludedTransitionViewModel + private lateinit var repository: FakeKeyguardTransitionRepository + private lateinit var shadeRepository: ShadeRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var configurationRepository: FakeConfigurationRepository + private lateinit var underTest: LockscreenToOccludedTransitionViewModel + + @Before + fun setUp() { + repository = kosmos.fakeKeyguardTransitionRepository + shadeRepository = kosmos.shadeRepository + keyguardRepository = kosmos.fakeKeyguardRepository + configurationRepository = kosmos.fakeConfigurationRepository + underTest = kosmos.lockscreenToOccludedTransitionViewModel + } @Test fun lockscreenFadeOut() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt index 639114c15000..dddf6485d0f4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt @@ -49,7 +49,9 @@ class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository val configurationRepository = kosmos.fakeConfigurationRepository - val underTest = kosmos.occludedToLockscreenTransitionViewModel + val underTest by lazy { + kosmos.occludedToLockscreenTransitionViewModel + } @Test fun lockscreenFadeIn() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index 30b87bbbcf11..30ac34402ffd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -54,7 +54,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val primaryBouncerInteractor = kosmos.primaryBouncerInteractor val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController - val underTest = kosmos.primaryBouncerToGoneTransitionViewModel + val underTest by lazy { + kosmos.primaryBouncerToGoneTransitionViewModel + } @Before fun setUp() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt index 96d57743e2ee..4207a9c27ad0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/interactor/WorkProfileAutoAddedAfterRestoreTest.kt @@ -54,7 +54,9 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) class WorkProfileAutoAddedAfterRestoreTest : SysuiTestCase() { - private val kosmos = Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) } + private val kosmos by lazy { + Kosmos().apply { fakeUserTracker.set(listOf(USER_0_INFO), 0) } + } // Getter here so it can change when there is a managed profile. private val workTileAvailable: Boolean get() = hasManagedProfile() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt index 8ee6d2005350..d05e98faee22 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt @@ -36,8 +36,9 @@ import org.junit.runner.RunWith class ColorCorrectionTileMapperTest : SysuiTestCase() { private val kosmos = Kosmos() private val colorCorrectionTileConfig = kosmos.qsColorCorrectionTileConfig - private val subtitleArray = + private val subtitleArray by lazy { context.resources.getStringArray(R.array.tile_states_color_correction) + } // Using lazy (versus =) to make sure we override the right context -- see b/311612168 private val mapper by lazy { ColorCorrectionTileMapper( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt index a84b9fa32d85..da60c18dcfd7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileRepositoryTest.kt @@ -308,21 +308,27 @@ class CustomTileRepositoryTest : SysuiTestCase() { val TEST_COMPONENT = ComponentName("test.pkg", "test.cls") val TEST_USER_1 = UserHandle.of(1)!! - val TEST_TILE_1 = + val TEST_TILE_1 by lazy { Tile().apply { label = "test_tile_1" icon = Icon.createWithContentUri("file://test_1") } + } val TEST_TILE_KEY_1 = TileServiceKey(TEST_COMPONENT, TEST_USER_1.identifier) - val TEST_DEFAULTS_1 = CustomTileDefaults.Result(TEST_TILE_1.icon, TEST_TILE_1.label) + val TEST_DEFAULTS_1 by lazy { + CustomTileDefaults.Result(TEST_TILE_1.icon, TEST_TILE_1.label) + } val TEST_USER_2 = UserHandle.of(2)!! - val TEST_TILE_2 = + val TEST_TILE_2 by lazy { Tile().apply { label = "test_tile_2" icon = Icon.createWithContentUri("file://test_2") } + } val TEST_TILE_KEY_2 = TileServiceKey(TEST_COMPONENT, TEST_USER_2.identifier) - val TEST_DEFAULTS_2 = CustomTileDefaults.Result(TEST_TILE_2.icon, TEST_TILE_2.label) + val TEST_DEFAULTS_2 by lazy { + CustomTileDefaults.Result(TEST_TILE_2.icon, TEST_TILE_2.label) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt index 20653ca18efc..995d6ac66137 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileInteractorTest.kt @@ -180,11 +180,14 @@ class CustomTileInteractorTest : SysuiTestCase() { val TEST_COMPONENT = ComponentName("test.pkg", "test.cls") val TEST_USER = UserHandle.of(1)!! - val TEST_TILE = + val TEST_TILE by lazy { Tile().apply { label = "test_tile_1" icon = Icon.createWithContentUri("file://test_1") } - val TEST_DEFAULTS = CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label) + } + val TEST_DEFAULTS by lazy { + CustomTileDefaults.Result(TEST_TILE.icon, TEST_TILE.label) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt index f3c3579966fd..ccd7ed92b884 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt @@ -39,7 +39,9 @@ class ColorInversionTileMapperTest : SysuiTestCase() { private val colorInversionTileConfig = kosmos.qsColorInversionTileConfig private val subtitleArrayId = SubtitleArrayMapping.getSubtitleId(colorInversionTileConfig.tileSpec.spec) - private val subtitleArray = context.resources.getStringArray(subtitleArrayId) + private val subtitleArray by lazy { + context.resources.getStringArray(subtitleArrayId) + } // Using lazy (versus =) to make sure we override the right context -- see b/311612168 private val mapper by lazy { ColorInversionTileMapper( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt index 7497ebdc2978..46e1609b02ad 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileDataInteractorTest.kt @@ -53,8 +53,9 @@ import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) class UiModeNightTileDataInteractorTest : SysuiTestCase() { - private val configurationController: ConfigurationController = + private val configurationController: ConfigurationController by lazy { ConfigurationControllerImpl(context) + } private val batteryController = FakeBatteryController(LeakCheck()) private val locationController = FakeLocationController(LeakCheck()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt index d9b1ea1aedcc..cae20d006dec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt @@ -16,12 +16,16 @@ package com.android.systemui.qs.ui.adapter +import android.content.res.Configuration import android.os.Bundle +import android.view.Surface import android.view.View import androidx.asynclayoutinflater.view.AsyncLayoutInflater import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.QSImpl import com.android.systemui.qs.dagger.QSComponent @@ -34,6 +38,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import java.util.Locale import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -81,11 +86,17 @@ class QSSceneAdapterImplTest : SysuiTestCase() { .also { components.add(it) } } } + private val configuration = Configuration(context.resources.configuration) + + private val fakeConfigurationRepository = + FakeConfigurationRepository().apply { onConfigurationChange(configuration) } + private val configurationInteractor = ConfigurationInteractor(fakeConfigurationRepository) private val mockAsyncLayoutInflater = mock<AsyncLayoutInflater>() { whenever(inflate(anyInt(), nullable(), any())).then { invocation -> val mockView = mock<View>() + whenever(mockView.context).thenReturn(context) invocation .getArgument<AsyncLayoutInflater.OnInflateFinishedListener>(2) .onInflateFinished( @@ -102,6 +113,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() { qsImplProvider, testDispatcher, testScope.backgroundScope, + configurationInteractor, { mockAsyncLayoutInflater }, ) @@ -297,6 +309,9 @@ class QSSceneAdapterImplTest : SysuiTestCase() { @Test fun reinflation_previousStateDestroyed() = testScope.runTest { + // Run all flows... In particular, initial configuration propagation that could cause + // QSImpl to re-inflate. + runCurrent() val qsImpl by collectLastValue(underTest.qsImpl) underTest.inflate(context) @@ -322,4 +337,81 @@ class QSSceneAdapterImplTest : SysuiTestCase() { bundleArgCaptor.value, ) } + + @Test + fun changeInLocale_reinflation() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + + val newLocale = + if (configuration.locales[0] == Locale("en-US")) { + Locale("es-UY") + } else { + Locale("en-US") + } + configuration.setLocale(newLocale) + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!) + } + + @Test + fun changeInFontSize_reinflation() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + + configuration.fontScale *= 2 + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!) + } + + @Test + fun changeInAssetPath_reinflation() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + + configuration.assetsSeq += 1 + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!) + } + + @Test + fun otherChangesInConfiguration_noReinflation_configurationChangeDispatched() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + configuration.densityDpi *= 2 + configuration.windowConfiguration.maxBounds.scale(2f) + configuration.windowConfiguration.rotation = Surface.ROTATION_270 + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isSameInstanceAs(qsImpl!!) + verify(qsImpl!!).onConfigurationChanged(configuration) + verify(qsImpl!!.view).dispatchConfigurationChanged(configuration) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index be523b813494..42200a3d33ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -23,6 +23,8 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Direction @@ -39,12 +41,16 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsVi import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.times +import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -52,10 +58,16 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() } + private val footerActionsViewModel = mock<FooterActionsViewModel>() + private val footerActionsViewModelFactory = + mock<FooterActionsViewModel.Factory> { + whenever(create(any())).thenReturn(footerActionsViewModel) + } + private val footerActionsController = mock<FooterActionsController>() private var mobileIconsViewModel: MobileIconsViewModel = MobileIconsViewModel( @@ -94,6 +106,8 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, notifications = kosmos.notificationsPlaceholderViewModel, + footerActionsViewModelFactory, + footerActionsController, ) } @@ -125,4 +139,12 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { ) ) } + + @Test + fun gettingViewModelInitializesControllerOnlyOnce() { + underTest.getFooterActionsViewModel(mock()) + underTest.getFooterActionsViewModel(mock()) + + verify(footerActionsController, times(1)).init() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 1cd764e01bad..1e5ebd0c2acd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -123,30 +123,32 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } private val testScope = kosmos.testScope - private val sceneContainerConfig = kosmos.sceneContainerConfig - private val sceneInteractor = kosmos.sceneInteractor - private val authenticationInteractor = kosmos.authenticationInteractor - private val deviceEntryInteractor = kosmos.deviceEntryInteractor - private val communalInteractor = kosmos.communalInteractor + private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } + private val communalInteractor by lazy { kosmos.communalInteractor } - private val transitionState = + private val transitionState by lazy { MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(sceneContainerConfig.initialSceneKey) ) - private val sceneContainerViewModel = + } + private val sceneContainerViewModel by lazy { SceneContainerViewModel( sceneInteractor = sceneInteractor, falsingInteractor = kosmos.falsingInteractor, ) .apply { setTransitionState(transitionState) } + } - private val bouncerInteractor = kosmos.bouncerInteractor + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor private lateinit var bouncerViewModel: BouncerViewModel - private val lockscreenSceneViewModel = + private val lockscreenSceneViewModel by lazy { LockscreenSceneViewModel( applicationScope = testScope.backgroundScope, deviceEntryInteractor = deviceEntryInteractor, @@ -157,6 +159,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { ), notifications = kosmos.notificationsPlaceholderViewModel, ) + } private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) @@ -179,8 +182,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel private lateinit var shadeSceneViewModel: ShadeSceneViewModel - private val keyguardInteractor = kosmos.keyguardInteractor - private val powerInteractor = kosmos.powerInteractor + private val keyguardInteractor by lazy { kosmos.keyguardInteractor } + private val powerInteractor by lazy { kosmos.powerInteractor } private var bouncerSceneJob: Job? = null diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index fc0df1288553..16cb6231a872 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -79,13 +79,13 @@ class SceneContainerStartableTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor - private val sceneContainerFlags = kosmos.fakeSceneContainerFlags - private val authenticationInteractor = kosmos.authenticationInteractor - private val bouncerInteractor = kosmos.bouncerInteractor - private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository - private val deviceEntryInteractor = kosmos.deviceEntryInteractor - private val keyguardInteractor = kosmos.keyguardInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val sceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags } + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private val faceAuthRepository by lazy { kosmos.fakeDeviceEntryFaceAuthRepository } + private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } + private val keyguardInteractor by lazy { kosmos.keyguardInteractor } private val sysUiState: SysUiState = mock() private val falsingCollector: FalsingCollector = mock() private val powerInteractor = PowerInteractorFactory.create().powerInteractor diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index ede453d85ee1..a16ce4325dcf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -41,7 +41,7 @@ import org.junit.runner.RunWith class SceneContainerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() - private val interactor = kosmos.sceneInteractor + private val interactor by lazy { kosmos.sceneInteractor } private lateinit var underTest: SceneContainerViewModel @Before diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 51745023c5be..e9a2a3befb03 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -35,7 +35,7 @@ import org.mockito.MockitoAnnotations class ShadeHeaderViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 251daffe2d91..5ef095fd8201 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -60,8 +60,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val sceneInteractor = kosmos.sceneInteractor - private val deviceEntryInteractor = kosmos.deviceEntryInteractor + private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt index ef2046d85a14..ad4b98bdb3ee 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/CommunalSmartspaceControllerTest.kt @@ -69,7 +69,9 @@ class CommunalSmartspaceControllerTest : SysuiTestCase() { private lateinit var controller: CommunalSmartspaceController // TODO(b/272811280): Remove usage of real view - private val fakeParent = FrameLayout(context) + private val fakeParent by lazy { + FrameLayout(context) + } /** * A class which implements SmartspaceView and extends View. This is mocked to provide the right diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt index e09385934991..3a386311223f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/DreamSmartspaceControllerTest.kt @@ -73,8 +73,9 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { @Mock private lateinit var weatherViewComponent: SmartspaceViewComponent - @Spy - private var weatherSmartspaceView: SmartspaceView = TestView(context) + private val weatherSmartspaceView: SmartspaceView by lazy { + Mockito.spy(TestView(context)) + } @Mock private lateinit var targetFilter: SmartspaceTargetFilter @@ -88,8 +89,9 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { @Mock private lateinit var precondition: SmartspacePrecondition - @Spy - private var smartspaceView: SmartspaceView = TestView(context) + private val smartspaceView: SmartspaceView by lazy { + Mockito.spy(TestView(context)) + } @Mock private lateinit var listener: BcSmartspaceDataPlugin.SmartspaceTargetListener @@ -100,7 +102,9 @@ class DreamSmartspaceControllerTest : SysuiTestCase() { private lateinit var controller: DreamSmartspaceController // TODO(b/272811280): Remove usage of real view - private val fakeParent = FrameLayout(context) + private val fakeParent by lazy { + FrameLayout(context) + } /** * A class which implements SmartspaceView and extends View. This is mocked to provide the right diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 607996d67d4f..6a2e31739c77 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -56,9 +56,9 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { } } private val testScope = kosmos.testScope - private val placeholderViewModel = kosmos.notificationsPlaceholderViewModel - private val appearanceViewModel = kosmos.notificationStackAppearanceViewModel - private val sceneInteractor = kosmos.sceneInteractor + private val placeholderViewModel by lazy { kosmos.notificationsPlaceholderViewModel } + private val appearanceViewModel by lazy { kosmos.notificationStackAppearanceViewModel } + private val sceneInteractor by lazy { kosmos.sceneInteractor } @Test fun updateBounds() = diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java index 3d9645a3d983..b1736b16875d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java @@ -227,5 +227,10 @@ public interface VolumeDialogController { void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch); // requires version 2 void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs); + + /** + * Callback function for when the volume changed due to a physical key press. + */ + void onVolumeChangedFromKey(); } } diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml new file mode 100644 index 000000000000..045c19eb09dc --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_satellite_connected_0.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" + android:fillColor="#fff"/> + <path + android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" + android:fillColor="#fff"/> + <path + android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" + android:fillColor="#fff"/> + <path + android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" + android:fillColor="#fff"/> + <path + android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" + android:fillColor="#fff"/> + <path + android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" + android:fillColor="#fff"/> + <path + android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" + android:fillAlpha="0.3" + android:fillColor="#fff"/> + <path + android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" + android:fillAlpha="0.3" + android:fillColor="#fff"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml new file mode 100644 index 000000000000..5e012ab7edbd --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_satellite_connected_1.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" + android:fillColor="#fff"/> + <path + android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" + android:fillColor="#fff"/> + <path + android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" + android:fillColor="#fff"/> + <path + android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" + android:fillColor="#fff"/> + <path + android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" + android:fillColor="#fff"/> + <path + android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" + android:fillColor="#fff"/> + <path + android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" + android:fillAlpha="0.3" + android:fillColor="#fff"/> + <path + android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" + android:fillColor="#fff"/> +</vector> + diff --git a/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml new file mode 100644 index 000000000000..d8a9a703260d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_satellite_connected_2.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" + android:fillColor="#fff"/> + <path + android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" + android:fillColor="#fff"/> + <path + android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" + android:fillColor="#fff"/> + <path + android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" + android:fillColor="#fff"/> + <path + android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" + android:fillColor="#fff"/> + <path + android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" + android:fillColor="#fff"/> + <path + android:pathData="M11,22.48V21.48C11,21.21 11.22,21 11.49,20.99C16.63,20.8 20.75,16.62 20.99,11.48C21,11.21 21.21,11 21.48,11H22.48C22.76,11 23,11.24 22.99,11.52C22.72,17.73 17.73,22.73 11.52,22.99C11.24,23 11,22.77 11,22.48Z" + android:fillColor="#fff"/> + <path + android:pathData="M11,18.98V17.98C11,17.71 11.21,17.51 11.48,17.49C14.69,17.26 17.33,14.7 17.49,11.49C17.5,11.22 17.71,11.01 17.98,11.01H18.79C19.26,11.01 19.5,11.25 19.48,11.53C19.22,15.8 15.79,19.23 11.52,19.49C11.24,19.51 11,19.27 11,18.99V18.98Z" + android:fillColor="#fff"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml new file mode 100644 index 000000000000..dec9930959a0 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_satellite_not_connected.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + > + <path + android:pathData="M14.73,3.36L17.63,6.2C17.83,6.39 17.83,6.71 17.63,6.91L16.89,7.65C16.69,7.85 16.37,7.85 16.18,7.65L13.34,4.78C13.15,4.59 13.15,4.28 13.34,4.08L14.01,3.37C14.2,3.17 14.52,3.16 14.72,3.36H14.73ZM14.37,1C13.85,1 13.32,1.2 12.93,1.61L11.56,3.06C10.8,3.84 10.81,5.09 11.58,5.86L15.13,9.41C15.52,9.8 16.03,10 16.55,10C17.07,10 17.58,9.8 17.97,9.41L19.42,7.96C20.21,7.17 20.2,5.89 19.4,5.12L15.77,1.57C15.38,1.19 14.88,1 14.37,1Z" + android:fillColor="#fff"/> + <path + android:pathData="M4.73,13.36L7.63,16.2C7.83,16.39 7.83,16.71 7.63,16.91L6.89,17.65C6.69,17.85 6.37,17.85 6.18,17.65L3.34,14.78C3.15,14.59 3.15,14.28 3.34,14.08L4.01,13.37C4.2,13.17 4.52,13.16 4.72,13.36H4.73ZM4.37,11C3.85,11 3.32,11.2 2.93,11.61L1.56,13.06C0.8,13.84 0.81,15.09 1.58,15.86L5.13,19.41C5.52,19.8 6.03,20 6.55,20C7.07,20 7.58,19.8 7.97,19.41L9.42,17.96C10.21,17.17 10.2,15.89 9.4,15.12L5.77,11.57C5.38,11.19 4.88,11 4.37,11Z" + android:fillColor="#fff"/> + <path + android:pathData="M8.622,5.368L5.372,8.618L10.112,13.358C11.009,14.255 12.464,14.255 13.362,13.358C14.259,12.46 14.259,11.005 13.362,10.108L8.622,5.368Z" + android:fillColor="#fff"/> + <path + android:pathData="M16.766,3.169L13.471,6.464L14.532,7.525L17.827,4.23L16.766,3.169Z" + android:fillColor="#fff"/> + <path + android:pathData="M14.728,5.226L3.478,16.476L4.538,17.536L15.788,6.286L14.728,5.226Z" + android:fillColor="#fff"/> + <path + android:pathData="M12.63,9.38L9.38,12.63L4.67,7.92C3.77,7.02 3.77,5.57 4.67,4.67C5.57,3.77 7.02,3.77 7.92,4.67L12.63,9.38Z" + android:fillColor="#fff"/> +</vector> diff --git a/packages/SystemUI/res/layout/bindable_status_bar_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml new file mode 100644 index 000000000000..ee4d05c3bda5 --- /dev/null +++ b/packages/SystemUI/res/layout/bindable_status_bar_icon.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Base layout that provides a single bindable icon_view id image view --> +<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical" + > + + <ImageView + android:id="@+id/icon_view" + android:layout_height="@dimen/status_bar_bindable_icon_size" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical" + android:padding="@dimen/status_bar_bindable_icon_padding" + android:scaleType="fitCenter" + /> + +</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView> diff --git a/packages/SystemUI/res/layout/qs_customize_panel_content.xml b/packages/SystemUI/res/layout/qs_customize_panel_content.xml index 3be99939ba0f..156c98333f3a 100644 --- a/packages/SystemUI/res/layout/qs_customize_panel_content.xml +++ b/packages/SystemUI/res/layout/qs_customize_panel_content.xml @@ -37,6 +37,7 @@ android:layout_height="wrap_content" android:background="@drawable/qs_customizer_toolbar" android:navigationContentDescription="@*android:string/action_bar_up_description" + android:titleTextAppearance="@*android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title" style="@style/QSCustomizeToolbar" /> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ee2a1ceab2b7..bc221b8e4e01 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -151,6 +151,8 @@ <dimen name="status_bar_icon_size_sp">@*android:dimen/status_bar_icon_size_sp</dimen> <!-- Original dp height of notification icons in the status bar --> <dimen name="status_bar_icon_size">@*android:dimen/status_bar_icon_size</dimen> + <dimen name="status_bar_bindable_icon_size">20sp</dimen> + <dimen name="status_bar_bindable_icon_padding">2sp</dimen> <!-- Default horizontal drawable padding for status bar icons. --> <dimen name="status_bar_horizontal_padding">2.5sp</dimen> @@ -1726,6 +1728,10 @@ <dimen name="communal_grid_height">630dp</dimen> <!-- Number of columns for each communal card --> <integer name="communal_grid_columns_per_card">6</integer> + <!-- Width of area on right edge of screen in which swipes will open the communal hub --> + <dimen name="communal_right_edge_swipe_region_width">16dp</dimen> + <!-- Height of area at top of communal hub where swipes should open the notification shade --> + <dimen name="communal_top_edge_swipe_region_height">32dp</dimen> <dimen name="drag_and_drop_icon_size">70dp</dimen> @@ -1930,5 +1936,9 @@ <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> <!-- UDFPS view attributes --> - <dimen name="udfps_icon_size">6mm</dimen> + <!-- UDFPS icon size in microns/um --> + <dimen name="udfps_icon_size" format="float">6000</dimen> + <!-- Microns/ums (1000 um = 1mm) per pixel for the given device. If unspecified, UI that + relies on this value will not be sized correctly. --> + <item name="pixel_pitch" format="float" type="dimen">-1</item> </resources> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl index 7088829bd2a3..d191a3c3f28b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl @@ -20,7 +20,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.os.Bundle; import android.view.MotionEvent; -import android.view.SurfaceControl; import com.android.systemui.shared.recents.ISystemUiProxy; // Next ID: 29 @@ -99,11 +98,6 @@ oneway interface IOverviewProxy { void enterStageSplitFromRunningApp(boolean leftOrTop) = 25; /** - * Sent when the surface for navigation bar is created or changed - */ - void onNavigationBarSurface(in SurfaceControl surface) = 26; - - /** * Sent when the task bar stash state is toggled. */ void onTaskbarToggled() = 27; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 033f93b260ab..ad303171d22e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -477,9 +477,13 @@ public class KeyguardClockSwitch extends RelativeLayout { public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardClockSwitch:"); pw.println(" mSmallClockFrame = " + mSmallClockFrame); - pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha()); + if (mSmallClockFrame != null) { + pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha()); + } pw.println(" mLargeClockFrame = " + mLargeClockFrame); - pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha()); + if (mLargeClockFrame != null) { + pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha()); + } pw.println(" mStatusArea = " + mStatusArea); pw.println(" mDisplayedClockSize = " + mDisplayedClockSize); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 6869bbaedcf3..97999cc19dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -240,7 +240,9 @@ class MenuViewLayer extends FrameLayout implements mMenuView.setOnTargetFeaturesChangeListener(newTargetFeatures -> { if (Flags.floatingMenuDragToHide()) { dismissNotification(); - undo(); + if (newTargetFeatures.size() > 0) { + undo(); + } } else { if (newTargetFeatures.size() < 1) { return; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt index 4fc1b5841047..a77cc1fea6a6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.domain.interactor import android.content.Context +import android.util.Log import android.view.MotionEvent import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams @@ -42,12 +43,23 @@ import kotlinx.coroutines.flow.stateIn class UdfpsOverlayInteractor @Inject constructor( - @Application context: Context, + @Application private val context: Context, private val authController: AuthController, private val selectedUserInteractor: SelectedUserInteractor, @Application scope: CoroutineScope ) { - private val iconSize: Int = context.resources.getDimensionPixelSize(R.dimen.udfps_icon_size) + private fun calculateIconSize(): Int { + val pixelPitch = context.resources.getFloat(R.dimen.pixel_pitch) + if (pixelPitch <= 0) { + Log.e( + "UdfpsOverlayInteractor", + "invalid pixelPitch: $pixelPitch. Pixel pitch must be updated per device." + ) + } + return (context.resources.getFloat(R.dimen.udfps_icon_size) / pixelPitch).toInt() + } + + private var iconSize: Int = calculateIconSize() /** Whether a touch is within the under-display fingerprint sensor area */ fun isTouchWithinUdfpsArea(ev: MotionEvent): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt index 13539850a598..5f6ff82c6038 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt @@ -72,6 +72,9 @@ class ConfigurationInteractor @Inject constructor(private val repository: Config val onAnyConfigurationChange: Flow<Unit> = repository.onAnyConfigurationChange.onStart { emit(Unit) } + /** Emits the new configuration on any configuration change */ + val configurationValues: Flow<Configuration> = repository.configurationValues + /** Emits the current resolution scaling factor */ val scaleForResolution: Flow<Float> = repository.scaleForResolution } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 92d01dbe3c3e..44b0383e12c6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -36,6 +36,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.smartspace.data.repository.SmartspaceRepository +import com.android.systemui.user.data.repository.UserRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -62,6 +63,7 @@ constructor( private val communalPrefsRepository: CommunalPrefsRepository, mediaRepository: CommunalMediaRepository, smartspaceRepository: SmartspaceRepository, + userRepository: UserRepository, keyguardInteractor: KeyguardInteractor, private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter @@ -75,7 +77,14 @@ constructor( val isCommunalAvailable: StateFlow<Boolean> = flowOf(isCommunalEnabled) .flatMapLatest { enabled -> - if (enabled) keyguardInteractor.isEncryptedOrLockdown.map { !it } else flowOf(false) + if (enabled) + combine( + keyguardInteractor.isEncryptedOrLockdown, + userRepository.selectedUserInfo, + ) { isEncryptedOrLockdown, selectedUserInfo -> + !isEncryptedOrLockdown && selectedUserInfo.isMain + } + else flowOf(false) } .distinctUntilChanged() .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) } @@ -263,6 +272,12 @@ constructor( companion object { /** + * The user activity timeout which should be used when the communal hub is opened. A value + * of -1 means that the user's chosen screen timeout will be used instead. + */ + const val AWAKE_INTERVAL_MS = -1 + + /** * Calculates the content size dynamically based on the total number of contents of that * type. * diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 5ca89f28f1fc..4e5be9b8aa5e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -45,14 +45,17 @@ constructor( private val communalTutorialRepository: CommunalTutorialRepository, keyguardInteractor: KeyguardInteractor, private val communalRepository: CommunalRepository, + communalInteractor: CommunalInteractor, ) { /** An observable for whether the tutorial is available. */ val isTutorialAvailable: Flow<Boolean> = combine( + communalInteractor.isCommunalAvailable, keyguardInteractor.isKeyguardVisible, communalTutorialRepository.tutorialSettingState, - ) { isKeyguardVisible, tutorialSettingState -> - isKeyguardVisible && + ) { isCommunalAvailable, isKeyguardVisible, tutorialSettingState -> + isCommunalAvailable && + isKeyguardVisible && tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED } .distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 2bb9d0e52757..a909383f4a2c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -74,6 +74,7 @@ constructor( // before the MediaHierarchyManager attempts to move the UMO to the hub. with(mediaHost) { expansion = MediaHostState.EXPANDED + expandedMatchesParentHeight = true showsOnlyActiveMedia = false falsingProtectionNeeded = false init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 641064becf24..2f9fd2e99619 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -17,6 +17,7 @@ package com.android.systemui.compose +import android.app.Dialog import android.content.Context import android.view.View import android.view.WindowInsets @@ -26,11 +27,13 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow @@ -86,6 +89,12 @@ interface BaseComposeFacade { sceneByKey: Map<SceneKey, Scene>, ): View + /** Creates sticky key dialog presenting provided [viewModel] **/ + fun createStickyKeysDialog( + dialogFactory: SystemUIDialogFactory, + viewModel: StickyKeysIndicatorViewModel + ): Dialog + /** Create a [View] to represent [viewModel] on screen. */ fun createCommunalView( context: Context, diff --git a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java index db0c3c68107a..0fd688760a32 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java +++ b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java @@ -18,10 +18,7 @@ package com.android.systemui.doze.dagger; import android.content.Context; import android.hardware.Sensor; -import android.os.Handler; -import com.android.systemui.res.R; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.doze.DozeAuthRemover; import com.android.systemui.doze.DozeBrightnessHostForwarder; @@ -40,6 +37,7 @@ import com.android.systemui.doze.DozeTransitionListener; import com.android.systemui.doze.DozeTriggers; import com.android.systemui.doze.DozeUi; import com.android.systemui.doze.DozeWallpaperState; +import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.sensors.AsyncSensorManager; @@ -75,9 +73,8 @@ public abstract class DozeModule { @Provides @DozeScope - static WakeLock providesDozeWakeLock(DelayedWakeLock.Builder delayedWakeLockBuilder, - @Main Handler handler) { - return delayedWakeLockBuilder.setHandler(handler).setTag("Doze").build(); + static WakeLock providesDozeWakeLock(DelayedWakeLock.Factory delayedWakeLockFactory) { + return delayedWakeLockFactory.create("Doze"); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 846736c04d98..3f7c152f47d8 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -574,9 +574,6 @@ object Flags { @JvmField val ENABLE_NEW_PRIVACY_DIALOG = releasedFlag("enable_new_privacy_dialog") - // TODO(b/289573946): Tracking Bug - @JvmField val PRECOMPUTED_TEXT = releasedFlag("precomputed_text") - // TODO(b/302087895): Tracking Bug @JvmField val CALL_LAYOUT_ASYNC_SET_DATA = unreleasedFlag("call_layout_async_set_data", teamfood = true) diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt new file mode 100644 index 000000000000..58fb6a95b872 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.slider + +import android.view.MotionEvent +import android.view.VelocityTracker +import android.widget.SeekBar +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.time.SystemClock +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback. + * + * A [SeekableSliderEventProducer] is used as the producer of slider events, a + * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback + * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that + * tracks and manipulates the slider state. + */ +class SeekableSliderHapticPlugin +@JvmOverloads +constructor( + vibratorHelper: VibratorHelper, + systemClock: SystemClock, + @Main private val mainDispatcher: CoroutineDispatcher, + @Application private val applicationScope: CoroutineScope, + sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(), + sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), +) { + + private val velocityTracker = VelocityTracker.obtain() + + private val sliderEventProducer = SeekableSliderEventProducer() + + private val sliderHapticFeedbackProvider = + SliderHapticFeedbackProvider( + vibratorHelper, + velocityTracker, + sliderHapticFeedbackConfig, + systemClock, + ) + + private val sliderTracker = + SeekableSliderTracker( + sliderHapticFeedbackProvider, + sliderEventProducer, + mainDispatcher, + sliderTrackerConfig, + ) + + val isTracking: Boolean + get() = sliderTracker.isTracking + + val trackerState: SliderState + get() = sliderTracker.currentState + + /** + * A waiting [Job] for a timer that estimates the key-up event when a key-down event is + * received. + * + * This is useful for the cases where the slider is being operated by an external key, but the + * release of the key is not easily accessible (e.g., the volume keys) + */ + private var keyUpJob: Job? = null + + @VisibleForTesting + val isKeyUpTimerWaiting: Boolean + get() = keyUpJob != null && keyUpJob?.isActive == true + + /** + * Start the plugin. + * + * This starts the tracking of slider states, events and triggering of haptic feedback. + */ + fun start() { + if (!isTracking) { + sliderTracker.startTracking() + } + } + + /** + * Stop the plugin + * + * This stops the tracking of slider states, events and triggers of haptic feedback. + */ + fun stop() = sliderTracker.stopTracking() + + /** React to a touch event */ + fun onTouchEvent(event: MotionEvent?) { + when (event?.actionMasked) { + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> velocityTracker.clear() + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE -> velocityTracker.addMovement(event) + } + } + + /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */ + fun onStartTrackingTouch(seekBar: SeekBar) { + if (isTracking) { + sliderEventProducer.onStartTrackingTouch(seekBar) + } + } + + /** onProgressChanged event from the slider's [android.widget.SeekBar] */ + fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (isTracking) { + sliderEventProducer.onProgressChanged(seekBar, progress, fromUser) + } + } + + /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */ + fun onStopTrackingTouch(seekBar: SeekBar) { + if (isTracking) { + sliderEventProducer.onStopTrackingTouch(seekBar) + } + } + + /** onArrowUp event recorded */ + fun onArrowUp() { + if (isTracking) { + sliderEventProducer.onArrowUp() + } + } + + /** + * An external key was pressed (e.g., a volume key). + * + * This event is used to estimate the key-up event based on by running a timer as a waiting + * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp + * event. Therefore, [onArrowUp] must be called after the timeout. + */ + fun onKeyDown() { + if (!isTracking) return + + if (isKeyUpTimerWaiting) { + // Cancel the ongoing wait + keyUpJob?.cancel() + } + keyUpJob = + applicationScope.launch { + delay(KEY_UP_TIMEOUT) + onArrowUp() + } + } + + companion object { + const val KEY_UP_TIMEOUT = 100L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt index 37034f63aca7..4ed812010100 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt @@ -26,11 +26,20 @@ import javax.inject.Inject private const val TAG = "stickyKeys" class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogBuffer) { - fun logNewStickyKeysReceived(linkedHashMap: Map<ModifierKey, Locked>) { + fun logNewStickyKeysReceived(stickyKeys: Map<ModifierKey, Locked>) { buffer.log( TAG, LogLevel.VERBOSE, - { str1 = linkedHashMap.toString() }, + { str1 = stickyKeys.toString() }, + { "new sticky keys state received: $str1" } + ) + } + + fun logNewUiState(stickyKeys: Map<ModifierKey, Locked>) { + buffer.log( + TAG, + LogLevel.INFO, + { str1 = stickyKeys.toString() }, { "new sticky keys state received: $str1" } ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt new file mode 100644 index 000000000000..b68551bf93b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.ui + +import android.app.Dialog +import android.util.Log +import android.view.Gravity +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.Window +import android.view.WindowManager +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +@SysUISingleton +class StickyKeysIndicatorCoordinator +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val dialogFactory: SystemUIDialogFactory, + private val viewModel: StickyKeysIndicatorViewModel, + private val stickyKeysLogger: StickyKeysLogger, +) { + + private var dialog: Dialog? = null + + fun startListening() { + // this check needs to be moved to PhysicalKeyboardCoreStartable + if (!ComposeFacade.isComposeAvailable()) { + Log.e("StickyKeysIndicatorCoordinator", "Compose is required for this UI") + return + } + applicationScope.launch { + viewModel.indicatorContent.collect { stickyKeys -> + stickyKeysLogger.logNewUiState(stickyKeys) + if (stickyKeys.isEmpty()) { + dialog?.dismiss() + dialog = null + } else if (dialog == null) { + dialog = ComposeFacade.createStickyKeysDialog(dialogFactory, viewModel).apply { + setCanceledOnTouchOutside(false) + window?.setAttributes() + show() + } + } + } + } + } + + private fun Window.setAttributes() { + setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setGravity(Gravity.TOP or Gravity.END) + attributes = WindowManager.LayoutParams().apply { + copyFrom(attributes) + width = WRAP_CONTENT + title = "StickyKeysIndicator" + } + } +} 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 d012d24b767e..14371949c9c8 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 @@ -132,6 +132,9 @@ interface KeyguardRepository { */ val isDozing: StateFlow<Boolean> + /** Keyguard can be clipped at the top as the shade is dragged */ + val topClippingBounds: MutableStateFlow<Int?> + /** * Observable for whether the device is dreaming. * @@ -326,6 +329,8 @@ constructor( private val _clockShouldBeCentered = MutableStateFlow(true) override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered.asStateFlow() + override val topClippingBounds = MutableStateFlow<Int?>(null) + override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 3292ea85639c..4df5d50a7a96 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -95,6 +95,7 @@ constructor( companion object { const val TAG = "FromGlanceableHubTransitionInteractor" - val DEFAULT_DURATION = 500.milliseconds + val DEFAULT_DURATION = 400.milliseconds + val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION } } 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 6170356d4a90..91747e0f69a0 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 @@ -171,6 +171,14 @@ constructor( /** Whether the keyguard is going away. */ val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway + /** Keyguard can be clipped at the top as the shade is dragged */ + val topClippingBounds: Flow<Int?> = + combine(configurationInteractor.onAnyConfigurationChange, repository.topClippingBounds) { + _, + topClippingBounds -> + topClippingBounds + } + /** Last point that [KeyguardRootView] view was tapped */ val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow() @@ -328,6 +336,10 @@ constructor( repository.keyguardDoneAnimationsFinished() } + fun setTopClippingBounds(top: Int?) { + repository.topClippingBounds.value = top + } + companion object { private const val TAG = "KeyguardInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 2aebd99e3664..48092c6374e0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter import android.annotation.DrawableRes import android.annotation.SuppressLint import android.graphics.Point +import android.graphics.Rect import android.view.HapticFeedbackConstants import android.view.View import android.view.View.OnLayoutChangeListener @@ -158,6 +159,23 @@ object KeyguardRootViewBinder { } launch { + val clipBounds = Rect() + viewModel.topClippingBounds.collect { clipTop -> + if (clipTop == null) { + view.setClipBounds(null) + } else { + clipBounds.apply { + top = clipTop + left = view.getLeft() + right = view.getRight() + bottom = view.getBottom() + } + view.setClipBounds(clipBounds) + } + } + } + + launch { viewModel.lockscreenStateAlpha.collect { alpha -> childViews[statusViewId]?.alpha = alpha } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index bc518210a067..6aa2ecabae75 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject @@ -37,7 +37,7 @@ constructor( ) { private val transitionAnimation = animationFlow.setup( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN, ) @@ -45,10 +45,20 @@ constructor( // TODO(b/315205222): implement full animation spec instead of just a simple fade. val keyguardAlpha: Flow<Float> = transitionAnimation.sharedFlow( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, onStep = { it }, onFinish = { 1f }, onCancel = { 0f }, name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", ) + + // TODO(b/315205216): implement full animation spec instead of just a simple fade. + val notificationAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, + onStep = { it }, + onFinish = { 1f }, + onCancel = { 0f }, + name = "GLANCEABLE_HUB->LOCKSCREEN: notificationAlpha", + ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 5d36da9b9df1..709e184e6e52 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -21,6 +21,7 @@ import android.graphics.Point import android.view.View.VISIBLE import com.android.systemui.Flags.newAodTransition import com.android.systemui.common.shared.model.NotificationContainerBounds +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -56,6 +57,7 @@ constructor( private val deviceEntryInteractor: DeviceEntryInteractor, private val dozeParameters: DozeParameters, private val keyguardInteractor: KeyguardInteractor, + communalInteractor: CommunalInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, @@ -80,13 +82,31 @@ constructor( val notificationBounds: StateFlow<NotificationContainerBounds> = keyguardInteractor.notificationContainerBounds + /** + * The keyguard root view can be clipped as the shade is pulled down, typically only for + * non-split shade cases. + */ + val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds + /** An observable for the alpha level for the entire keyguard root view. */ val alpha: Flow<Float> = - merge( - aodAlphaViewModel.alpha, - lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, - glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, - ) + combine( + communalInteractor.isIdleOnCommunal, + merge( + aodAlphaViewModel.alpha, + lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, + glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, + ) + ) { isIdleOnCommunal, alpha -> + if (isIdleOnCommunal) { + // Keyguard should not show while the communal hub is fully visible. This check + // is added since at the moment, closing the notification shade will cause the + // keyguard alpha to be set back to 1. + 0f + } else { + alpha + } + } .distinctUntilChanged() /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt index 3ea83ae21085..3afa49e50167 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt @@ -51,4 +51,14 @@ constructor( onCancel = { 1f }, name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha", ) + + // TODO(b/315205216): implement full animation spec instead of just a simple fade. + val notificationAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + onStep = { 1f - it }, + onFinish = { 0f }, + onCancel = { 1f }, + name = "LOCKSCREEN->GLANCEABLE_HUB: notificationAlpha", + ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt index 631a0b8471b8..437218f9f440 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHost.kt @@ -228,6 +228,14 @@ constructor( } } + override var expandedMatchesParentHeight: Boolean = false + set(value) { + if (value != field) { + field = value + changedListener?.invoke() + } + } + override var squishFraction: Float = 1.0f set(value) { if (!value.equals(field)) { @@ -282,6 +290,7 @@ constructor( override fun copy(): MediaHostState { val mediaHostState = MediaHostStateHolder() mediaHostState.expansion = expansion + mediaHostState.expandedMatchesParentHeight = expandedMatchesParentHeight mediaHostState.squishFraction = squishFraction mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia mediaHostState.measurementInput = measurementInput?.copy() @@ -360,6 +369,12 @@ interface MediaHostState { */ var expansion: Float + /** + * If true, the [EXPANDED] layout should stretch to match the height of its parent container, + * rather than having a fixed height. + */ + var expandedMatchesParentHeight: Boolean + /** Fraction of the height animation. */ var squishFraction: Float diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index be9393655c5d..962764c028fc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.res.Configuration import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import com.android.app.tracing.traceSection import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.media.controls.models.player.MediaViewHolder @@ -152,18 +153,11 @@ constructor( lastOrientation = newOrientation // Update the height of media controls for the expanded layout. it is needed // for large screen devices. - val backgroundIds = - if (type == TYPE.PLAYER) { - MediaViewHolder.backgroundIds - } else { - setOf(RecommendationViewHolder.backgroundId) - } - backgroundIds.forEach { id -> - expandedLayout.getConstraint(id).layout.mHeight = - context.resources.getDimensionPixelSize( - R.dimen.qs_media_session_height_expanded - ) - } + setBackgroundHeights( + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + ) } if (this@MediaViewController::configurationChangeListener.isInitialized) { configurationChangeListener.invoke() @@ -276,6 +270,17 @@ constructor( private fun constraintSetForExpansion(expansion: Float): ConstraintSet = if (expansion > 0) expandedLayout else collapsedLayout + /** Set the height of UMO background constraints. */ + private fun setBackgroundHeights(height: Int) { + val backgroundIds = + if (type == TYPE.PLAYER) { + MediaViewHolder.backgroundIds + } else { + setOf(RecommendationViewHolder.backgroundId) + } + backgroundIds.forEach { id -> expandedLayout.getConstraint(id).layout.mHeight = height } + } + /** * Set the views to be showing/hidden based on the [isGutsVisible] for a given * [TransitionViewState]. @@ -454,6 +459,18 @@ constructor( } // Let's create a new measurement if (state.expansion == 0.0f || state.expansion == 1.0f) { + if (state.expansion == 1.0f) { + val height = + if (state.expandedMatchesParentHeight) { + MATCH_CONSTRAINT + } else { + context.resources.getDimensionPixelSize( + R.dimen.qs_media_session_height_expanded + ) + } + setBackgroundHeights(height) + } + result = transitionLayout!!.calculateViewState( state.measurementInput!!, diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 0a72a2f15e95..068e5fd61ea4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -504,24 +504,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } }; - private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback = - new SurfaceChangedCallback() { - @Override - public void surfaceCreated(Transaction t) { - notifyNavigationBarSurface(); - } - - @Override - public void surfaceDestroyed() { - notifyNavigationBarSurface(); - } - - @Override - public void surfaceReplaced(Transaction t) { - notifyNavigationBarSurface(); - } - }; - private boolean mScreenPinningActive = false; private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override @@ -787,8 +769,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mView.getViewTreeObserver().addOnComputeInternalInsetsListener( mOnComputeInternalInsetsListener); - mView.getViewRootImpl().addSurfaceChangedCallback(mSurfaceChangedCallback); - notifyNavigationBarSurface(); mPipOptional.ifPresent(mView::addPipExclusionBoundsChangeListener); mBackAnimation.ifPresent(mView::registerBackAnimation); @@ -860,13 +840,8 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mHandler.removeCallbacks(mEnableLayoutTransitions); mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater); mPipOptional.ifPresent(mView::removePipExclusionBoundsChangeListener); - ViewRootImpl viewRoot = mView.getViewRootImpl(); - if (viewRoot != null) { - viewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback); - } mFrame = null; mOrientationHandle = null; - notifyNavigationBarSurface(); } // TODO: Remove this when we update nav bar recreation @@ -1026,17 +1001,6 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } } - private void notifyNavigationBarSurface() { - ViewRootImpl viewRoot = mView.getViewRootImpl(); - SurfaceControl surface = mView.getParent() != null - && viewRoot != null - && viewRoot.getSurfaceControl() != null - && viewRoot.getSurfaceControl().isValid() - ? viewRoot.getSurfaceControl() - : null; - mOverviewProxyService.onNavigationBarSurfaceChanged(surface); - } - private int deltaRotation(int oldRotation, int newRotation) { int delta = newRotation - oldRotation; if (delta < 0) delta += 4; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index a3b92541d593..a2dfc0159c6e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -27,8 +27,11 @@ import android.graphics.PointF; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.annotation.Nullable; + import com.android.systemui.Dumpable; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.res.R; @@ -53,6 +56,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { private QuickStatusBarHeader mHeader; private float mQsExpansion; private QSCustomizer mQSCustomizer; + private QSPanel mQSPanel; private NonInterceptingScrollView mQSPanelContainer; private int mHorizontalMargins; @@ -72,6 +76,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { protected void onFinishInflate() { super.onFinishInflate(); mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view); + mQSPanel = findViewById(R.id.quick_settings_panel); mHeader = findViewById(R.id.header); mQSCustomizer = findViewById(R.id.qs_customize); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); @@ -79,6 +84,13 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { void setSceneContainerEnabled(boolean enabled) { mSceneContainerEnabled = enabled; + if (enabled) { + mQSPanelContainer.removeAllViews(); + removeView(mQSPanelContainer); + LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + addView(mQSPanel, 0, lp); + } } @Override @@ -97,20 +109,26 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the // bottom and footer are inside the screen. - MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams(); - int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec); - int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin - - getPaddingBottom(); - int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin - + layoutParams.rightMargin; - final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding, - layoutParams.width); - mQSPanelContainer.measure(qsPanelWidthSpec, - MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST)); - int width = mQSPanelContainer.getMeasuredWidth() + padding; - super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY)); + + if (!mSceneContainerEnabled) { + MarginLayoutParams layoutParams = + (MarginLayoutParams) mQSPanelContainer.getLayoutParams(); + int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin + - getPaddingBottom(); + int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin + + layoutParams.rightMargin; + final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding, + layoutParams.width); + mQSPanelContainer.measure(qsPanelWidthSpec, + MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST)); + int width = mQSPanelContainer.getMeasuredWidth() + padding; + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY)); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + // QSCustomizer will always be the height of the screen, but do this after // other measuring to avoid changing the height of the QS. mQSCustomizer.measure(widthMeasureSpec, @@ -130,12 +148,15 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { @Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { - // Do not measure QSPanel again when doing super.onMeasure. - // This prevents the pages in PagedTileLayout to be remeasured with a different (incorrect) - // size to the one used for determining the number of rows and then the number of pages. - if (child != mQSPanelContainer) { - super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, - parentHeightMeasureSpec, heightUsed); + if (!mSceneContainerEnabled) { + // Do not measure QSPanel again when doing super.onMeasure. + // This prevents the pages in PagedTileLayout to be remeasured with a different + // (incorrect) size to the one used for determining the number of rows and then the + // number of pages. + if (child != mQSPanelContainer) { + super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, + parentHeightMeasureSpec, heightUsed); + } } } @@ -151,6 +172,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { updateClippingPath(); } + @Nullable public NonInterceptingScrollView getQSPanelContainer() { return mQSPanelContainer; } @@ -172,11 +194,19 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { .getDimensionPixelSize( R.dimen.large_screen_shade_header_height); } - mQSPanelContainer.setPaddingRelative( - mQSPanelContainer.getPaddingStart(), - mSceneContainerEnabled ? 0 : topPadding, - mQSPanelContainer.getPaddingEnd(), - mQSPanelContainer.getPaddingBottom()); + if (mQSPanelContainer != null) { + mQSPanelContainer.setPaddingRelative( + mQSPanelContainer.getPaddingStart(), + mSceneContainerEnabled ? 0 : topPadding, + mQSPanelContainer.getPaddingEnd(), + mQSPanelContainer.getPaddingBottom()); + } else { + mQSPanel.setPaddingRelative( + mQSPanel.getPaddingStart(), + mSceneContainerEnabled ? 0 : topPadding, + mQSPanel.getPaddingEnd(), + mQSPanel.getPaddingBottom()); + } int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin); int horizontalPadding = getResources().getDimensionPixelSize( @@ -220,7 +250,9 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { public void setExpansion(float expansion) { mQsExpansion = expansion; - mQSPanelContainer.setScrollingEnabled(expansion > 0f); + if (mQSPanelContainer != null) { + mQSPanelContainer.setScrollingEnabled(expansion > 0f); + } updateExpansion(); } @@ -239,7 +271,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { lp.rightMargin = mHorizontalMargins; lp.leftMargin = mHorizontalMargins; } - if (view == mQSPanelContainer) { + if (view == mQSPanelContainer || view == mQSPanel) { // QS panel lays out some of its content full width qsPanelController.setContentMargins(mContentHorizontalPadding, mContentHorizontalPadding); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java index 7b001c7b72f7..ffbc56098e26 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java @@ -81,6 +81,9 @@ public class QSContainerImplController extends ViewController<QSContainerImpl> { public void onInit() { mQuickStatusBarHeaderController.init(); mView.setSceneContainerEnabled(mSceneContainerEnabled); + if (mSceneContainerEnabled && mQsPanelController != null) { + mQSPanelContainer.setOnTouchListener(null); + } } public void setListening(boolean listening) { @@ -91,13 +94,17 @@ public class QSContainerImplController extends ViewController<QSContainerImpl> { protected void onViewAttached() { mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController); mConfigurationController.addCallback(mConfigurationListener); - mQSPanelContainer.setOnTouchListener(mContainerTouchHandler); + if (!mSceneContainerEnabled && mQSPanelContainer != null) { + mQSPanelContainer.setOnTouchListener(mContainerTouchHandler); + } } @Override protected void onViewDetached() { mConfigurationController.removeCallback(mConfigurationListener); - mQSPanelContainer.setOnTouchListener(null); + if (mQSPanelContainer != null) { + mQSPanelContainer.setOnTouchListener(null); + } } public QSContainerImpl getView() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java index 7f91fd2ebb80..290821e4ab13 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java @@ -61,6 +61,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; @@ -171,8 +172,11 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl private CommandQueue mCommandQueue; private View mRootView; + @Nullable private View mFooterActionsView; + private final SceneContainerFlags mSceneContainerFlags; + @Inject public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue, @@ -185,7 +189,8 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl FooterActionsViewModel.Factory footerActionsViewModelFactory, FooterActionsViewBinder footerActionsViewBinder, LargeScreenShadeInterpolator largeScreenShadeInterpolator, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + SceneContainerFlags sceneContainerFlags) { mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; mQsMediaHost = qsMediaHost; mQqsMediaHost = qqsMediaHost; @@ -201,6 +206,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mFooterActionsViewModelFactory = footerActionsViewModelFactory; mFooterActionsViewBinder = footerActionsViewBinder; mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner(); + mSceneContainerFlags = sceneContainerFlags; } /** @@ -216,10 +222,17 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mQSPanelController.init(); mQuickQSPanelController.init(); - mQSFooterActionsViewModel = mFooterActionsViewModelFactory - .create(mListeningAndVisibilityLifecycleOwner); - bindFooterActionsView(mRootView); - mFooterActionsController.init(); + if (!mSceneContainerFlags.isEnabled()) { + mQSFooterActionsViewModel = mFooterActionsViewModelFactory + .create(mListeningAndVisibilityLifecycleOwner); + bindFooterActionsView(mRootView); + mFooterActionsController.init(); + } else { + View footerView = mRootView.findViewById(R.id.qs_footer_actions); + if (footerView != null) { + ((ViewGroup) footerView.getParent()).removeView(footerView); + } + } mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view); mQSPanelScrollView.addOnLayoutChangeListener( @@ -234,6 +247,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mScrollListener.onQsPanelScrollChanged(scrollY); } }); + mQSPanelScrollView.setScrollingEnabled(!mSceneContainerFlags.isEnabled()); mHeader = mRootView.findViewById(R.id.header); mFooter = qsComponent.getQSFooter(); @@ -481,7 +495,9 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing || mHeaderAnimating || mShowCollapsedOnKeyguard); mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); - mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); + if (mFooterActionsView != null) { + mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); + } mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) || (mQsExpanded && !mStackScrollerOverscrolling)); mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE); @@ -622,8 +638,13 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl @Override public int getHeightDiff() { - return mQSPanelScrollView.getBottom() - mHeader.getBottom() - + mHeader.getPaddingBottom(); + if (mSceneContainerFlags.isEnabled()) { + return mQSPanelController.getViewBottom() - mHeader.getBottom() + + mHeader.getPaddingBottom(); + } else { + return mQSPanelScrollView.getBottom() - mHeader.getBottom() + + mHeader.getPaddingBottom(); + } } @Override @@ -678,25 +699,29 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); float footerActionsExpansion = onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion; - mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion, - mInSplitShade); + if (mQSFooterActionsViewModel != null) { + mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion, + mInSplitShade); + } mQSPanelController.setRevealExpansion(expansion); mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); - float qsScrollViewTranslation = - onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0; - mQSPanelScrollView.setTranslationY(qsScrollViewTranslation); + if (!mSceneContainerFlags.isEnabled()) { + float qsScrollViewTranslation = + onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0; + mQSPanelScrollView.setTranslationY(qsScrollViewTranslation); - if (fullyCollapsed) { - mQSPanelScrollView.setScrollY(0); - } + if (fullyCollapsed) { + mQSPanelScrollView.setScrollY(0); + } - if (!fullyExpanded) { - // Set bounds on the QS panel so it doesn't run over the header when animating. - mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY(); - mQsBounds.right = mQSPanelScrollView.getWidth(); - mQsBounds.bottom = mQSPanelScrollView.getHeight(); + if (!fullyExpanded) { + // Set bounds on the QS panel so it doesn't run over the header when animating. + mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY(); + mQsBounds.right = mQSPanelScrollView.getWidth(); + mQsBounds.bottom = mQSPanelScrollView.getHeight(); + } } updateQsBounds(); @@ -786,15 +811,17 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin, mQSPanelScrollView.getHeight()); } - mQSPanelScrollView.setClipBounds(mQsBounds); - - mQSPanelScrollView.getLocationOnScreen(mLocationTemp); - int left = mLocationTemp[0]; - int top = mLocationTemp[1]; - mQsMediaHost.getCurrentClipping().set(left, top, - left + getView().getMeasuredWidth(), - top + mQSPanelScrollView.getMeasuredHeight() - - mQSPanelController.getPaddingBottom()); + if (!mSceneContainerFlags.isEnabled()) { + mQSPanelScrollView.setClipBounds(mQsBounds); + + mQSPanelScrollView.getLocationOnScreen(mLocationTemp); + int left = mLocationTemp[0]; + int top = mLocationTemp[1]; + mQsMediaHost.getCurrentClipping().set(left, top, + left + getView().getMeasuredWidth(), + top + mQSPanelScrollView.getMeasuredHeight() + - mQSPanelController.getPaddingBottom()); + } } private void updateMediaPositions() { @@ -867,9 +894,15 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl // The customize state changed, so our height changed. mContainer.updateExpansion(); boolean customizing = isCustomizing(); - mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + if (mSceneContainerFlags.isEnabled()) { + mQSPanelController.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + } else { + mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + } mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); - mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + if (mFooterActionsView != null) { + mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + } mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); // Let the panel know the position changed and it needs to update where notifications // and whatnot are. diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 51b94dd983f3..7a7ee59fa63f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -387,7 +387,7 @@ public class QSPanel extends LinearLayout implements Tunable { setPaddingRelative(getPaddingStart(), mSceneContainerEnabled ? 0 : paddingTop, getPaddingEnd(), - paddingBottom); + mSceneContainerEnabled ? 0 : paddingBottom); } void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index ef58a608aa1f..c3f5086b0096 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -278,5 +278,9 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { public int getPaddingBottom() { return mView.getPaddingBottom(); } + + int getViewBottom() { + return mView.getBottom(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index a103566400a6..07705f361881 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -58,6 +58,7 @@ public class QSCustomizer extends LinearLayout { private final RecyclerView mRecyclerView; private boolean mCustomizing; private QSContainerController mQsContainerController; + private final Toolbar mToolbar; private QS mQs; private int mX; private int mY; @@ -69,15 +70,15 @@ public class QSCustomizer extends LinearLayout { LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this); mClipper = new QSDetailClipper(findViewById(R.id.customize_container)); - Toolbar toolbar = findViewById(com.android.internal.R.id.action_bar); + mToolbar = findViewById(com.android.internal.R.id.action_bar); TypedValue value = new TypedValue(); mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true); - toolbar.setNavigationIcon( + mToolbar.setNavigationIcon( getResources().getDrawable(value.resourceId, mContext.getTheme())); - toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset) + mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset) .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - toolbar.setTitle(R.string.qs_edit); + mToolbar.setTitle(R.string.qs_edit); mRecyclerView = findViewById(android.R.id.list); mTransparentView = findViewById(R.id.customizer_transparent_view); DefaultItemAnimator animator = new DefaultItemAnimator(); @@ -184,6 +185,14 @@ public class QSCustomizer extends LinearLayout { return isShown; } + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mToolbar.setTitleTextAppearance(mContext, + android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title); + updateToolbarMenuFontSize(); + } + void setCustomizing(boolean customizing) { mCustomizing = customizing; if (mQs != null) { @@ -269,4 +278,11 @@ public class QSCustomizer extends LinearLayout { lp.height = QSUtils.getQsHeaderSystemIconsAreaHeight(mContext); mTransparentView.setLayoutParams(lp); } + + private void updateToolbarMenuFontSize() { + // Clearing and re-adding the toolbar action force updates the font size + mToolbar.getMenu().clear(); + mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt index 456520051f58..c5eeb2f87d3a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt @@ -249,6 +249,10 @@ open class QSTileViewImpl @JvmOverloads constructor( height = iconSize marginEnd = endMargin } + + background = createTileBackground() + setColor(backgroundColor) + setOverlayColor(backgroundOverlayColor) } private fun createAndAddLabels() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt index ce840eec29d9..0d4339680dac 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt @@ -17,10 +17,13 @@ package com.android.systemui.qs.ui.adapter import android.content.Context +import android.content.pm.ActivityInfo import android.os.Bundle import android.view.View import androidx.annotation.VisibleForTesting import androidx.asynclayoutinflater.view.AsyncLayoutInflater +import com.android.settingslib.applications.InterestingConfigChanges +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -58,7 +61,7 @@ interface QSSceneAdapter { /** * Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in - * [qsView] + * [qsView]. Re-inflations due to configuration changes will use the last used [context]. */ suspend fun inflate(context: Context) @@ -90,6 +93,7 @@ constructor( private val qsImplProvider: Provider<QSImpl>, @Main private val mainDispatcher: CoroutineDispatcher, @Application applicationScope: CoroutineScope, + private val configurationInteractor: ConfigurationInteractor, private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater, ) : QSContainerController, QSSceneAdapter { @@ -99,7 +103,15 @@ constructor( qsImplProvider: Provider<QSImpl>, @Main dispatcher: CoroutineDispatcher, @Application scope: CoroutineScope, - ) : this(qsSceneComponentFactory, qsImplProvider, dispatcher, scope, ::AsyncLayoutInflater) + configurationInteractor: ConfigurationInteractor, + ) : this( + qsSceneComponentFactory, + qsImplProvider, + dispatcher, + scope, + configurationInteractor, + ::AsyncLayoutInflater, + ) private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED) private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -109,14 +121,36 @@ constructor( val qsImpl = _qsImpl.asStateFlow() override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull() + // Same config changes as in FragmentHostManager + private val interestingChanges = + InterestingConfigChanges( + ActivityInfo.CONFIG_FONT_SCALE or + ActivityInfo.CONFIG_LOCALE or + ActivityInfo.CONFIG_ASSETS_PATHS + ) + init { applicationScope.launch { - state.sample(_isCustomizing, ::Pair).collect { (state, customizing) -> - _qsImpl.value?.apply { - if (state != QSSceneAdapter.State.QS && customizing) { - this@apply.closeCustomizerImmediately() + launch { + state.sample(_isCustomizing, ::Pair).collect { (state, customizing) -> + _qsImpl.value?.apply { + if (state != QSSceneAdapter.State.QS && customizing) { + this@apply.closeCustomizerImmediately() + } + applyState(state) + } + } + } + launch { + configurationInteractor.configurationValues.collect { config -> + if (interestingChanges.applyNewConfig(config)) { + // Assumption: The context is always the same and with the same theme. + // If colors change they will be reflected as attributes in the theme. + qsImpl.value?.view?.let { inflate(it.context) } + } else { + qsImpl.value?.onConfigurationChanged(config) + qsImpl.value?.view?.dispatchConfigurationChanged(config) } - applyState(state) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt index e5e1e8445e94..8a900ece2750 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -16,7 +16,10 @@ package com.android.systemui.qs.ui.viewmodel +import androidx.lifecycle.LifecycleOwner import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey @@ -24,6 +27,7 @@ import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.flow.map @@ -35,6 +39,8 @@ constructor( val shadeHeaderViewModel: ShadeHeaderViewModel, val qsSceneAdapter: QSSceneAdapter, val notifications: NotificationsPlaceholderViewModel, + private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, + private val footerActionsController: FooterActionsController, ) { val destinationScenes = qsSceneAdapter.isCustomizing.map { customizing -> @@ -47,4 +53,13 @@ constructor( ) } } + + private val footerActionsControllerInitialized = AtomicBoolean(false) + + fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { + if (footerActionsControllerInitialized.compareAndSet(false, true)) { + footerActionsController.init() + } + return footerActionsViewModelFactory.create(lifecycleOwner) + } } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index fd53423e7f22..cc53aabfcd25 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -169,7 +169,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private final DisplayTracker mDisplayTracker; private Region mActiveNavBarRegion; - private SurfaceControl mNavigationBarSurface; private IOverviewProxy mOverviewProxy; private int mConnectionBackoffAttempts; @@ -488,7 +487,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis Log.e(TAG_OPS, "Failed to call onInitialize()", e); } dispatchNavButtonBounds(); - dispatchNavigationBarSurface(); // Force-update the systemui state flags updateSystemUiStateFlags(); @@ -679,28 +677,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis .commitUpdate(mContext.getDisplayId()); } - /** - * Called when the navigation bar surface is created or changed - */ - public void onNavigationBarSurfaceChanged(SurfaceControl navbarSurface) { - mNavigationBarSurface = navbarSurface; - dispatchNavigationBarSurface(); - } - - private void dispatchNavigationBarSurface() { - try { - if (mOverviewProxy != null) { - // Catch all for cases where the surface is no longer valid - if (mNavigationBarSurface != null && !mNavigationBarSurface.isValid()) { - mNavigationBarSurface = null; - } - mOverviewProxy.onNavigationBarSurface(mNavigationBarSurface); - } - } catch (RemoteException e) { - Log.e(TAG_OPS, "Failed to notify back action", e); - } - } - private void updateEnabledAndBinding() { updateEnabledState(); startConnectionToCurrentUser(); @@ -1075,7 +1051,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis pw.print(" mInputFocusTransferStartY="); pw.println(mInputFocusTransferStartY); pw.print(" mInputFocusTransferStartMillis="); pw.println(mInputFocusTransferStartMillis); pw.print(" mActiveNavBarRegion="); pw.println(mActiveNavBarRegion); - pw.print(" mNavigationBarSurface="); pw.println(mNavigationBarSurface); pw.print(" mNavBarMode="); pw.println(mNavBarMode); mSysUiState.dump(pw, args); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 782d6519468c..3362ebc8ebb4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -55,7 +55,15 @@ constructor( * The width of the area in which a right edge swipe can open the hub, in pixels. Read from * resources when [initView] is called. */ - private var edgeSwipeRegionWidth: Int = 0 + // TODO(b/320786721): support RTL layouts + private var rightEdgeSwipeRegionWidth: Int = 0 + + /** + * The height of the area in which a top edge swipe while the hub is open will not intercept + * touches, in pixels. This allows the top edge swipe to instead open the notification shade. + * Read from resources when [initView] is called. + */ + private var topEdgeSwipeRegionWidth: Int = 0 /** * True if we are currently tracking a gesture for opening the hub that started in the edge @@ -63,6 +71,9 @@ constructor( */ private var isTrackingOpenGesture = false + /** True if we are currently tracking a touch on the hub while it's open. */ + private var isTrackingHubTouch = false + /** * True if the hub UI is fully open, meaning it should receive touch input. * @@ -113,8 +124,14 @@ constructor( communalContainerView = containerView - edgeSwipeRegionWidth = - communalContainerView.resources.getDimensionPixelSize(R.dimen.communal_grid_gutter_size) + rightEdgeSwipeRegionWidth = + communalContainerView.resources.getDimensionPixelSize( + R.dimen.communal_right_edge_swipe_region_width + ) + topEdgeSwipeRegionWidth = + communalContainerView.resources.getDimensionPixelSize( + R.dimen.communal_top_edge_swipe_region_height + ) collectFlow( communalContainerView, @@ -157,17 +174,38 @@ constructor( // fully showing state val hubOccluded = anyBouncerShowing || shadeShowing - // If the hub is fully visible, send all touch events to it. - val communalVisible = hubShowing && !hubOccluded - if (communalVisible) { + // If the hub is fully visible, send all touch events to it, other than top and bottom edge + // swipes. + if (hubShowing && isDown) { + val y = ev.rawY + val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth + + // TODO(b/315207481): allow bottom edge swipes to open the bouncer + if (topSwipe) { + // Don't intercept touches at the top edge so that swipes can open the notification + // shade. + return false + } + + if (!hubOccluded) { + isTrackingHubTouch = true + dispatchTouchEvent(ev) + // Return true regardless of dispatch result as some touches at the start of a + // gesture may return false from dispatchTouchEvent. + return true + } + } else if (isTrackingHubTouch) { + if (isUp || isCancel) { + isTrackingHubTouch = false + } dispatchTouchEvent(ev) // Return true regardless of dispatch result as some touches at the start of a gesture // may return false from dispatchTouchEvent. return true } - if (edgeSwipeRegionWidth == 0) { - // If the edge region width has not been read yet or whatever reason, don't bother + if (rightEdgeSwipeRegionWidth == 0) { + // If the edge region width has not been read yet for whatever reason, don't bother // intercepting touches to open the hub. return false } @@ -175,7 +213,7 @@ constructor( if (!isTrackingOpenGesture && isDown) { val x = ev.rawX val inOpeningSwipeRegion: Boolean = - x >= communalContainerView.width - edgeSwipeRegionWidth + x >= communalContainerView.width - rightEdgeSwipeRegionWidth if (inOpeningSwipeRegion && !hubOccluded) { isTrackingOpenGesture = true dispatchTouchEvent(ev) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 60feb82bf4aa..00534743588c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -50,6 +50,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dumpable; import com.android.systemui.biometrics.AuthController; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -118,6 +119,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW private final Lazy<SelectedUserInteractor> mUserInteractor; private final Lazy<ShadeInteractor> mShadeInteractorLazy; private final SceneContainerFlags mSceneContainerFlags; + private final Lazy<CommunalInteractor> mCommunalInteractor; private ViewGroup mWindowRootView; private LayoutParams mLp; private boolean mHasTopUi; @@ -165,7 +167,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW ShadeWindowLogger logger, Lazy<SelectedUserInteractor> userInteractor, UserTracker userTracker, - SceneContainerFlags sceneContainerFlags) { + SceneContainerFlags sceneContainerFlags, + Lazy<CommunalInteractor> communalInteractor) { mContext = context; mWindowRootViewComponentFactory = windowRootViewComponentFactory; mWindowManager = windowManager; @@ -184,6 +187,7 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mAuthController = authController; mUserInteractor = userInteractor; mSceneContainerFlags = sceneContainerFlags; + mCommunalInteractor = communalInteractor; mLastKeyguardRotationAllowed = mKeyguardStateController.isKeyguardScreenRotationAllowed(); mLockScreenDisplayTimeout = context.getResources() .getInteger(R.integer.config_lockScreenDisplayTimeout); @@ -325,6 +329,11 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW mShadeInteractorLazy.get().isQsExpanded(), this::onQsExpansionChanged ); + collectFlow( + mWindowRootView, + mCommunalInteractor.get().isCommunalShowing(), + this::onCommunalShowingChanged + ); } @Override @@ -501,14 +510,21 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } private void applyUserActivityTimeout(NotificationShadeWindowState state) { - if (state.isKeyguardShowingAndNotOccluded() + final Boolean communalShowing = state.isCommunalShowingAndNotOccluded(); + final Boolean keyguardShowing = state.isKeyguardShowingAndNotOccluded(); + long timeout = -1; + if ((communalShowing || keyguardShowing) && state.statusBarState == StatusBarState.KEYGUARD && !state.qsExpanded) { - mLpChanged.userActivityTimeout = state.bouncerShowing - ? KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS : mLockScreenDisplayTimeout; - } else { - mLpChanged.userActivityTimeout = -1; + if (state.bouncerShowing) { + timeout = KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS; + } else if (communalShowing) { + timeout = CommunalInteractor.AWAKE_INTERVAL_MS; + } else if (keyguardShowing) { + timeout = mLockScreenDisplayTimeout; + } } + mLpChanged.userActivityTimeout = timeout; } private void applyInputFeatures(NotificationShadeWindowState state) { @@ -607,7 +623,8 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW state.forcePluginOpen, state.dozing, state.scrimsVisibility, - state.backgroundBlurRadius + state.backgroundBlurRadius, + state.communalShowing ); } @@ -731,6 +748,12 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW apply(mCurrentState); } + @VisibleForTesting + void onCommunalShowingChanged(Boolean showing) { + mCurrentState.communalShowing = showing; + apply(mCurrentState); + } + @Override public void setForceUserActivity(boolean forceUserActivity) { mCurrentState.forceUserActivity = forceUserActivity; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt index 0b20170834d8..f9c9d83e03aa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt @@ -58,12 +58,17 @@ class NotificationShadeWindowState( @JvmField var dreaming: Boolean = false, @JvmField var scrimsVisibility: Int = 0, @JvmField var backgroundBlurRadius: Int = 0, + @JvmField var communalShowing: Boolean = false, ) { fun isKeyguardShowingAndNotOccluded(): Boolean { return keyguardShowing && !keyguardOccluded } + fun isCommunalShowingAndNotOccluded(): Boolean { + return communalShowing && !keyguardOccluded + } + /** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */ val asStringList: List<String> by lazy { listOf( @@ -93,7 +98,8 @@ class NotificationShadeWindowState( forcePluginOpen.toString(), dozing.toString(), scrimsVisibility.toString(), - backgroundBlurRadius.toString() + backgroundBlurRadius.toString(), + communalShowing.toString(), ) } @@ -134,6 +140,7 @@ class NotificationShadeWindowState( dozing: Boolean, scrimsVisibility: Int, backgroundBlurRadius: Int, + communalShowing: Boolean, ) { buffer.advance().apply { this.keyguardShowing = keyguardShowing @@ -165,6 +172,7 @@ class NotificationShadeWindowState( this.dozing = dozing this.scrimsVisibility = scrimsVisibility this.backgroundBlurRadius = backgroundBlurRadius + this.communalShowing = communalShowing } } @@ -209,7 +217,8 @@ class NotificationShadeWindowState( "forcePluginOpen", "dozing", "scrimsVisibility", - "backgroundBlurRadius" + "backgroundBlurRadius", + "communalShowing" ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt index 46806e66cb8c..54b6ad71e734 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt @@ -790,7 +790,7 @@ private fun <R> HeadsUpManager.modifyHuns(block: (HunMutator) -> R): R { /** Mutates the HeadsUp state of notifications. */ private interface HunMutator { - fun updateNotification(key: String, alert: Boolean) + fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) fun removeNotification(key: String, releaseImmediately: Boolean) } @@ -801,8 +801,8 @@ private interface HunMutator { private class HunMutatorImpl(private val headsUpManager: HeadsUpManager) : HunMutator { private val deferred = mutableListOf<Pair<String, Boolean>>() - override fun updateNotification(key: String, alert: Boolean) { - headsUpManager.updateNotification(key, alert) + override fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) { + headsUpManager.updateNotification(key, shouldHeadsUpAgain) } override fun removeNotification(key: String, releaseImmediately: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index 380cdadd1361..ae4ba27775b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.os.UserHandle import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags.screenshareNotificationHiding import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.StatusBarState @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import dagger.Binds import dagger.Module @@ -55,6 +57,8 @@ class SensitiveContentCoordinatorImpl @Inject constructor( private val statusBarStateController: StatusBarStateController, private val keyguardStateController: KeyguardStateController, private val selectedUserInteractor: SelectedUserInteractor, + private val sensitiveNotificationProtectionController: + SensitiveNotificationProtectionController, ) : Invalidator("SensitiveContentInvalidator"), SensitiveContentCoordinator, DynamicPrivacyController.Listener, @@ -82,10 +86,13 @@ class SensitiveContentCoordinatorImpl @Inject constructor( return } + val isSensitiveContentProtectionActive = screenshareNotificationHiding() && + sensitiveNotificationProtectionController.isSensitiveStateActive val currentUserId = lockscreenUserManager.currentUserId val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId) - val deviceSensitive = devicePublic && - !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId) + val deviceSensitive = (devicePublic && + !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) || + isSensitiveContentProtectionActive val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) { val notifUserId = entry.sbn.user.identifier @@ -105,9 +112,13 @@ class SensitiveContentCoordinatorImpl @Inject constructor( else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId) } } + + val shouldProtectNotification = screenshareNotificationHiding() && + sensitiveNotificationProtectionController.shouldProtectNotification(entry) + val needsRedaction = lockscreenUserManager.needsRedaction(entry) val isSensitive = userPublic && needsRedaction - entry.setSensitive(isSensitive, deviceSensitive) + entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt index 0bc8e682c891..f375ebce2de3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt @@ -187,7 +187,7 @@ object NotificationIconContainerViewBinder { configuration.getDimensionPixelSize(RInternal.dimen.status_bar_icon_size_sp) val iconHorizontalPaddingFlow: Flow<Int> = configuration.getDimensionPixelSize(R.dimen.status_bar_icon_horizontal_margin) - val layoutParams: Flow<FrameLayout.LayoutParams> = + val layoutParams: StateFlow<FrameLayout.LayoutParams> = combine(iconSizeFlow, iconHorizontalPaddingFlow, systemBarUtilsState.statusBarHeight) { iconSize, iconHPadding, @@ -206,7 +206,7 @@ object NotificationIconContainerViewBinder { private suspend fun Flow<NotificationIconsViewData>.bindIcons( view: NotificationIconContainer, - layoutParams: Flow<FrameLayout.LayoutParams>, + layoutParams: StateFlow<FrameLayout.LayoutParams>, notifyBindingFailures: (Collection<String>) -> Unit, viewStore: IconViewStore, bindIcon: suspend (iconKey: String, view: StatusBarIconView) -> Unit, @@ -259,7 +259,7 @@ object NotificationIconContainerViewBinder { // added again. removeTransientView(sbiv) } - view.addView(sbiv) + view.addView(sbiv, layoutParams.value) boundViewsByNotifKey.remove(notifKey)?.second?.cancel() boundViewsByNotifKey[notifKey] = Pair( @@ -267,7 +267,9 @@ object NotificationIconContainerViewBinder { launch { launch { layoutParams.collectTracingEach("SBIV#bindLayoutParams") { - sbiv.layoutParams = it + if (it != sbiv.layoutParams) { + sbiv.layoutParams = it + } } } bindIcon(notifKey, sbiv) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java index 3a59978d415c..46ddba4d4d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java @@ -65,9 +65,7 @@ public abstract class NotificationRowModule { CallLayoutSetDataAsyncFactory callLayoutSetDataAsyncFactory ) { final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>(); - if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) { - replacementFactories.add(precomputedTextViewFactory); - } + replacementFactories.add(precomputedTextViewFactory); if (featureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) { replacementFactories.add(bigPictureLayoutInflaterFactory); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt new file mode 100644 index 000000000000..a21dd9bb9579 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notification avalanche suppression flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationAvalancheSuppression { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationAvalancheSuppression() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 6a66bb74f16d..1143481863f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -22,6 +22,7 @@ import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_N import static com.android.app.animation.Interpolators.STANDARD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; +import static com.android.systemui.Flags.screenshareNotificationHiding; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener; @@ -135,6 +136,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; @@ -218,6 +220,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final SecureSettings mSecureSettings; private final NotificationDismissibilityProvider mDismissibilityProvider; private final ActivityStarter mActivityStarter; + private final SensitiveNotificationProtectionController + mSensitiveNotificationProtectionController; private View mLongPressedView; @@ -295,6 +299,15 @@ public class NotificationStackScrollLayoutController implements Dumpable { } }; + private final Runnable mSensitiveStateChangedListener = new Runnable() { + @Override + public void run() { + // Animate false to protect against screen recording capturing content + // during the animation + updateSensitivenessWithAnimation(false); + } + }; + private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> { if (mView.isExpanded()) { // The bottom might change because we're using the final actual height of the view @@ -342,6 +355,12 @@ public class NotificationStackScrollLayoutController implements Dumpable { private float mMaxAlphaForExpansion = 1.0f; private float mMaxAlphaForUnhide = 1.0f; + /** + * Maximum alpha when to and from or sitting idle on the glanceable hub. Will be 1.0f when the + * hub is not visible or transitioning. + */ + private float mMaxAlphaForGlanceableHub = 1.0f; + private final NotificationListViewBinder mViewBinder; private void updateResources() { @@ -399,7 +418,20 @@ public class NotificationStackScrollLayoutController implements Dumpable { } private void updateSensitivenessWithAnimation(boolean animate) { - mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); + Trace.beginSection("NSSLC.updateSensitivenessWithAnimation"); + if (screenshareNotificationHiding()) { + boolean isAnyProfilePublic = mLockscreenUserManager.isAnyProfilePublicMode(); + boolean isSensitiveContentProtectionActive = + mSensitiveNotificationProtectionController.isSensitiveStateActive(); + boolean isSensitive = isAnyProfilePublic || isSensitiveContentProtectionActive; + + // Only animate if in a non-sensitive state (not screen sharing) + boolean shouldAnimate = animate && !isSensitiveContentProtectionActive; + mView.updateSensitiveness(shouldAnimate, isSensitive); + } else { + mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode()); + } + Trace.endSection(); } /** @@ -708,7 +740,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { SecureSettings secureSettings, NotificationDismissibilityProvider dismissibilityProvider, ActivityStarter activityStarter, - SplitShadeStateController splitShadeStateController) { + SplitShadeStateController splitShadeStateController, + SensitiveNotificationProtectionController sensitiveNotificationProtectionController) { mView = view; mKeyguardTransitionRepo = keyguardTransitionRepo; mViewBinder = viewBinder; @@ -756,6 +789,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { mSecureSettings = secureSettings; mDismissibilityProvider = dismissibilityProvider; mActivityStarter = activityStarter; + mSensitiveNotificationProtectionController = sensitiveNotificationProtectionController; mView.passSplitShadeStateController(splitShadeStateController); mDumpManager.registerDumpable(this); updateResources(); @@ -860,6 +894,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); mDeviceProvisionedListener.onDeviceProvisionedChanged(); + if (screenshareNotificationHiding()) { + mSensitiveNotificationProtectionController + .registerSensitiveStateListener(mSensitiveStateChangedListener); + } + if (mView.isAttachedToWindow()) { mOnAttachStateChangeListener.onViewAttachedToWindow(mView); } @@ -1252,9 +1291,19 @@ public class NotificationStackScrollLayoutController implements Dumpable { updateAlpha(); } + /** + * Sets the max alpha value for notifications when idle on the glanceable hub or when + * transitioning to/from the glanceable hub. + */ + public void setMaxAlphaForGlanceableHub(float alpha) { + mMaxAlphaForGlanceableHub = alpha; + updateAlpha(); + } + private void updateAlpha() { if (mView != null) { - mView.setAlpha(Math.min(mMaxAlphaForExpansion, mMaxAlphaForUnhide)); + mView.setAlpha(Math.min(mMaxAlphaForExpansion, + Math.min(mMaxAlphaForUnhide, mMaxAlphaForGlanceableHub))); } } @@ -1746,6 +1795,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mMaxAlphaForExpansion=" + mMaxAlphaForExpansion); pw.println("mMaxAlphaForUnhide=" + mMaxAlphaForUnhide); + pw.println("mMaxAlphaForGlanceableHub=" + mMaxAlphaForGlanceableHub); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 12927b87630e..f842e304ffdf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.dagger.qualifiers.Main @@ -125,7 +124,14 @@ object SharedNotificationContainerBinder { launch { viewModel.translationY.collect { controller.setTranslationY(it) } } - launch { viewModel.alpha.collect { controller.setMaxAlphaForExpansion(it) } } + launch { + viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) } + } + launch { + viewModel.glanceableHubAlpha.collect { + controller.setMaxAlphaForGlanceableHub(it) + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index a48fb45861d2..99cd89b84c14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.common.shared.model.NotificationContainerBounds +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -27,6 +28,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -61,8 +64,11 @@ constructor( private val keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val shadeInteractor: ShadeInteractor, + communalInteractor: CommunalInteractor, occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel ) { private val statesForConstrainedNotifications = setOf( @@ -87,6 +93,20 @@ constructor( .distinctUntilChanged() .onStart { emit(false) } + private val lockscreenToGlanceableHubRunning = + keyguardTransitionInteractor + .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + .map { it.transitionState == STARTED || it.transitionState == RUNNING } + .distinctUntilChanged() + .onStart { emit(false) } + + private val glanceableHubToLockscreenRunning = + keyguardTransitionInteractor + .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN) + .map { it.transitionState == STARTED || it.transitionState == RUNNING } + .distinctUntilChanged() + .onStart { emit(false) } + val shadeCollapseFadeInComplete = MutableStateFlow(false) val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = @@ -144,6 +164,24 @@ constructor( initialValue = false, ) + /** Are we purely on the glanceable hub without the shade/qs? */ + internal val isOnGlanceableHubWithoutShade: Flow<Boolean> = + combine( + communalInteractor.isIdleOnCommunal, + // Shade with notifications + shadeInteractor.shadeExpansion.map { it > 0f }, + // Shade without notifications, quick settings only (pull down from very top on + // lockscreen) + shadeInteractor.qsExpansion.map { it > 0f }, + ) { isIdleOnCommunal, isShadeVisible, qsExpansion -> + isIdleOnCommunal && !(isShadeVisible || qsExpansion) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + /** Fade in only for use after the shade collapses */ val shadeCollpaseFadeIn: Flow<Boolean> = flow { @@ -201,7 +239,7 @@ constructor( initialValue = NotificationContainerBounds(), ) - val alpha: Flow<Float> = + val expansionAlpha: Flow<Float> = // Due to issues with the legacy shade, some shade expansion events are sent incorrectly, // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while @@ -235,6 +273,43 @@ constructor( } /** + * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition + * or idle on the glanceable hub. + * + * Must return 1.0f when not controlling the alpha since notifications does a min of all the + * alpha sources. + */ + val glanceableHubAlpha: Flow<Float> = + isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade -> + combineTransform( + lockscreenToGlanceableHubRunning, + glanceableHubToLockscreenRunning, + merge( + lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, + glanceableHubToLockscreenTransitionViewModel.notificationAlpha, + ) + .onStart { + // Transition flows don't emit a value on start, kick things off so the + // combine starts. + emit(1f) + } + ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha -> + if (isOnGlanceableHubWithoutShade) { + // Notifications should not be visible on the glanceable hub. + // TODO(b/321075734): implement a way to actually set the notifications to gone + // while on the hub instead of just adjusting alpha + emit(0f) + } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) { + emit(alpha) + } else { + // Not on the hub and no transitions running, return full visibility so we don't + // block the notifications from showing. + emit(1f) + } + } + } + + /** * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be * translated as the keyguard fades out. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index ae04eaf49b65..aabe4a0d66f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -60,6 +60,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.ScrimAlpha; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -217,6 +218,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private final ScreenOffAnimationController mScreenOffAnimationController; private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private final KeyguardInteractor mKeyguardInteractor; private GradientColors mColors; private boolean mNeedsDrawableColorUpdate; @@ -298,7 +300,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump DozeParameters dozeParameters, AlarmManager alarmManager, KeyguardStateController keyguardStateController, - DelayedWakeLock.Builder delayedWakeLockBuilder, + DelayedWakeLock.Factory delayedWakeLockFactory, Handler handler, KeyguardUpdateMonitor keyguardUpdateMonitor, DockManager dockManager, @@ -311,6 +313,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, AlternateBouncerToGoneTransitionViewModel alternateBouncerToGoneTransitionViewModel, KeyguardTransitionInteractor keyguardTransitionInteractor, + KeyguardInteractor keyguardInteractor, WallpaperRepository wallpaperRepository, @Main CoroutineDispatcher mainDispatcher, LargeScreenShadeInterpolator largeScreenShadeInterpolator) { @@ -328,7 +331,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mScreenOffAnimationController = screenOffAnimationController; mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout, "hide_aod_wallpaper", mHandler); - mWakeLock = delayedWakeLockBuilder.setHandler(mHandler).setTag("Scrims").build(); + mWakeLock = delayedWakeLockFactory.create("Scrims"); // Scrim alpha is initially set to the value on the resource but might be changed // to make sure that text on top of it is legible. mDozeParameters = dozeParameters; @@ -357,6 +360,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel; mAlternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel; mKeyguardTransitionInteractor = keyguardTransitionInteractor; + mKeyguardInteractor = keyguardInteractor; mWallpaperRepository = wallpaperRepository; mMainDispatcher = mainDispatcher; } @@ -759,7 +763,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump // see: b/186644628 mNotificationsScrim.setDrawableBounds(left - 1, top, right + 1, bottom); mScrimBehind.setBottomEdgePosition((int) top); + mKeyguardInteractor.setTopClippingBounds((int) top); } else { + mKeyguardInteractor.setTopClippingBounds(null); mNotificationsScrim.setDrawableBounds(left, top, right, bottom); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt new file mode 100644 index 000000000000..252945f1ed6a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/OemSatelliteInputLog.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.dagger + +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import javax.inject.Qualifier + +/** Detailed [DeviceBasedSatelliteRepository] logs */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class OemSatelliteInputLog 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 e309c32df64e..2b90e649a154 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 @@ -265,6 +265,13 @@ abstract class StatusBarPipelineModule { return factory.create("VerboseMobileViewLog", 100) } + @Provides + @SysUISingleton + @OemSatelliteInputLog + fun provideOemSatelliteInputLog(factory: LogBufferFactory): LogBuffer { + return factory.create("DeviceBasedSatelliteInputLog", 32) + } + const val FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON = "FirstMobileSubShowingNetworkTypeIcon" } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt index e3c3139f6906..8400fb08e147 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.icons.shared import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon import javax.inject.Inject /** @@ -38,11 +39,12 @@ interface BindableIconsRegistry { class BindableIconsRegistryImpl @Inject constructor( -/** Bindables go here */ + /** Bindables go here */ + oemSatellite: DeviceBasedSatelliteBindableIcon ) : BindableIconsRegistry { /** * Adding the injected bindables to this list will get them registered with * StatusBarIconController */ - override val bindableIcons: List<BindableIcon> = listOf() + override val bindableIcons: List<BindableIcon> = listOf(oemSatellite) } 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 bc38b538f8c0..a608be39f7f7 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 @@ -72,6 +72,9 @@ interface MobileConnectionRepository { */ val isInService: StateFlow<Boolean> + /** Reflects [android.telephony.ServiceState.isUsingNonTerrestrialNetwork] */ + val isNonTerrestrial: StateFlow<Boolean> + /** True if [android.telephony.SignalStrength] told us that this connection is using GSM */ val isGsm: StateFlow<Boolean> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt index b2a773375cd5..6de7a00b5a10 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepository.kt @@ -32,6 +32,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullM import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_GSM import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_IN_SERVICE +import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_NTN import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING @@ -109,6 +110,17 @@ class DemoMobileConnectionRepository( ) .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value) + private val _isNonTerrestrial = MutableStateFlow(false) + override val isNonTerrestrial = + _isNonTerrestrial + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_NTN, + _isNonTerrestrial.value + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), _isNonTerrestrial.value) + private val _isGsm = MutableStateFlow(false) override val isGsm = _isGsm @@ -227,6 +239,7 @@ class DemoMobileConnectionRepository( (event.activity ?: TelephonyManager.DATA_ACTIVITY_NONE).toMobileDataActivityModel() _carrierNetworkChangeActive.value = event.carrierNetworkChange _resolvedNetworkType.value = resolvedNetworkType + _isNonTerrestrial.value = event.ntn isAllowedDuringAirplaneMode.value = false hasPrioritizedNetworkCapabilities.value = event.slice 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 4cd877eb1a14..11a61a9fd319 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 @@ -77,6 +77,7 @@ constructor( val roaming = getString("roam") == "show" val name = getString("networkname") ?: "demo mode" val slice = getString("slice").toBoolean() + val ntn = getString("ntn").toBoolean() return Mobile( level = level, @@ -89,6 +90,7 @@ constructor( roaming = roaming, name = name, slice = slice, + ntn = ntn, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt index 0aa95f8821cc..4836abe73f86 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt @@ -37,6 +37,7 @@ sealed interface FakeNetworkEventModel { val roaming: Boolean, val name: String, val slice: Boolean = false, + val ntn: Boolean = false, ) : FakeNetworkEventModel data class MobileDisabled( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt index e5a5695d3655..f8858c5037ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt @@ -168,6 +168,7 @@ class CarrierMergedConnectionRepository( override val isEmergencyOnly = MutableStateFlow(false).asStateFlow() override val operatorAlphaShort = MutableStateFlow(null).asStateFlow() override val isInService = MutableStateFlow(true).asStateFlow() + override val isNonTerrestrial = MutableStateFlow(false).asStateFlow() override val isGsm = MutableStateFlow(false).asStateFlow() override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt index 48bf7ac60ba7..a1241965de7a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt @@ -175,6 +175,21 @@ class FullMobileConnectionRepository( ) .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value) + override val isNonTerrestrial = + activeRepo + .flatMapLatest { it.isNonTerrestrial } + .logDiffsForTable( + tableLogBuffer, + columnPrefix = "", + columnName = COL_IS_NTN, + activeRepo.value.isNonTerrestrial.value + ) + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + activeRepo.value.isNonTerrestrial.value + ) + override val isGsm = activeRepo .flatMapLatest { it.isGsm } @@ -366,6 +381,7 @@ class FullMobileConnectionRepository( const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChangeActive" const val COL_CDMA_LEVEL = "cdmaLevel" const val COL_EMERGENCY = "emergencyOnly" + const val COL_IS_NTN = "isNtn" const val COL_IS_GSM = "isGsm" const val COL_IS_IN_SERVICE = "isInService" const val COL_OPERATOR = "operatorName" 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 f44401ba4702..77fd6bef8a33 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 @@ -227,6 +227,12 @@ class MobileConnectionRepositoryImpl( .map { Utils.isInService(it.serviceState) } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isNonTerrestrial = + callbackEvents + .mapNotNull { it.onServiceStateChanged } + .map { it.serviceState.isUsingNonTerrestrialNetwork } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isGsm = callbackEvents .mapNotNull { it.onSignalStrengthChanged } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index de46a5ed99d6..0e6775685611 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -19,11 +19,18 @@ package com.android.systemui.statusbar.pipeline.satellite.data.prod import android.os.OutcomeReceiver import android.telephony.satellite.NtnSignalStrengthCallback import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS import android.telephony.satellite.SatelliteModemStateCallback +import androidx.annotation.VisibleForTesting 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.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.core.MessageInitializer +import com.android.systemui.log.core.MessagePrinter +import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported @@ -62,8 +69,10 @@ private typealias SupportedSatelliteManager = SatelliteManager /** * "Supported" here means supported by the device. The value of this should be stable during the * process lifetime. + * + * @VisibleForTesting */ -private sealed interface SatelliteSupport { +sealed interface SatelliteSupport { /** Not yet fetched */ data object Unknown : SatelliteSupport @@ -123,6 +132,7 @@ constructor( satelliteManagerOpt: Optional<SatelliteManager>, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, + @OemSatelliteInputLog private val logBuffer: LogBuffer, private val systemClock: SystemClock, ) : DeviceBasedSatelliteRepository { @@ -132,7 +142,8 @@ constructor( // Some calls into satellite manager will throw exceptions if it is not supported. // This is never expected to change after boot, but may need to be retried in some cases - private val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown) + @get:VisibleForTesting + val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown) init { satelliteManager = satelliteManagerOpt.getOrNull() @@ -145,6 +156,11 @@ constructor( ensureMinUptime(systemClock, MIN_UPTIME) satelliteSupport.value = satelliteManager.checkSatelliteSupported() + logBuffer.i( + { str1 = satelliteSupport.value.toString() }, + { "Checked for system support. support=$str1" }, + ) + // We only need to check location availability if this mode is supported if (satelliteSupport.value is Supported) { isSatelliteAllowedForCurrentLocation.subscriptionCount @@ -159,6 +175,9 @@ constructor( * connection might cause more frequent checks. */ while (true) { + logBuffer.i { + "requestIsSatelliteCommunicationAllowedForCurrentLocation" + } checkIsSatelliteAllowed() delay(POLLING_INTERVAL_MS) } @@ -167,6 +186,8 @@ constructor( } } } else { + logBuffer.i { "Satellite manager is null" } + satelliteSupport.value = NotSupported } } @@ -181,12 +202,21 @@ constructor( private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> = conflatedCallbackFlow { val cb = SatelliteModemStateCallback { state -> + logBuffer.i({ int1 = state }) { "onSatelliteModemStateChanged: state=$int1" } trySend(SatelliteConnectionState.fromModemState(state)) } - sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) + var registered = false - awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) } + try { + val res = + sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb) + registered = res == SATELLITE_RESULT_SUCCESS + } catch (e: Exception) { + logBuffer.e("error registering for modem state", e) + } + + awaitClose { if (registered) sm.unregisterForSatelliteModemStateChanged(cb) } } .flowOn(bgDispatcher) @@ -197,12 +227,21 @@ constructor( private fun signalStrengthFlow(sm: SupportedSatelliteManager) = conflatedCallbackFlow { val cb = NtnSignalStrengthCallback { signalStrength -> + logBuffer.i({ int1 = signalStrength.level }) { + "onNtnSignalStrengthChanged: level=$int1" + } trySend(signalStrength.level) } - sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) + var registered = false + try { + sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb) + registered = true + } catch (e: Exception) { + logBuffer.e("error registering for signal strength", e) + } - awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) } + awaitClose { if (registered) sm.unregisterForNtnSignalStrengthChanged(cb) } } .flowOn(bgDispatcher) @@ -213,11 +252,15 @@ constructor( bgDispatcher.asExecutor(), object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> { override fun onError(e: SatelliteManager.SatelliteException) { - android.util.Log.e(TAG, "Found exception when checking for satellite: ", e) + logBuffer.e( + "Found exception when checking availability", + e, + ) isSatelliteAllowedForCurrentLocation.value = false } override fun onResult(allowed: Boolean) { + logBuffer.i { allowed.toString() } isSatelliteAllowedForCurrentLocation.value = allowed } } @@ -239,12 +282,27 @@ constructor( } override fun onError(error: SatelliteManager.SatelliteException) { + logBuffer.e( + "Exception when checking for satellite support. " + + "Assuming it is not supported for this device.", + error, + ) + // Assume that an error means it's not supported continuation.resume(NotSupported) } } - requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb) + try { + requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb) + } catch (error: Exception) { + logBuffer.e( + "Exception when checking for satellite support. " + + "Assuming it is not supported for this device.", + error, + ) + continuation.resume(NotSupported) + } } companion object { @@ -264,5 +322,19 @@ constructor( delay(timeTilMinUptime) } } + + /** A couple of convenience logging methods rather than a whole class */ + private fun LogBuffer.i( + initializer: MessageInitializer = {}, + printer: MessagePrinter, + ) = this.log(TAG, LogLevel.INFO, initializer, printer) + + private fun LogBuffer.e(message: String, exception: Throwable? = null) = + this.log( + tag = TAG, + level = LogLevel.ERROR, + message = message, + exception = exception, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt new file mode 100644 index 000000000000..f5d0f6b8f07c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/DeviceBasedSatelliteBindableIcon.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.ui + +import android.content.Context +import com.android.internal.telephony.flags.Flags.oemEnabledSatelliteFlag +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator +import com.android.systemui.statusbar.pipeline.satellite.ui.binder.DeviceBasedSatelliteIconBinder +import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView +import javax.inject.Inject + +@SysUISingleton +class DeviceBasedSatelliteBindableIcon +@Inject +constructor( + context: Context, + viewModel: DeviceBasedSatelliteViewModel, +) : BindableIcon { + override val slot: String = + context.getString(com.android.internal.R.string.status_bar_oem_satellite) + + override val initializer = ModernStatusBarViewCreator { context -> + SingleBindableStatusBarIconView.createView(context).also { view -> + view.initView(slot) { DeviceBasedSatelliteIconBinder.bind(view, viewModel) } + } + } + + override val shouldBindIcon: Boolean = oemEnabledSatelliteFlag() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt new file mode 100644 index 000000000000..59ac5f29b66c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/binder/DeviceBasedSatelliteIconBinder.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.ui.binder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.common.ui.binder.IconViewBinder +import com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel.DeviceBasedSatelliteViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarIconView +import kotlinx.coroutines.launch + +object DeviceBasedSatelliteIconBinder { + fun bind( + view: SingleBindableStatusBarIconView, + viewModel: DeviceBasedSatelliteViewModel, + ): ModernStatusBarViewBinding { + return SingleBindableStatusBarIconView.withDefaultBinding( + view = view, + shouldBeVisible = { viewModel.icon.value != null } + ) { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.icon.collect { newIcon -> + if (newIcon == null) { + view.iconView.setImageDrawable(null) + } else { + IconViewBinder.bind(newIcon, view.iconView) + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt new file mode 100644 index 000000000000..6938d667ca81 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.ui.model + +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.res.R +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState + +/** + * Define the [Icon] that relates to a given satellite connection state + level. Note that for now + * We don't need any data class box, so we can just use a simple mapping function. + */ +object SatelliteIconModel { + fun fromConnectionState( + connectionState: SatelliteConnectionState, + signalStrength: Int, + ): Icon? = + when (connectionState) { + // TODO(b/316635648): check if this should be null + SatelliteConnectionState.Unknown, + SatelliteConnectionState.Off, + SatelliteConnectionState.On -> + Icon.Resource( + res = R.drawable.ic_satellite_not_connected, + contentDescription = null, + ) + SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength) + } + + private fun fromSignalStrength( + signalStrength: Int, + ): Icon? = + // TODO(b/316634365): these need content descriptions + when (signalStrength) { + // No signal + 0 -> Icon.Resource(res = R.drawable.ic_satellite_connected_0, contentDescription = null) + + // Poor -> Moderate + 1, + 2 -> Icon.Resource(res = R.drawable.ic_satellite_connected_1, contentDescription = null) + + // Good -> Great + 3, + 4 -> Icon.Resource(res = R.drawable.ic_satellite_connected_2, contentDescription = null) + else -> null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt new file mode 100644 index 000000000000..0051161eff35 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel + +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor +import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn + +/** + * View-Model for the device-based satellite icon. This icon will only show in the status bar if + * satellite is available AND all other service states are considered OOS. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class DeviceBasedSatelliteViewModel +@Inject +constructor( + interactor: DeviceBasedSatelliteInteractor, + @Application scope: CoroutineScope, +) { + private val shouldShowIcon: StateFlow<Boolean> = + interactor.areAllConnectionsOutOfService + .flatMapLatest { allOos -> + if (!allOos) { + flowOf(false) + } else { + interactor.isSatelliteAllowed + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + val icon: StateFlow<Icon?> = + combine( + shouldShowIcon, + interactor.connectionState, + interactor.signalStrength, + ) { shouldShow, state, signalStrength -> + if (shouldShow) { + SatelliteIconModel.fromConnectionState(state, signalStrength) + } else { + null + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt index 3b87bed2e0ef..25a2c9dd3caf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt @@ -103,7 +103,7 @@ open class ModernStatusBarView(context: Context, attrs: AttributeSet?) : * * Creates a dot view, and uses [bindingCreator] to get and set the binding. */ - fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { + open fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot // view. So, this is the required order. this.slot = slot diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt new file mode 100644 index 000000000000..c663c37fec98 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconView.kt @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.shared.ui.view + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +/** Simple single-icon view that is bound to bindable_status_bar_icon.xml */ +class SingleBindableStatusBarIconView( + context: Context, + attrs: AttributeSet?, +) : ModernStatusBarView(context, attrs) { + + internal lateinit var iconView: ImageView + internal lateinit var dotView: StatusBarIconView + + override fun toString(): String { + return "SingleBindableStatusBarIcon(" + + "slot='$slot', " + + "isCollecting=${binding.isCollecting()}, " + + "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " + + "viewString=${super.toString()}" + } + + override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { + super.initView(slot, bindingCreator) + + iconView = requireViewById(R.id.icon_view) + dotView = requireViewById(R.id.status_bar_dot) + } + + companion object { + fun createView( + context: Context, + ): SingleBindableStatusBarIconView { + return LayoutInflater.from(context).inflate(R.layout.bindable_status_bar_icon, null) + as SingleBindableStatusBarIconView + } + + /** + * Using a given binding [block], create the necessary scaffolding to handle the general + * case of a single status bar icon. This includes eliding into a dot view when there is not + * enough space, and handling tint. + * + * [block] should be a simple [launch] call that handles updating the single icon view with + * its new view. Currently there is no simple way to e.g., extend to handle multiple tints + * for dual-layered icons, and any more complex logic should probably find a way to return + * its own version of [ModernStatusBarViewBinding]. + */ + fun withDefaultBinding( + view: SingleBindableStatusBarIconView, + shouldBeVisible: () -> Boolean, + block: suspend LifecycleOwner.(View) -> Unit + ): SingleBindableStatusBarIconViewBinding { + @StatusBarIconView.VisibleState + val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) + + val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) + val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) + + var isCollecting: Boolean = false + + view.repeatWhenAttached { + // Child binding + block(view) + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // isVisible controls the visibility state of the outer group, and thus it + // needs + // to run in the CREATED lifecycle so it can continue to watch while + // invisible + // See (b/291031862) for details + launch { + visibilityState.collect { visibilityState -> + // for b/296864006, we can not hide all the child views if + // visibilityState is STATE_HIDDEN. Because hiding all child views + // would cause the + // getWidth() of this view return 0, and that would cause the + // translation + // calculation fails in StatusIconContainer. Therefore, like class + // MobileIconBinder, instead of set the child views visibility to + // View.GONE, + // we set their visibility to View.INVISIBLE to make them invisible + // but + // keep the width. + ModernStatusBarViewVisibilityHelper.setVisibilityState( + visibilityState, + view.iconView, + view.dotView, + ) + } + } + + launch { + iconTint.collect { tint -> + val tintList = ColorStateList.valueOf(tint) + view.iconView.imageTintList = tintList + view.dotView.setDecorColor(tint) + } + } + + launch { + decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) } + } + + try { + awaitCancellation() + } finally { + isCollecting = false + } + } + } + } + + return object : SingleBindableStatusBarIconViewBinding { + override val decorTint: Int + get() = decorTint.value + + override val iconTint: Int + get() = iconTint.value + + override fun getShouldIconBeVisible(): Boolean { + return shouldBeVisible() + } + + override fun onVisibilityStateChanged(state: Int) { + visibilityState.value = state + } + + override fun onIconTintChanged(newTint: Int, contrastTint: Int) { + iconTint.value = newTint + } + + override fun onDecorTintChanged(newTint: Int) { + decorTint.value = newTint + } + + override fun isCollecting(): Boolean { + return isCollecting + } + } + } + } +} + +@VisibleForTesting +interface SingleBindableStatusBarIconViewBinding : ModernStatusBarViewBinding { + val iconTint: Int + val decorTint: Int +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 1528c9befda7..1414150c5511 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -159,7 +159,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { public void showNotification(@NonNull NotificationEntry entry) { mLogger.logShowNotification(entry); addEntry(entry); - updateNotification(entry.getKey(), true /* show */); + updateNotification(entry.getKey(), true /* shouldHeadsUpAgain */); entry.setInterruption(); } @@ -190,12 +190,12 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { /** * Called when the notification state has been updated. * @param key the key of the entry that was updated - * @param show whether the notification should show again and force reevaluation of - * removal time + * @param shouldHeadsUpAgain whether the notification should show again and force reevaluation + * of removal time */ - public void updateNotification(@NonNull String key, boolean show) { + public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) { HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key); - mLogger.logUpdateNotification(key, show, headsUpEntry != null); + mLogger.logUpdateNotification(key, shouldHeadsUpAgain, headsUpEntry != null); if (headsUpEntry == null) { // the entry was released before this update (i.e by a listener) This can happen // with the groupmanager @@ -204,7 +204,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - if (show) { + if (shouldHeadsUpAgain) { headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification"); if (headsUpEntry != null) { setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt index b8c7e202ce7c..a7352be8d80a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt @@ -182,7 +182,7 @@ interface HeadsUpManager : Dumpable { */ fun unpinAll(userUnPinned: Boolean) - fun updateNotification(key: String, alert: Boolean) + fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) } /** Sets the animation state of the HeadsUpManager. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java new file mode 100644 index 000000000000..970cc75bbb6b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +/** + * A controller which provides the current sensitive notification protections status as well as + * to assist in feature usage and exemptions + */ +public interface SensitiveNotificationProtectionController { + /** + * Register a runnable that triggers on changes to protection state + * + * <p> onSensitiveStateChanged not invoked on registration + */ + void registerSensitiveStateListener(Runnable onSensitiveStateChanged); + + /** Unregister a previously registered onSensitiveStateChanged runnable */ + void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged); + + /** Return {@code true} if device in state in which notifications should be protected */ + boolean isSensitiveStateActive(); + + /** Return {@code true} when notification should be protected */ + boolean shouldProtectNotification(NotificationEntry entry); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java new file mode 100644 index 000000000000..3c4ca4465874 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy; + +import static com.android.systemui.Flags.screenshareNotificationHiding; + +import android.media.projection.MediaProjectionInfo; +import android.media.projection.MediaProjectionManager; +import android.os.Handler; +import android.os.Trace; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.util.ListenerSet; + +import javax.inject.Inject; + +/** Implementation of SensitiveNotificationProtectionController. **/ +@SysUISingleton +public class SensitiveNotificationProtectionControllerImpl + implements SensitiveNotificationProtectionController { + private final MediaProjectionManager mMediaProjectionManager; + private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); + private volatile MediaProjectionInfo mProjection; + + @VisibleForTesting + final MediaProjectionManager.Callback mMediaProjectionCallback = + new MediaProjectionManager.Callback() { + @Override + public void onStart(MediaProjectionInfo info) { + Trace.beginSection( + "SNPC.onProjectionStart"); + mProjection = info; + mListeners.forEach(Runnable::run); + Trace.endSection(); + } + + @Override + public void onStop(MediaProjectionInfo info) { + Trace.beginSection( + "SNPC.onProjectionStop"); + mProjection = null; + mListeners.forEach(Runnable::run); + Trace.endSection(); + } + }; + + @Inject + public SensitiveNotificationProtectionControllerImpl( + MediaProjectionManager mediaProjectionManager, + @Main Handler mainHandler) { + mMediaProjectionManager = mediaProjectionManager; + + if (screenshareNotificationHiding()) { + mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler); + } + } + + @Override + public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) { + mListeners.addIfAbsent(onSensitiveStateChanged); + } + + @Override + public void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged) { + mListeners.remove(onSensitiveStateChanged); + } + + @Override + public boolean isSensitiveStateActive() { + // TODO(b/316955558): Add disabled by developer option + // TODO(b/316955306): Add feature exemption for sysui and bug handlers + // TODO(b/316955346): Add feature exemption for single app screen sharing + return mProjection != null; + } + + @Override + public boolean shouldProtectNotification(NotificationEntry entry) { + if (!isSensitiveStateActive()) { + return false; + } + + // Exempt foreground service notifications from protection in effort to keep screen share + // stop actions easily accessible + // TODO(b/316955208): Exempt FGS notifications only for app that started projection + return !entry.getSbn().getNotification().isFgsOrUij(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index 3304b9827fd8..15200bd0ac54 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -60,6 +60,8 @@ import com.android.systemui.statusbar.policy.RotationLockController; import com.android.systemui.statusbar.policy.RotationLockControllerImpl; import com.android.systemui.statusbar.policy.SecurityController; import com.android.systemui.statusbar.policy.SecurityControllerImpl; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionControllerImpl; import com.android.systemui.statusbar.policy.SplitShadeStateController; import com.android.systemui.statusbar.policy.SplitShadeStateControllerImpl; import com.android.systemui.statusbar.policy.UserInfoController; @@ -146,6 +148,11 @@ public interface StatusBarPolicyModule { /** */ @Binds + SensitiveNotificationProtectionController provideSensitiveNotificationProtectionController( + SensitiveNotificationProtectionControllerImpl controllerImpl); + + /** */ + @Binds UserInfoController provideUserInfoContrller(UserInfoControllerImpl controllerImpl); /** */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt index ce811e205436..10137a0d3430 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ui/SystemBarUtilsState.kt @@ -17,10 +17,16 @@ package com.android.systemui.statusbar.ui import com.android.internal.policy.SystemBarUtils +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onConfigChanged import javax.inject.Inject +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -31,6 +37,8 @@ import kotlinx.coroutines.flow.onStart class SystemBarUtilsState @Inject constructor( + @Background bgContext: CoroutineContext, + @Main mainContext: CoroutineContext, configurationController: ConfigurationController, proxy: SystemBarUtilsProxy, ) { @@ -38,5 +46,10 @@ constructor( val statusBarHeight: Flow<Int> = configurationController.onConfigChanged .onStart<Any> { emit(Unit) } + .flowOn(mainContext) + .conflate() .map { proxy.getStatusBarHeight() } + .distinctUntilChanged() + .flowOn(bgContext) + .conflate() } diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java index 972895d4a192..039109e9ddc6 100644 --- a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java +++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java @@ -19,7 +19,11 @@ package com.android.systemui.util.wakelock; import android.content.Context; import android.os.Handler; -import javax.inject.Inject; +import com.android.systemui.dagger.qualifiers.Background; + +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; /** * A wake lock that has a built in delay when releasing to give the framebuffer time to update. @@ -32,9 +36,11 @@ public class DelayedWakeLock implements WakeLock { private final Handler mHandler; private final WakeLock mInner; - public DelayedWakeLock(Handler h, WakeLock inner) { - mHandler = h; - mInner = inner; + @AssistedInject + public DelayedWakeLock(@Background Handler handler, Context context, WakeLockLogger logger, + @Assisted String tag) { + mInner = WakeLock.createPartial(context, logger, tag); + mHandler = handler; } @Override @@ -58,46 +64,11 @@ public class DelayedWakeLock implements WakeLock { } /** - * An injectable builder for {@see DelayedWakeLock} that has the context already filled in. + * Factory to create the instance of DelayedWakeLock class. */ - public static class Builder { - private final Context mContext; - private final WakeLockLogger mLogger; - private String mTag; - private Handler mHandler; - - /** - * Constructor for DelayedWakeLock.Builder - */ - @Inject - public Builder(Context context, WakeLockLogger logger) { - mContext = context; - mLogger = logger; - } - - /** - * Set the tag for the WakeLock. - */ - public Builder setTag(String tag) { - mTag = tag; - - return this; - } - - /** - * Set the handler for the DelayedWakeLock. - */ - public Builder setHandler(Handler handler) { - mHandler = handler; - - return this; - } - - /** - * Build the DelayedWakeLock. - */ - public DelayedWakeLock build() { - return new DelayedWakeLock(mHandler, WakeLock.createPartial(mContext, mLogger, mTag)); - } + @AssistedFactory + public interface Factory { + /** creates the instance of DelayedWakeLock class. */ + DelayedWakeLock create(String tag); } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 9ee3d220a79b..aee441a13a5d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -535,6 +535,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } if (changed && fromKey) { Events.writeEvent(Events.EVENT_KEY, stream, lastAudibleStreamVolume); + mCallbacks.onVolumeChangedFromKey(); } return changed; } @@ -1030,6 +1031,18 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override + public void onVolumeChangedFromKey() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onVolumeChangedFromKey(); + } + }); + } + } + + @Override public void onAccessibilityModeChanged(Boolean showA11yStream) { boolean show = showA11yStream != null && showA11yStream; for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 404621d1fe81..b127c5160bba 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -34,6 +34,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; +import static com.android.systemui.Flags.hapticVolumeSlider; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -117,7 +118,11 @@ import com.android.internal.view.RotationPolicy; import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.Prefs; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin; +import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; @@ -125,6 +130,7 @@ import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.VolumeDialogController.State; import com.android.systemui.plugins.VolumeDialogController.StreamState; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -140,6 +146,9 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + /** * Visual presentation of the volume dialog. * @@ -303,6 +312,10 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private int mOrientation; private final Lazy<SecureSettings> mSecureSettings; private int mDialogTimeoutMillis; + private final CoroutineDispatcher mMainDispatcher; + private final CoroutineScope mApplicationScope; + private final VibratorHelper mVibratorHelper; + private final com.android.systemui.util.time.SystemClock mSystemClock; public VolumeDialogImpl( Context context, @@ -319,11 +332,18 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, DevicePostureController devicePostureController, Looper looper, DumpManager dumpManager, - Lazy<SecureSettings> secureSettings) { + Lazy<SecureSettings> secureSettings, + VibratorHelper vibratorHelper, + @Main CoroutineDispatcher mainDispatcher, + @Application CoroutineScope applicationScope, + com.android.systemui.util.time.SystemClock systemClock) { mContext = new ContextThemeWrapper(context, R.style.volume_dialog_theme); mHandler = new H(looper); - + mMainDispatcher = mainDispatcher; + mApplicationScope = applicationScope; + mVibratorHelper = vibratorHelper; + mSystemClock = systemClock; mShouldListenForJank = shouldListenForJank; mController = volumeDialogController; mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); @@ -839,6 +859,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)}); } row.slider = row.view.findViewById(R.id.volume_row_slider); + row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope); row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); row.number = row.view.findViewById(R.id.volume_number); @@ -1480,6 +1501,12 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mController.getCaptionsComponentState(false); checkODICaptionsTooltip(false); updateBackgroundForDrawerClosedAmount(); + for (int i = 0; i < mRows.size(); i++) { + VolumeRow row = mRows.get(i); + if (row.slider.getVisibility() == VISIBLE) { + row.addHaptics(); + } + } Trace.endSection(); } @@ -1532,7 +1559,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, protected void dismissH(int reason) { Trace.beginSection("VolumeDialogImpl#dismissH"); - + for (int i = 0; i < mRows.size(); i++) { + mRows.get(i).removeHaptics(); + } Log.i(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason] + " from: " + Debug.getCaller()); @@ -2358,6 +2387,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkForSwitchState) { updateCaptionsEnabledH(isEnabled, checkForSwitchState); } + + @Override + public void onVolumeChangedFromKey() { + VolumeRow activeRow = getActiveRow(); + if (activeRow.mHapticPlugin != null) { + activeRow.mHapticPlugin.onKeyDown(); + } + } }; @VisibleForTesting void onPostureChanged(int posture) { @@ -2459,6 +2496,15 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (mRow.ss == null) return; + if (getActiveRow().equals(mRow) + && mRow.slider.getVisibility() == VISIBLE + && mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser); + if (!fromUser) { + // Consider a change from program as the volume key being continuously pressed + mRow.mHapticPlugin.onKeyDown(); + } + } if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) + " onProgressChanged " + progress + " fromUser=" + fromUser); if (!fromUser) return; @@ -2485,6 +2531,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onStartTrackingTouch(SeekBar seekBar) { if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); + if (mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onStartTrackingTouch(seekBar); + } mController.setActiveStream(mRow.stream); mRow.tracking = true; } @@ -2492,6 +2541,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onStopTrackingTouch(SeekBar seekBar) { if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); + if (mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onStopTrackingTouch(seekBar); + } mRow.tracking = false; mRow.userAttempt = SystemClock.uptimeMillis(); final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); @@ -2524,6 +2576,22 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } private static class VolumeRow { + private static final SliderHapticFeedbackConfig sSliderHapticFeedbackConfig = + new SliderHapticFeedbackConfig( + /* velocityInterpolatorFactor= */ 1f, + /* progressInterpolatorFactor= */ 1f, + /* progressBasedDragMinScale= */ 0f, + /* progressBasedDragMaxScale= */ 0.2f, + /* additionalVelocityMaxBump= */ 0.15f, + /* deltaMillisForDragInterval= */ 0f, + /* deltaProgressForDragThreshold= */ 0.015f, + /* numberOfLowTicks= */ 5, + /* maxVelocityToScale= */ 300f, + /* velocityAxis= */ MotionEvent.AXIS_Y, + /* upperBookendScale= */ 1f, + /* lowerBookendScale= */ 0.05f, + /* exponent= */ 1f / 0.89f); + private View view; private TextView header; private ImageButton icon; @@ -2544,6 +2612,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private ObjectAnimator anim; // slider progress animation for non-touch-related updates private int animTargetProgress; private int lastAudibleLevel = 1; + private SeekableSliderHapticPlugin mHapticPlugin; void setIcon(int iconRes, Resources.Theme theme) { if (icon != null) { @@ -2554,6 +2623,50 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, sliderProgressIcon.setDrawable(view.getResources().getDrawable(iconRes, theme)); } } + + void createPlugin( + VibratorHelper vibratorHelper, + com.android.systemui.util.time.SystemClock systemClock, + CoroutineDispatcher mainDispatcher, + CoroutineScope applicationScope) { + if (!hapticVolumeSlider() || mHapticPlugin != null) return; + + mHapticPlugin = new SeekableSliderHapticPlugin( + vibratorHelper, + systemClock, + mainDispatcher, + applicationScope, + sSliderHapticFeedbackConfig); + } + + + @SuppressLint("ClickableViewAccessibility") + void addTouchListener() { + slider.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (mHapticPlugin != null) { + mHapticPlugin.onTouchEvent(motionEvent); + } + return false; + } + }); + } + + void addHaptics() { + if (mHapticPlugin != null) { + addTouchListener(); + mHapticPlugin.start(); + } + } + + @SuppressLint("ClickableViewAccessibility") + void removeHaptics() { + slider.setOnTouchListener(null); + if (mHapticPlugin != null) { + mHapticPlugin.stop(); + } + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 497c4cb070f0..f180a942af70 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -22,16 +22,20 @@ import android.os.Looper; import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.SystemClock; import com.android.systemui.volume.CsdWarningDialog; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.volume.VolumeDialogComponent; @@ -49,6 +53,9 @@ import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; import dagger.multibindings.IntoSet; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + /** Dagger Module for code in the volume package. */ @Module( subcomponents = { @@ -90,7 +97,11 @@ public interface VolumeModule { CsdWarningDialog.Factory csdFactory, DevicePostureController devicePostureController, DumpManager dumpManager, - Lazy<SecureSettings> secureSettings) { + Lazy<SecureSettings> secureSettings, + VibratorHelper vibratorHelper, + @Main CoroutineDispatcher mainDispatcher, + @Application CoroutineScope applicationScope, + SystemClock systemClock) { VolumeDialogImpl impl = new VolumeDialogImpl( context, volumeDialogController, @@ -106,7 +117,11 @@ public interface VolumeModule { devicePostureController, Looper.getMainLooper(), dumpManager, - secureSettings); + secureSettings, + vibratorHelper, + mainDispatcher, + applicationScope, + systemClock); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); impl.setAutomute(true); impl.setSilentMode(false); diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt index b8f95832b852..1ba269e25dff 100644 --- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt @@ -24,7 +24,7 @@ import android.content.IntentFilter import android.os.UserHandle import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.model.SelectedUserModel import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository @@ -54,7 +54,7 @@ interface WallpaperRepository { class WallpaperRepositoryImpl @Inject constructor( - @Application scope: CoroutineScope, + @Background scope: CoroutineScope, broadcastDispatcher: BroadcastDispatcher, userRepository: UserRepository, private val wallpaperManager: WallpaperManager, diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index c2efc05132ba..c2efc05132ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index 0959f1b2bcf6..0959f1b2bcf6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index 1281e4409a83..1281e4409a83 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 5e5273b779c6..bc9a0a5484ac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -181,16 +181,16 @@ public class MenuViewLayerTest extends SysuiTestCase { @Test public void onAttachedToWindow_menuIsVisible() { mMenuViewLayer.onAttachedToWindow(); - final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); + final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); assertThat(menuView.getVisibility()).isEqualTo(VISIBLE); } @Test - public void onAttachedToWindow_menuIsGone() { + public void onDetachedFromWindow_menuIsGone() { mMenuViewLayer.onDetachedFromWindow(); - final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); + final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); assertThat(menuView.getVisibility()).isEqualTo(GONE); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt new file mode 100644 index 000000000000..df73cc8f0212 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.ui + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.keyboard.data.repository.FakeStickyKeysRepository +import com.android.systemui.keyboard.data.repository.keyboardRepository +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.ComponentSystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class StickyKeysIndicatorCoordinatorTest : SysuiTestCase() { + + private lateinit var coordinator: StickyKeysIndicatorCoordinator + private val testScope = TestScope(StandardTestDispatcher()) + private val stickyKeysRepository = FakeStickyKeysRepository() + private val dialog = mock<ComponentSystemUIDialog>() + + @Before + fun setup() { + Assume.assumeTrue(ComposeFacade.isComposeAvailable()) + val dialogFactory = mock<SystemUIDialogFactory> { + whenever(applicationContext).thenReturn(context) + whenever(create(any(), anyInt(), anyBoolean())).thenReturn(dialog) + } + val keyboardRepository = Kosmos().keyboardRepository + val viewModel = StickyKeysIndicatorViewModel( + stickyKeysRepository, + keyboardRepository, + testScope.backgroundScope) + coordinator = StickyKeysIndicatorCoordinator( + testScope.backgroundScope, + dialogFactory, + viewModel, + mock<StickyKeysLogger>()) + coordinator.startListening() + keyboardRepository.setIsAnyKeyboardConnected(true) + } + + @Test + fun dialogIsShownWhenStickyKeysAreEmitted() { + testScope.run { + verifyZeroInteractions(dialog) + + stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true))) + runCurrent() + + verify(dialog).show() + } + } + + @Test + fun dialogDisappearsWhenStickyKeysAreEmpty() { + testScope.run { + verifyZeroInteractions(dialog) + + stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true))) + runCurrent() + stickyKeysRepository.setStickyKeys(linkedMapOf()) + runCurrent() + + verify(dialog).dismiss() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt index d397fc202637..8a713688acf9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -46,6 +47,7 @@ import org.mockito.ArgumentCaptor import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class StickyKeysIndicatorViewModelTest : SysuiTestCase() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index b57cf53911e9..754a7fd81475 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -97,6 +97,7 @@ import com.android.systemui.flags.Flags; import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.log.SessionTracker; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.scene.FakeWindowRootViewComponent; @@ -150,6 +151,7 @@ import kotlinx.coroutines.test.TestScope; @TestableLooper.RunWithLooper @SmallTest public class KeyguardViewMediatorTest extends SysuiTestCase { + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private KeyguardViewMediator mViewMediator; private final TestScope mTestScope = TestScopeProvider.getTestScope(); @@ -265,7 +267,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mShadeWindowLogger, () -> mSelectedUserInteractor, mUserTracker, - mSceneContainerFlags); + mSceneContainerFlags, + mKosmos::getCommunalInteractor); mFeatureFlags = new FakeFeatureFlags(); mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER); 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 90eaa5a2eeaf..dafd9e64371b 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,8 +23,9 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.flags.FakeFeatureFlags @@ -34,6 +35,8 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.InWindowLauncherUnlockAnimationRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel @@ -42,11 +45,15 @@ import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -57,7 +64,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runCurrent @@ -83,7 +89,8 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(JUnit4::class) class KeyguardTransitionScenariosTest : SysuiTestCase() { - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var keyguardRepository: FakeKeyguardRepository private lateinit var bouncerRepository: FakeKeyguardBouncerRepository @@ -119,17 +126,14 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - val testDispatcher = StandardTestDispatcher() - testScope = TestScope(testDispatcher) - keyguardRepository = FakeKeyguardRepository() - bouncerRepository = FakeKeyguardBouncerRepository() + keyguardRepository = kosmos.fakeKeyguardRepository + bouncerRepository = kosmos.fakeKeyguardBouncerRepository commandQueue = FakeCommandQueue() - shadeRepository = FakeShadeRepository() - transitionRepository = spy(FakeKeyguardTransitionRepository()) + shadeRepository = kosmos.fakeShadeRepository + transitionRepository = spy(kosmos.fakeKeyguardTransitionRepository) powerInteractor = PowerInteractorFactory.create().powerInteractor - communalInteractor = - CommunalInteractorFactory.create(testScope = testScope).communalInteractor + communalInteractor = kosmos.communalInteractor whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN) @@ -160,8 +164,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -184,8 +188,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromPrimaryBouncerTransitionInteractor = FromPrimaryBouncerTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -199,8 +203,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromDreamingTransitionInteractor = FromDreamingTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -210,8 +214,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromDreamingLockscreenHostedTransitionInteractor = FromDreamingLockscreenHostedTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -221,8 +225,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromAodTransitionInteractor = FromAodTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -232,8 +236,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromGoneTransitionInteractor = FromGoneTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -244,8 +248,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromDozingTransitionInteractor = FromDozingTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -257,8 +261,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromOccludedTransitionInteractor = FromOccludedTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -269,8 +273,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromAlternateBouncerTransitionInteractor = FromAlternateBouncerTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, @@ -281,8 +285,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { fromGlanceableHubTransitionInteractor = FromGlanceableHubTransitionInteractor( scope = testScope, - bgDispatcher = testDispatcher, - mainDispatcher = testDispatcher, + bgDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, glanceableHubTransitions = glanceableHubTransitions, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 6be92756ba8a..ba7927d6b8bd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -25,8 +25,7 @@ import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardViewController import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.data.repository.FakeCommunalRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController @@ -43,6 +42,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController +import com.android.systemui.testKosmos import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock @@ -79,6 +79,8 @@ import org.mockito.junit.MockitoJUnit @TestableLooper.RunWithLooper(setAsMainLooper = true) class MediaHierarchyManagerTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Mock private lateinit var lockHost: MediaHost @Mock private lateinit var qsHost: MediaHost @Mock private lateinit var qqsHost: MediaHost @@ -110,10 +112,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { private lateinit var isQsBypassingShade: MutableStateFlow<Boolean> private lateinit var mediaFrame: ViewGroup private val configurationController = FakeConfigurationController() - private val communalRepository = - FakeCommunalRepository(applicationScope = testScope.backgroundScope) - private val communalInteractor = - CommunalInteractorFactory.create(communalRepository = communalRepository).communalInteractor + private val communalInteractor = kosmos.communalInteractor private val settings = FakeSettings() private lateinit var testableLooper: TestableLooper private lateinit var fakeHandler: FakeHandler diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index 0a464e6047d3..b701d7f315bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.media.controls.models.player.MediaViewHolder @@ -171,6 +172,38 @@ class MediaViewControllerTest : SysuiTestCase() { } @Test + fun testObtainViewState_expandedMatchesParentHeight() { + mediaViewController.attach(player, MediaViewController.TYPE.PLAYER) + player.measureState = + TransitionViewState().apply { + this.height = 100 + this.measureHeight = 100 + } + mediaHostStateHolder.expandedMatchesParentHeight = true + mediaHostStateHolder.expansion = 1f + mediaHostStateHolder.measurementInput = + MeasurementInput( + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY), + ) + + // Assign the height of each expanded layout + MediaViewHolder.backgroundIds.forEach { id -> + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight = 100 + } + + mediaViewController.obtainViewState(mediaHostStateHolder) + + // Verify height of each expanded layout is updated to match constraint + MediaViewHolder.backgroundIds.forEach { id -> + assertTrue( + mediaViewController.expandedLayout.getConstraint(id).layout.mHeight == + ConstraintSet.MATCH_CONSTRAINT + ) + } + } + + @Test fun testSquishViewState_applySquishFraction_toTransitionViewState_alpha_forMediaPlayer() { whenever(mockViewState.copy()).thenReturn(mockCopiedState) whenever(mockCopiedState.widgetStates) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index 72847a6b6c45..c6cfabc61300 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -414,6 +414,18 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void onDeviceListUpdate_verifyDeviceListCallback() { + // This test relies on mMediaOutputController.start being called while the selected device + // list has exactly one item, and that item's id is: + // - Different from both ids in mMediaDevices. + // - Different from the id of the route published by the device under test (usually the + // built-in speakers). + // So mock the selected device to respect these two preconditions. + MediaDevice mockSelectedMediaDevice = Mockito.mock(MediaDevice.class); + when(mockSelectedMediaDevice.getId()).thenReturn(TEST_DEVICE_3_ID); + doReturn(List.of(mockSelectedMediaDevice)) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + mMediaOutputController.start(mCb); reset(mCb); @@ -434,6 +446,18 @@ public class MediaOutputControllerTest extends SysuiTestCase { @Test public void advanced_onDeviceListUpdateWithConnectedDeviceRemote_verifyItemSize() { + // This test relies on mMediaOutputController.start being called while the selected device + // list has exactly one item, and that item's id is: + // - Different from both ids in mMediaDevices. + // - Different from the id of the route published by the device under test (usually the + // built-in speakers). + // So mock the selected device to respect these two preconditions. + MediaDevice mockSelectedMediaDevice = Mockito.mock(MediaDevice.class); + when(mockSelectedMediaDevice.getId()).thenReturn(TEST_DEVICE_3_ID); + doReturn(List.of(mockSelectedMediaDevice)) + .when(mLocalMediaManager) + .getSelectedMediaDevice(); + when(mMediaDevice1.getFeatures()).thenReturn( ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK)); when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java index c8c134a9474a..563a3fe9fc7f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.annotation.Nullable; @@ -47,6 +48,7 @@ import android.view.Display; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import androidx.lifecycle.Lifecycle; import androidx.test.filters.SmallTest; @@ -63,6 +65,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.CommandQueue; @@ -111,7 +114,8 @@ public class QSImplTest extends SysuiTestCase { @Mock private FooterActionsViewBinder mFooterActionsViewBinder; @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; @Mock private FeatureFlagsClassic mFeatureFlags; - private View mQsView; + @Mock private SceneContainerFlags mSceneContainerFlags; + private ViewGroup mQsView; private final CommandQueue mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext)); @@ -121,6 +125,9 @@ public class QSImplTest extends SysuiTestCase { @Before public void setup() { + MockitoAnnotations.initMocks(this); + when(mSceneContainerFlags.isEnabled()).thenReturn(false); + mUnderTest = instantiate(); mUnderTest.onComponentCreated(mQsComponent, null); @@ -487,9 +494,24 @@ public class QSImplTest extends SysuiTestCase { verify(mQSAnimator).setOnKeyguard(true); } - private QSImpl instantiate() { - MockitoAnnotations.initMocks(this); + @Test + public void testSceneContainerFlagsEnabled_FooterActionsRemoved_controllerNotStarted() { + when(mSceneContainerFlags.isEnabled()).thenReturn(true); + clearInvocations( + mFooterActionsViewBinder, mFooterActionsViewModel, mFooterActionsViewModelFactory); + QSImpl other = instantiate(); + + other.onComponentCreated(mQsComponent, null); + assertThat((View) other.getView().findViewById(R.id.qs_footer_actions)).isNull(); + verifyZeroInteractions( + mFooterActionsViewModel, + mFooterActionsViewBinder, + mFooterActionsViewModelFactory + ); + } + + private QSImpl instantiate() { setupQsComponent(); setUpViews(); setUpInflater(); @@ -514,7 +536,8 @@ public class QSImplTest extends SysuiTestCase { mFooterActionsViewModelFactory, mFooterActionsViewBinder, mLargeScreenShadeInterpolator, - mFeatureFlags); + mFeatureFlags, + mSceneContainerFlags); } private void setUpOther() { @@ -533,14 +556,23 @@ public class QSImplTest extends SysuiTestCase { } private void setUpViews() { - mQsView = spy(new View(mContext)); + mQsView = spy(new FrameLayout(mContext)); when(mQsComponent.getRootView()).thenReturn(mQsView); - when(mQsView.findViewById(R.id.expanded_qs_scroll_view)) + + when(mQSPanelScrollView.findViewById(R.id.expanded_qs_scroll_view)) .thenReturn(mQSPanelScrollView); - when(mQsView.findViewById(R.id.header)).thenReturn(mHeader); - when(mQsView.findViewById(android.R.id.edit)).thenReturn(new View(mContext)); - when(mQsView.findViewById(R.id.qs_footer_actions)).thenAnswer( - invocation -> new FooterActionsViewBinder().create(mContext)); + mQsView.addView(mQSPanelScrollView); + + when(mHeader.findViewById(R.id.header)).thenReturn(mHeader); + mQsView.addView(mHeader); + + View customizer = new View(mContext); + customizer.setId(android.R.id.edit); + mQsView.addView(customizer); + + View footerActionsView = new FooterActionsViewBinder().create(mContext); + footerActionsView.setId(R.id.qs_footer_actions); + mQsView.addView(footerActionsView); } private void setUpInflater() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index b7a9ea751438..c1f5d85ca0f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -25,14 +25,16 @@ import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.compose.ComposeFacade import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -52,6 +54,8 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest class GlanceableHubContainerControllerTest : SysuiTestCase() { + private val kosmos = testKosmos() + @Mock private lateinit var communalViewModel: CommunalViewModel @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock private lateinit var shadeInteractor: ShadeInteractor @@ -71,9 +75,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) - val withDeps = CommunalInteractorFactory.create() - communalInteractor = withDeps.communalInteractor - communalRepository = withDeps.communalRepository + communalInteractor = kosmos.communalInteractor + communalRepository = kosmos.fakeCommunalRepository underTest = GlanceableHubContainerController( @@ -91,7 +94,8 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { .thenReturn(bouncerShowingFlow) whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow) - overrideResource(R.dimen.communal_grid_gutter_size, SWIPE_REGION_WIDTH) + overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH) + overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH) } @Test @@ -132,7 +136,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { initAndAttachContainerView() // Touch events are intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue() } @Test @@ -144,7 +148,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { // Initial touch down is intercepted, and so are touches outside of the region, until an up // event is received. - assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue() assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue() assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue() assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse() @@ -165,6 +169,17 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun onTouchEvent_topSwipeWhenHubOpen_returnsFalse() { + // Communal is open. + communalRepository.setDesiredScene(CommunalSceneKey.Communal) + + initAndAttachContainerView() + + // Touch event in the top swipe reqgion is not intercepted. + assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse() + } + + @Test fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() { // Communal is open. communalRepository.setDesiredScene(CommunalSceneKey.Communal) @@ -210,11 +225,29 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { companion object { private const val CONTAINER_WIDTH = 100 private const val CONTAINER_HEIGHT = 100 - private const val SWIPE_REGION_WIDTH = 20 - - private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) - private val DOWN_IN_SWIPE_REGION_EVENT = + private const val RIGHT_SWIPE_REGION_WIDTH = 20 + private const val TOP_SWIPE_REGION_WIDTH = 20 + + private val DOWN_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_DOWN, + CONTAINER_WIDTH.toFloat(), + CONTAINER_HEIGHT.toFloat(), + 0 + ) + private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0) + private val DOWN_IN_TOP_SWIPE_REGION_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_DOWN, + 0f, + TOP_SWIPE_REGION_WIDTH.toFloat(), + 0 + ) private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 11da2376328a..200e758245a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -57,7 +57,6 @@ import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.communal.domain.interactor.CommunalInteractor; -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlagsClassic; @@ -202,8 +201,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { new ConfigurationInteractor(configurationRepository), shadeRepository, () -> sceneInteractor); - CommunalInteractor communalInteractor = - CommunalInteractorFactory.create().getCommunalInteractor(); + CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); FakeKeyguardTransitionRepository keyguardTransitionRepository = new FakeKeyguardTransitionRepository(); @@ -303,7 +301,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mShadeWindowLogger, () -> mSelectedUserInteractor, mUserTracker, - mSceneContainerFlags) { + mSceneContainerFlags, + () -> communalInteractor) { @Override protected boolean isDebuggable() { return false; @@ -451,6 +450,24 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { } @Test + public void setCommunalShowing_userTimeout() { + setKeyguardShowing(); + clearInvocations(mWindowManager); + + mNotificationShadeWindowController.onCommunalShowingChanged(true); + verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); + assertThat(mLayoutParameters.getValue().userActivityTimeout) + .isEqualTo(CommunalInteractor.AWAKE_INTERVAL_MS); + clearInvocations(mWindowManager); + + // Bouncer showing over communal overrides communal value + mNotificationShadeWindowController.setBouncerShowing(true); + verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); + assertThat(mLayoutParameters.getValue().userActivityTimeout) + .isEqualTo(KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS); + } + + @Test public void setKeyguardShowing_notFocusable_byDefault() { mNotificationShadeWindowController.setKeyguardShowing(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index 8f46a37bf540..6fc88ce95325 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -43,7 +43,6 @@ import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepositor import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.communal.domain.interactor.CommunalInteractor; -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; @@ -233,8 +232,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { new ConfigurationInteractor(configurationRepository), mShadeRepository, () -> sceneInteractor); - CommunalInteractor communalInteractor = - CommunalInteractorFactory.create().getCommunalInteractor(); + CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); FakeKeyguardTransitionRepository keyguardTransitionRepository = new FakeKeyguardTransitionRepository(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index 05e866e85112..13934dac2401 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -27,7 +27,7 @@ import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepositor import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeCommandQueue @@ -144,7 +144,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { { fromLockscreenTransitionInteractor }, { fromPrimaryBouncerTransitionInteractor } ) - val communalInteractor = CommunalInteractorFactory.create().communalInteractor + val communalInteractor = kosmos.communalInteractor fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor( keyguardTransitionRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt index df547ae5883e..350ed2d9ff22 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt @@ -17,9 +17,11 @@ package com.android.systemui.statusbar.notification.collection.coordinator import android.os.UserHandle +import android.platform.test.annotations.EnableFlags import android.service.notification.StatusBarNotification import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.NotificationLockscreenUserManager @@ -33,6 +35,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnBefo import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -55,28 +58,31 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { val statusBarStateController: StatusBarStateController = mock() val keyguardStateController: KeyguardStateController = mock() val mSelectedUserInteractor: SelectedUserInteractor = mock() + val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController = + mock() val coordinator: SensitiveContentCoordinator = - DaggerTestSensitiveContentCoordinatorComponent - .factory() - .create( - dynamicPrivacyController, - lockscreenUserManager, - keyguardUpdateMonitor, - statusBarStateController, - keyguardStateController, - mSelectedUserInteractor) - .coordinator + DaggerTestSensitiveContentCoordinatorComponent.factory() + .create( + dynamicPrivacyController, + lockscreenUserManager, + keyguardUpdateMonitor, + statusBarStateController, + keyguardStateController, + mSelectedUserInteractor, + sensitiveNotificationProtectionController + ) + .coordinator @Test fun onDynamicPrivacyChanged_invokeInvalidationListener() { coordinator.attach(pipeline) - val invalidator = withArgCaptor<Invalidator> { - verify(pipeline).addPreRenderInvalidator(capture()) - } - val dynamicPrivacyListener = withArgCaptor<DynamicPrivacyController.Listener> { - verify(dynamicPrivacyController).addListener(capture()) - } + val invalidator = + withArgCaptor<Invalidator> { verify(pipeline).addPreRenderInvalidator(capture()) } + val dynamicPrivacyListener = + withArgCaptor<DynamicPrivacyController.Listener> { + verify(dynamicPrivacyController).addListener(capture()) + } val invalidationListener = mock<Pluggable.PluggableListener<Invalidator>>() invalidator.setInvalidationListener(invalidationListener) @@ -89,9 +95,10 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) @@ -105,11 +112,59 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) @@ -123,11 +178,59 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) @@ -141,17 +244,87 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, false) + } + + @Test fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) val entry = fakeNotification(1, false) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -159,17 +332,92 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, false) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test fun onBeforeRenderList_deviceLocked_notifNeedsRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -179,15 +427,37 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + val entry = fakeNotification(1, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(false, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) val entry = fakeNotification(1, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -195,19 +465,96 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + val entry = fakeNotification(1, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) + val entry = fakeNotification(2, true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_sensitiveActive() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) + val entry = fakeNotification(2, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + + onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) + + verify(entry.representativeEntry!!).setSensitive(true, true) + } + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + @Suppress("ktlint:standard:max-line-length") + fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_shouldProtectNotification() { + coordinator.attach(pipeline) + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } + + whenever(lockscreenUserManager.currentUserId).thenReturn(1) + whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) + whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false) + whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) + whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true) val entry = fakeNotification(2, true) + whenever( + sensitiveNotificationProtectionController.shouldProtectNotification( + entry.getRepresentativeEntry() + ) + ) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -217,9 +564,10 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { @Test fun onBeforeRenderList_deviceDynamicallyUnlocked_deviceBiometricBypassingLockScreen() { coordinator.attach(pipeline) - val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> { - verify(pipeline).addOnBeforeRenderListListener(capture()) - } + val onBeforeRenderListListener = + withArgCaptor<OnBeforeRenderListListener> { + verify(pipeline).addOnBeforeRenderListListener(capture()) + } whenever(lockscreenUserManager.currentUserId).thenReturn(1) whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true) @@ -227,9 +575,11 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true) whenever(statusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD) whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(any())) - .thenReturn(true) - + .thenReturn(true) val entry = fakeNotification(2, true) + whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true) + whenever(sensitiveNotificationProtectionController.shouldProtectNotification(any())) + .thenReturn(true) onBeforeRenderListListener.onBeforeRenderList(listOf(entry)) @@ -237,15 +587,11 @@ class SensitiveContentCoordinatorTest : SysuiTestCase() { } private fun fakeNotification(notifUserId: Int, needsRedaction: Boolean): ListEntry { - val mockUserHandle = mock<UserHandle>().apply { - whenever(identifier).thenReturn(notifUserId) - } - val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply { - whenever(user).thenReturn(mockUserHandle) - } - val mockEntry = mock<NotificationEntry>().apply { - whenever(sbn).thenReturn(mockSbn) - } + val mockUserHandle = + mock<UserHandle>().apply { whenever(identifier).thenReturn(notifUserId) } + val mockSbn: StatusBarNotification = + mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) } + val mockEntry = mock<NotificationEntry>().apply { whenever(sbn).thenReturn(mockSbn) } whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction) whenever(mockEntry.rowExists()).thenReturn(true) return object : ListEntry("key", 0) { @@ -268,6 +614,8 @@ interface TestSensitiveContentCoordinatorComponent { @BindsInstance statusBarStateController: StatusBarStateController, @BindsInstance keyguardStateController: KeyguardStateController, @BindsInstance selectedUserInteractor: SelectedUserInteractor, + @BindsInstance + sensitiveNotificationProtectionController: SensitiveNotificationProtectionController, ): TestSensitiveContentCoordinatorComponent } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 89f826b2049d..1ab4c32c7d08 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.stack; +import static com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING; import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -32,6 +33,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static kotlinx.coroutines.flow.FlowKt.emptyFlow; @@ -39,6 +41,7 @@ import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDis import android.metrics.LogMaker; import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.View; @@ -101,6 +104,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; +import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.tuner.TunerService; import com.android.systemui.util.settings.SecureSettings; @@ -172,10 +176,16 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private ActivityStarter mActivityStarter; @Mock private KeyguardTransitionRepository mKeyguardTransitionRepo; @Mock private NotificationListViewBinder mViewBinder; + @Mock + private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController; + + @Captor + private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor; @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor; + private final ActiveNotificationListRepository mActiveNotificationsRepository = new ActiveNotificationListRepository(); @@ -386,6 +396,23 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + public void testOnUserChange_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test public void testOnUserChange_verifySensitiveProfile() { when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); initController(/* viewIsAttached= */ true); @@ -403,6 +430,80 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifyNotSensitive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifySensitiveProfile_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnUserChange_verifySensitiveActive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + initController(/* viewIsAttached= */ true); + + ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor + .forClass(UserChangedListener.class); + + verify(mNotificationLockscreenUserManager) + .addUserChangedListener(userChangedCaptor.capture()); + reset(mNotificationStackScrollLayout); + + UserChangedListener changedListener = userChangedCaptor.getValue(); + changedListener.onUserChanged(0); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + public void testOnStatePostChange_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test public void testOnStatePostChange_verifyIfProfileIsPublic() { when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); @@ -418,6 +519,194 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { } @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyNotSensitive_screenshareNotificationHidingEnabled() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyIfProfileIsPublic_screenshareNotificationHidingEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_verifyIfSensitiveActive_screenshareNotificationHidingEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + public void testOnStatePostChange_goingFullShade_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, false); + } + + @Test + public void testOnStatePostChange_goingFullShade_verifyIfProfileIsPublic() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifyNotSensitive_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifyProfileIsPublic_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(true, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnStatePostChange_goingFullShade_verifySensitiveActive_screenshareHideEnabled( + ) { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSysuiStatusBarStateController).addCallback( + mStateListenerArgumentCaptor.capture(), anyInt()); + + StatusBarStateController.StateListener stateListener = + mStateListenerArgumentCaptor.getValue(); + + stateListener.onStatePostChange(); + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyNotSensitive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()) + .thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, false); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyIfProfileIsPublic() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void testOnProjectionStateChanged_verifyIfSensitiveActive() { + when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false); + when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true); + + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController) + .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture()); + + mSensitiveStateListenerArgumentCaptor.getValue().run(); + + verify(mNotificationStackScrollLayout).updateSensitiveness(false, true); + } + + @Test public void testOnMenuShownLogging() { ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS); when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker( @@ -666,6 +955,20 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean()); } + @Test + @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void sensitiveNotificationProtectionControllerListenerNotRegistered() { + initController(/* viewIsAttached= */ true); + verifyZeroInteractions(mSensitiveNotificationProtectionController); + } + + @Test + @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING) + public void sensitiveNotificationProtectionControllerListenerRegistered() { + initController(/* viewIsAttached= */ true); + verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any()); + } + private LogMaker logMatcher(int category, int type) { return argThat(new LogMatcher(category, type)); } @@ -744,7 +1047,8 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSecureSettings, mock(NotificationDismissibilityProvider.class), mActivityStarter, - new ResourcesSplitShadeStateController()); + new ResourcesSplitShadeStateController(), + mSensitiveNotificationProtectionController); } static class LogMatcher implements ArgumentMatcher<LogMaker> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index a824bc4f803f..06298b78ae57 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -25,6 +25,9 @@ import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -33,20 +36,19 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.shade.mockLargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -68,6 +70,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { val keyguardInteractor = kosmos.keyguardInteractor val keyguardRootViewModel = kosmos.keyguardRootViewModel val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + val communalInteractor = kosmos.communalInteractor val shadeRepository = kosmos.shadeRepository val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper @@ -214,6 +217,61 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun glanceableHubAlpha() = + testScope.runTest { + val alpha by collectLastValue(underTest.glanceableHubAlpha) + + // Start on lockscreen + showLockscreen() + assertThat(alpha).isEqualTo(1f) + + // Start transitioning to glanceable hub + val progress = 0.6f + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = 0f, + ) + ) + runCurrent() + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = progress, + ) + ) + runCurrent() + // Expected alpha is inverse of progress as notifications are fading away + assertThat(alpha).isEqualTo(1 - progress) + + // Finish transition to glanceable hub + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = 1f, + ) + ) + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + assertThat(alpha).isEqualTo(0f) + + // While state is GLANCEABLE_HUB, verify alpha is restored to full if glanceable hub is + // not fully visible. + shadeRepository.setLockscreenShadeExpansion(0.1f) + assertThat(alpha).isEqualTo(1f) + } + + @Test fun validateMarginTop() = testScope.runTest { overrideResource(R.bool.config_use_large_screen_shade_header, false) @@ -302,6 +360,43 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun isOnGlanceableHubWithoutShade() = + testScope.runTest { + val isOnGlanceableHubWithoutShade by + collectLastValue(underTest.isOnGlanceableHubWithoutShade) + + // Start on lockscreen + showLockscreen() + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + // Move to glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + assertThat(isOnGlanceableHubWithoutShade).isTrue() + + // While state is GLANCEABLE_HUB, validate variations of both shade and qs expansion + shadeRepository.setLockscreenShadeExpansion(0.1f) + shadeRepository.setQsExpansion(0f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0.1f) + shadeRepository.setQsExpansion(0.1f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0f) + shadeRepository.setQsExpansion(0.1f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0f) + shadeRepository.setQsExpansion(0f) + assertThat(isOnGlanceableHubWithoutShade).isTrue() + } + + @Test fun boundsOnLockscreenNotInSplitShade() = testScope.runTest { val bounds by collectLastValue(underTest.bounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 4827c92ce452..b3a47d77a307 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -48,7 +48,6 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; -import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.MathUtils; @@ -66,6 +65,7 @@ import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionState; @@ -134,7 +134,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private AlarmManager mAlarmManager; @Mock private DozeParameters mDozeParameters; @Mock private LightBarController mLightBarController; - @Mock private DelayedWakeLock.Builder mDelayedWakeLockBuilder; + @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory; @Mock private DelayedWakeLock mWakeLock; @Mock private KeyguardStateController mKeyguardStateController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @@ -145,6 +145,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private AlternateBouncerToGoneTransitionViewModel mAlternateBouncerToGoneTransitionViewModel; @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + @Mock private KeyguardInteractor mKeyguardInteractor; private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository(); @Mock private CoroutineDispatcher mMainDispatcher; @Mock private TypedArray mMockTypedArray; @@ -260,11 +261,7 @@ public class ScrimControllerTest extends SysuiTestCase { }).when(mLightBarController).setScrimState( any(ScrimState.class), anyFloat(), any(GradientColors.class)); - when(mDelayedWakeLockBuilder.setHandler(any(Handler.class))) - .thenReturn(mDelayedWakeLockBuilder); - when(mDelayedWakeLockBuilder.setTag(any(String.class))) - .thenReturn(mDelayedWakeLockBuilder); - when(mDelayedWakeLockBuilder.build()).thenReturn(mWakeLock); + when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock); when(mDockManager.isDocked()).thenReturn(false); when(mKeyguardTransitionInteractor.transition(any(), any())) @@ -279,7 +276,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDozeParameters, mAlarmManager, mKeyguardStateController, - mDelayedWakeLockBuilder, + mDelayedWakeLockFactory, new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor, mDockManager, @@ -292,6 +289,7 @@ public class ScrimControllerTest extends SysuiTestCase { mPrimaryBouncerToGoneTransitionViewModel, mAlternateBouncerToGoneTransitionViewModel, mKeyguardTransitionInteractor, + mKeyguardInteractor, mWallpaperRepository, mMainDispatcher, mLinearLargeScreenShadeInterpolator); @@ -987,7 +985,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDozeParameters, mAlarmManager, mKeyguardStateController, - mDelayedWakeLockBuilder, + mDelayedWakeLockFactory, new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor, mDockManager, @@ -1000,6 +998,7 @@ public class ScrimControllerTest extends SysuiTestCase { mPrimaryBouncerToGoneTransitionViewModel, mAlternateBouncerToGoneTransitionViewModel, mKeyguardTransitionInteractor, + mKeyguardInteractor, mWallpaperRepository, mMainDispatcher, mLinearLargeScreenShadeInterpolator); 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 1f8cc54211e6..6785de930849 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 @@ -168,6 +168,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC assertThat(conn.carrierName.value) .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}")) assertThat(conn.hasPrioritizedNetworkCapabilities.value).isEqualTo(model.slice) + assertThat(conn.isNonTerrestrial.value).isEqualTo(model.ntn) // TODO(b/261029387): check these once we start handling them assertThat(conn.isEmergencyOnly.value).isFalse() @@ -194,6 +195,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC val roaming: Boolean, val name: String, val slice: Boolean, + val ntn: Boolean, ) { override fun toString(): String { return "INPUT(level=$level, " + @@ -205,7 +207,8 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC "carrierNetworkChange=$carrierNetworkChange, " + "roaming=$roaming, " + "name=$name," + - "slice=$slice)" + "slice=$slice" + + "ntn=$ntn)" } // Convenience for iterating test data and creating new cases @@ -220,6 +223,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC roaming: Boolean? = null, name: String? = null, slice: Boolean? = null, + ntn: Boolean? = null, ): TestCase = TestCase( level = level ?: this.level, @@ -232,6 +236,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC roaming = roaming ?: this.roaming, name = name ?: this.name, slice = slice ?: this.slice, + ntn = ntn ?: this.ntn, ) } @@ -262,6 +267,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC private val roaming = listOf(false, true) private val names = listOf("name 1", "name 2") private val slice = listOf(false, true) + private val ntn = listOf(false, true) @Parameters(name = "{0}") @JvmStatic fun data() = testData() @@ -300,6 +306,7 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC roaming.first(), names.first(), slice.first(), + ntn.first(), ) val tail = @@ -312,7 +319,8 @@ internal class DemoMobileConnectionParameterizedTest(private val testCase: TestC carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) }, roaming.map { baseCase.modifiedBy(roaming = it) }, names.map { baseCase.modifiedBy(name = it) }, - slice.map { baseCase.modifiedBy(slice = it) } + slice.map { baseCase.modifiedBy(slice = it) }, + ntn.map { baseCase.modifiedBy(ntn = it) } ) .flatten() 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 03814bdfa430..b958f35c0e53 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 @@ -579,6 +579,7 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() { assertThat(conn.carrierName.value) .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}")) assertThat(conn.hasPrioritizedNetworkCapabilities.value).isEqualTo(model.slice) + assertThat(conn.isNonTerrestrial.value).isEqualTo(model.ntn) // TODO(b/261029387) check these once we start handling them assertThat(conn.isEmergencyOnly.value).isFalse() 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 9d6f3156f83e..98556514f8ec 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 @@ -21,6 +21,7 @@ import android.content.Context import android.content.Intent import android.net.ConnectivityManager import android.net.ConnectivityManager.NetworkCallback +import android.platform.test.annotations.EnableFlags import android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WLAN import android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WWAN import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL @@ -963,6 +964,31 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + fun isNonTerrestrial_updatesFromServiceState() = + testScope.runTest { + val latest by collectLastValue(underTest.isNonTerrestrial) + + // Lambda makes it a little clearer what we are testing IMO + val serviceStateCreator = { ntn: Boolean -> + mock<ServiceState>().also { + whenever(it.isUsingNonTerrestrialNetwork).thenReturn(ntn) + } + } + + // Starts out false + assertThat(latest).isFalse() + + getTelephonyCallbackForType<ServiceStateListener>() + .onServiceStateChanged(serviceStateCreator(true)) + assertThat(latest).isTrue() + + getTelephonyCallbackForType<ServiceStateListener>() + .onServiceStateChanged(serviceStateCreator(false)) + assertThat(latest).isFalse() + } + + @Test fun numberOfLevels_usesCarrierConfig() = testScope.runTest { var latest: Int? = null diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 02e6fd5a9d6e..0e0d4897d667 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -35,6 +35,7 @@ import android.telephony.satellite.SatelliteModemStateCallback import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.log.core.FakeLogBuffer import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState @@ -87,6 +88,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { Optional.empty(), dispatcher, testScope.backgroundScope, + FakeLogBuffer.Factory.create(), systemClock, ) @@ -100,6 +102,45 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { } @Test + fun satelliteManagerThrows_checkSupportDoesNotCrash() = + testScope.runTest { + whenever(satelliteManager.requestIsSatelliteSupported(any(), any())) + .thenThrow(IllegalStateException()) + + systemClock.setUptimeMillis(Process.getStartUptimeMillis() + MIN_UPTIME) + + underTest = + DeviceBasedSatelliteRepositoryImpl( + Optional.of(satelliteManager), + dispatcher, + testScope.backgroundScope, + FakeLogBuffer.Factory.create(), + systemClock, + ) + + runCurrent() + + // Creating the repo does not crash, and we consider the feature not to be supported + assertThat(underTest.satelliteSupport.value).isEqualTo(SatelliteSupport.NotSupported) + } + + @Test + fun satelliteManagerThrows_doesNotCrash() = + testScope.runTest { + setupDefaultRepo() + + whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any())) + .thenThrow(SatelliteException(13)) + + val conn by collectLastValue(underTest.connectionState) + val strength by collectLastValue(underTest.signalStrength) + + // Flows have not emitted, we haven't crashed + assertThat(conn).isNull() + assertThat(strength).isNull() + } + + @Test fun connectionState_mapsFromSatelliteModemState() = testScope.runTest { setupDefaultRepo() @@ -380,6 +421,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { if (satMan != null) Optional.of(satMan) else Optional.empty(), dispatcher, testScope.backgroundScope, + FakeLogBuffer.Factory.create(), systemClock, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt new file mode 100644 index 000000000000..21c038ad476d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue +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.satellite.data.prod.FakeDeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.mockito.MockitoAnnotations + +@SmallTest +class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { + private lateinit var underTest: DeviceBasedSatelliteViewModel + private lateinit var interactor: DeviceBasedSatelliteInteractor + + private val repo = FakeDeviceBasedSatelliteRepository() + private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) + + private val testScope = TestScope() + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + interactor = + DeviceBasedSatelliteInteractor( + repo, + mobileIconsInteractor, + testScope.backgroundScope, + ) + + underTest = + DeviceBasedSatelliteViewModel( + interactor, + testScope.backgroundScope, + ) + } + + @Test + fun icon_nullWhenShouldNotShow_satelliteNotAllowed() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is not allowed + repo.isSatelliteAllowedForCurrentLocation.value = false + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + + // THEN icon is null because we should not be showing it + assertThat(latest).isNull() + } + + @Test + fun icon_nullWhenShouldNotShow_notAllOos() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are not OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = true + + // THEN icon is null because we have service + assertThat(latest).isNull() + } + + @Test + fun icon_satelliteIsOff() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + + // THEN icon is null because we have service + assertThat(latest).isInstanceOf(Icon::class.java) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt new file mode 100644 index 000000000000..ca9df57e8798 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarIconViewTest.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.shared.ui.view + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.StatusBarIconView +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Being a simple subclass of [ModernStatusBarView], use the same basic test cases to verify the + * root behavior, and add testing for the new [SingleBindableStatusBarIconView.withDefaultBinding] + * method. + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class SingleBindableStatusBarIconViewTest : SysuiTestCase() { + private lateinit var binding: SingleBindableStatusBarIconViewBinding + + // Visibility is outsourced to view-models. This simulates it + private var isVisible = true + private var visibilityFn: () -> Boolean = { isVisible } + + @Test + fun initView_hasCorrectSlot() { + val view = createAndInitView() + + assertThat(view.slot).isEqualTo(SLOT_NAME) + } + + @Test + fun getVisibleState_icon_returnsIcon() { + val view = createAndInitView() + + view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_ICON) + } + + @Test + fun getVisibleState_dot_returnsDot() { + val view = createAndInitView() + + view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_DOT) + } + + @Test + fun getVisibleState_hidden_returnsHidden() { + val view = createAndInitView() + + view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(StatusBarIconView.STATE_HIDDEN) + } + + @Test + fun onDarkChanged_bindingReceivesIconAndDecorTint() { + val view = createAndInitView() + + view.onDarkChangedWithContrast(arrayListOf(), 0x12345678, 0x12344321) + + assertThat(binding.iconTint).isEqualTo(0x12345678) + assertThat(binding.decorTint).isEqualTo(0x12345678) + } + + @Test + fun setStaticDrawableColor_bindingReceivesIconTint() { + val view = createAndInitView() + + view.setStaticDrawableColor(0x12345678, 0x12344321) + + assertThat(binding.iconTint).isEqualTo(0x12345678) + } + + @Test + fun setDecorColor_bindingReceivesDecorColor() { + val view = createAndInitView() + + view.setDecorColor(0x23456789) + + assertThat(binding.decorTint).isEqualTo(0x23456789) + } + + @Test + fun isIconVisible_usesBinding_true() { + val view = createAndInitView() + + isVisible = true + + assertThat(view.isIconVisible).isEqualTo(true) + } + + @Test + fun isIconVisible_usesBinding_false() { + val view = createAndInitView() + + isVisible = false + + assertThat(view.isIconVisible).isEqualTo(false) + } + + @Test + fun getDrawingRect_takesTranslationIntoAccount() { + val view = createAndInitView() + + view.translationX = 50f + view.translationY = 60f + + val drawingRect = Rect() + view.getDrawingRect(drawingRect) + + assertThat(drawingRect.left).isEqualTo(view.left + 50) + assertThat(drawingRect.right).isEqualTo(view.right + 50) + assertThat(drawingRect.top).isEqualTo(view.top + 60) + assertThat(drawingRect.bottom).isEqualTo(view.bottom + 60) + } + + private fun createAndInitView(): SingleBindableStatusBarIconView { + val view = SingleBindableStatusBarIconView.createView(context) + binding = SingleBindableStatusBarIconView.withDefaultBinding(view, visibilityFn) {} + view.initView(SLOT_NAME) { binding } + return view + } + + companion object { + private const val SLOT_NAME = "test_slot" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt new file mode 100644 index 000000000000..cd5d5ed0d08e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt @@ -0,0 +1,231 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import android.app.Notification +import android.media.projection.MediaProjectionInfo +import android.media.projection.MediaProjectionManager +import android.os.Handler +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +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.Captor +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.reset +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { + @Mock private lateinit var handler: Handler + + @Mock private lateinit var mediaProjectionManager: MediaProjectionManager + + @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo + + @Mock private lateinit var listener1: Runnable + @Mock private lateinit var listener2: Runnable + @Mock private lateinit var listener3: Runnable + + @Captor + private lateinit var mediaProjectionCallbackCaptor: + ArgumentCaptor<MediaProjectionManager.Callback> + + private lateinit var controller: SensitiveNotificationProtectionControllerImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) + + // Obtain useful MediaProjectionCallback + verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any()) + } + + @Test + fun init_flagEnabled_registerMediaProjectionManagerCallback() { + assertNotNull(mediaProjectionCallbackCaptor.value) + } + + @Test + fun init_flagDisabled_noRegisterMediaProjectionManagerCallback() { + mSetFlagsRule.disableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) + reset(mediaProjectionManager) + + controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler) + + verifyZeroInteractions(mediaProjectionManager) + } + + @Test + fun registerSensitiveStateListener_singleListener() { + controller.registerSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + } + + @Test + fun registerSensitiveStateListener_multipleListeners() { + controller.registerSensitiveStateListener(listener1) + controller.registerSensitiveStateListener(listener2) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + verify(listener2, times(2)).run() + } + + @Test + fun registerSensitiveStateListener_afterProjectionActive() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + controller.registerSensitiveStateListener(listener1) + verifyZeroInteractions(listener1) + + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1).run() + } + + @Test + fun unregisterSensitiveStateListener_singleListener() { + controller.registerSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + + controller.unregisterSensitiveStateListener(listener1) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verifyNoMoreInteractions(listener1) + } + + @Test + fun unregisterSensitiveStateListener_multipleListeners() { + controller.registerSensitiveStateListener(listener1) + controller.registerSensitiveStateListener(listener2) + controller.registerSensitiveStateListener(listener3) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verify(listener1, times(2)).run() + verify(listener2, times(2)).run() + verify(listener3, times(2)).run() + + controller.unregisterSensitiveStateListener(listener1) + controller.unregisterSensitiveStateListener(listener2) + + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + verifyNoMoreInteractions(listener1) + verifyNoMoreInteractions(listener2) + verify(listener3, times(4)).run() + } + + @Test + fun isSensitiveStateActive_projectionInactive_false() { + assertFalse(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionActive_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionInactiveAfterActive_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test + fun isSensitiveStateActive_projectionActiveAfterInactive_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo) + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + fun shouldProtectNotification_projectionInactive_false() { + val notificationEntry = mock(NotificationEntry::class.java) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_fgsNotification_false() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = mock(NotificationEntry::class.java) + val sbn = mock(StatusBarNotification::class.java) + val notification = mock(Notification::class.java) + `when`(notificationEntry.sbn).thenReturn(sbn) + `when`(sbn.notification).thenReturn(notification) + `when`(notification.isFgsOrUij).thenReturn(true) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + fun shouldProtectNotification_projectionActive_notFgsNotification_true() { + mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo) + + val notificationEntry = mock(NotificationEntry::class.java) + val sbn = mock(StatusBarNotification::class.java) + val notification = mock(Notification::class.java) + `when`(notificationEntry.sbn).thenReturn(sbn) + `when`(sbn.notification).thenReturn(notification) + `when`(notification.isFgsOrUij).thenReturn(false) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 8c823b2376c3..d839da1d6e1b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -32,6 +32,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assume.assumeNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -63,6 +64,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.keyguard.TestScopeProvider; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; @@ -72,6 +74,7 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.VolumeDialogController.State; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -79,6 +82,7 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.FakeSystemClock; import dagger.Lazy; @@ -97,6 +101,8 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.function.Predicate; +import kotlinx.coroutines.Dispatchers; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -138,7 +144,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { DevicePostureController mPostureController; @Mock private Lazy<SecureSettings> mLazySecureSettings; - private final CsdWarningDialog.Factory mCsdWarningDialogFactory = new CsdWarningDialog.Factory() { @Override @@ -146,6 +151,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { return mCsdWarningDialog; } }; + @Mock + private VibratorHelper mVibratorHelper; private int mLongestHideShowAnimationDuration = 250; private FakeSettings mSecureSettings; @@ -180,6 +187,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { when(mLazySecureSettings.get()).thenReturn(mSecureSettings); + when(mVibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(new int[]{0}); + mDialog = new VolumeDialogImpl( getContext(), mVolumeDialogController, @@ -195,7 +204,11 @@ public class VolumeDialogImplTest extends SysuiTestCase { mPostureController, mTestableLooper.getLooper(), mDumpManager, - mLazySecureSettings); + mLazySecureSettings, + mVibratorHelper, + Dispatchers.getUnconfined(), + TestScopeProvider.getTestScope(), + new FakeSystemClock()); mDialog.init(0, null); State state = createShellState(); mDialog.onStateChangedH(state); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 589f7c23ea13..93d8dcc8f092 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -100,7 +100,6 @@ import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository; import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor; import com.android.systemui.communal.domain.interactor.CommunalInteractor; -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; @@ -443,8 +442,7 @@ public class BubblesTest extends SysuiTestCase { () -> keyguardInteractor, () -> mFromLockscreenTransitionInteractor, () -> mFromPrimaryBouncerTransitionInteractor); - CommunalInteractor communalInteractor = - CommunalInteractorFactory.create().getCommunalInteractor(); + CommunalInteractor communalInteractor = mKosmos.getCommunalInteractor(); mFromLockscreenTransitionInteractor = new FromLockscreenTransitionInteractor( keyguardTransitionRepository, @@ -536,7 +534,8 @@ public class BubblesTest extends SysuiTestCase { mShadeWindowLogger, () -> mSelectedUserInteractor, mUserTracker, - mSceneContainerFlags + mSceneContainerFlags, + mKosmos::getCommunalInteractor ); mNotificationShadeWindowController.fetchWindowRootView(); mNotificationShadeWindowController.attach(); diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java index b820ca612bfc..1afe56f27be7 100644 --- a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java +++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.os.Looper; import android.os.SystemClock; import android.util.AndroidRuntimeException; +import android.util.Singleton; import android.view.Choreographer; import com.android.internal.util.Preconditions; @@ -67,7 +68,12 @@ import java.util.function.Consumer; public final class AnimatorTestRule implements TestRule { private final Object mLock = new Object(); - private final TestHandler mTestHandler = new TestHandler(); + private final Singleton<TestHandler> mTestHandler = new Singleton<>() { + @Override + protected TestHandler create() { + return new TestHandler(); + } + }; private final long mStartTime; private long mTotalTimeDelta = 0; @@ -95,16 +101,17 @@ public final class AnimatorTestRule implements TestRule { return new Statement() { @Override public void evaluate() throws Throwable { - AnimationHandler objAtStart = AnimationHandler.setTestHandler(mTestHandler); + final TestHandler testHandler = mTestHandler.get(); + AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler); try { base.evaluate(); } finally { AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart); - if (mTestHandler != objAtEnd) { + if (testHandler != objAtEnd) { // pass or fail, inner logic not restoring the handler needs to be reported. // noinspection ThrowFromFinallyBlock throw new IllegalStateException("Test handler was altered: expected=" - + mTestHandler + " actual=" + objAtEnd); + + testHandler + " actual=" + objAtEnd); } } } @@ -125,8 +132,9 @@ public final class AnimatorTestRule implements TestRule { public void initNewAnimators() { requireLooper("AnimationTestRule#initNewAnimators()"); long currentTime = getCurrentTime(); - List<AnimationFrameCallback> newCallbacks = new ArrayList<>(mTestHandler.mNewCallbacks); - mTestHandler.mNewCallbacks.clear(); + final TestHandler testHandler = mTestHandler.get(); + List<AnimationFrameCallback> newCallbacks = new ArrayList<>(testHandler.mNewCallbacks); + testHandler.mNewCallbacks.clear(); for (AnimationFrameCallback newCallback : newCallbacks) { newCallback.doAnimationFrame(currentTime); } @@ -158,9 +166,10 @@ public final class AnimatorTestRule implements TestRule { public void advanceTimeBy(long timeDelta, @Nullable Consumer<Long> preFrameAction) { Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative"); requireLooper("AnimationTestRule#advanceTimeBy(long)"); + final TestHandler testHandler = mTestHandler.get(); if (timeDelta == 0) { // If time is not being advanced, all animators will get a tick; don't double tick these - mTestHandler.mNewCallbacks.clear(); + testHandler.mNewCallbacks.clear(); } else { // before advancing time, start new animators with the current time initNewAnimators(); @@ -172,10 +181,10 @@ public final class AnimatorTestRule implements TestRule { if (preFrameAction != null) { preFrameAction.accept(timeDelta); // After letting other code run, clear any new callbacks to avoid double-ticking them - mTestHandler.mNewCallbacks.clear(); + testHandler.mNewCallbacks.clear(); } // produce a frame - mTestHandler.doFrame(); + testHandler.doFrame(); } /** diff --git a/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt new file mode 100644 index 000000000000..e5121d5de6dd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.keyguardManager by Kosmos.Fixture { mock<KeyguardManager>() } diff --git a/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt new file mode 100644 index 000000000000..4e2683bd7a2f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os + +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.concurrency.mockExecutorHandler + +val Kosmos.fakeExecutorHandler by Kosmos.Fixture { mockExecutorHandler(fakeExecutor) } diff --git a/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt new file mode 100644 index 000000000000..fb51f0fec997 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.dream + +import android.service.dreams.IDreamManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.dreamManager by Kosmos.Fixture { mock<IDreamManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt new file mode 100644 index 000000000000..d9ea5e92710c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.internal.widget + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.lockPatternUtils by Kosmos.Fixture { mock<LockPatternUtils>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt new file mode 100644 index 000000000000..7185b7cd0ac6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +val Kosmos.activityIntentHelper by Kosmos.Fixture { ActivityIntentHelper(applicationContext) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt new file mode 100644 index 000000000000..128f58bf9751 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt new file mode 100644 index 000000000000..b7d6f3a5f91f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.assist + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.assistManager by Kosmos.Fixture { mock<AssistManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt deleted file mode 100644 index 1ba863052e2d..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.communal.domain.interactor - -import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository -import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository -import com.android.systemui.communal.data.repository.FakeCommunalRepository -import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository -import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository -import com.android.systemui.communal.widgets.CommunalAppWidgetHost -import com.android.systemui.communal.widgets.EditWidgetsActivityStarter -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory -import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository -import com.android.systemui.util.mockito.mock -import kotlinx.coroutines.test.TestScope - -// TODO(b/319335645): get rid of me and use kosmos. -object CommunalInteractorFactory { - - @JvmOverloads - @JvmStatic - fun create( - testScope: TestScope = TestScope(), - communalRepository: FakeCommunalRepository = - FakeCommunalRepository(testScope.backgroundScope), - keyguardRepository: FakeKeyguardRepository = FakeKeyguardRepository(), - widgetRepository: FakeCommunalWidgetRepository = - FakeCommunalWidgetRepository(testScope.backgroundScope), - mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(), - smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(), - tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), - communalPrefsRepository: FakeCommunalPrefsRepository = FakeCommunalPrefsRepository(), - appWidgetHost: CommunalAppWidgetHost = mock(), - editWidgetsActivityStarter: EditWidgetsActivityStarter = mock(), - ): WithDependencies { - val keyguardInteractor = - KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor - val communalTutorialInteractor = - CommunalTutorialInteractor( - testScope.backgroundScope, - tutorialRepository, - keyguardInteractor, - communalRepository, - ) - - return WithDependencies( - testScope, - communalRepository, - widgetRepository, - communalPrefsRepository, - mediaRepository, - smartspaceRepository, - tutorialRepository, - keyguardRepository, - keyguardInteractor, - communalTutorialInteractor, - appWidgetHost, - editWidgetsActivityStarter, - CommunalInteractor( - testScope.backgroundScope, - communalRepository, - widgetRepository, - communalPrefsRepository, - mediaRepository, - smartspaceRepository, - keyguardInteractor, - appWidgetHost, - editWidgetsActivityStarter, - ), - ) - } - - data class WithDependencies( - val testScope: TestScope, - val communalRepository: FakeCommunalRepository, - val widgetRepository: FakeCommunalWidgetRepository, - val communalPrefsRepository: FakeCommunalPrefsRepository, - val mediaRepository: FakeCommunalMediaRepository, - val smartspaceRepository: FakeSmartspaceRepository, - val tutorialRepository: FakeCommunalTutorialRepository, - val keyguardRepository: FakeKeyguardRepository, - val keyguardInteractor: KeyguardInteractor, - val tutorialInteractor: CommunalTutorialInteractor, - val appWidgetHost: CommunalAppWidgetHost, - val editWidgetsActivityStarter: EditWidgetsActivityStarter, - val communalInteractor: CommunalInteractor, - ) -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index 65579a6d9ddf..1abf71fde14c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -20,11 +20,13 @@ import com.android.systemui.communal.data.repository.communalMediaRepository import com.android.systemui.communal.data.repository.communalPrefsRepository import com.android.systemui.communal.data.repository.communalRepository import com.android.systemui.communal.data.repository.communalWidgetRepository +import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.smartspace.data.repository.smartspaceRepository +import com.android.systemui.user.data.repository.userRepository import com.android.systemui.util.mockito.mock val Kosmos.communalInteractor by Fixture { @@ -35,8 +37,11 @@ val Kosmos.communalInteractor by Fixture { mediaRepository = communalMediaRepository, communalPrefsRepository = communalPrefsRepository, smartspaceRepository = smartspaceRepository, + userRepository = userRepository, appWidgetHost = mock(), keyguardInteractor = keyguardInteractor, - editWidgetsActivityStarter = mock(), + editWidgetsActivityStarter = editWidgetsActivityStarter, ) } + +val Kosmos.editWidgetsActivityStarter by Fixture<EditWidgetsActivityStarter> { mock() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt index baba88db8e55..adaea7cbf64d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt @@ -29,5 +29,6 @@ val Kosmos.communalTutorialInteractor by communalTutorialRepository = communalTutorialRepository, keyguardInteractor = keyguardInteractor, communalRepository = communalRepository, + communalInteractor = communalInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt new file mode 100644 index 000000000000..68e14573a9b2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.data.repository + +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeStickyKeysRepository : StickyKeysRepository { + override val settingEnabled: Flow<Boolean> = MutableStateFlow(true) + private val _stickyKeys: MutableStateFlow<LinkedHashMap<ModifierKey, Locked>> = + MutableStateFlow(LinkedHashMap()) + + override val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> = _stickyKeys + + fun setStickyKeys(keys: LinkedHashMap<ModifierKey, Locked>) { + _stickyKeys.value = keys + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt new file mode 100644 index 000000000000..46f7355dfe75 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.keyboardRepository by Kosmos.Fixture { FakeKeyboardRepository() }
\ No newline at end of file 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 59f56dd18f0e..5766f7a9028c 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 @@ -130,6 +130,8 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { private val _isEncryptedOrLockdown = MutableStateFlow(true) override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown + override val topClippingBounds = MutableStateFlow<Int?>(null) + override fun setQuickSettingsVisible(isVisible: Boolean) { _isQuickSettingsVisible.value = isVisible } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt deleted file mode 100644 index 92193fd6bc06..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.domain.interactor - -import com.android.systemui.communal.data.repository.communalMediaRepository -import com.android.systemui.communal.data.repository.communalPrefsRepository -import com.android.systemui.communal.data.repository.communalRepository -import com.android.systemui.communal.data.repository.communalWidgetRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.widgets.CommunalAppWidgetHost -import com.android.systemui.communal.widgets.EditWidgetsActivityStarter -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.smartspace.data.repository.smartspaceRepository -import org.mockito.Mockito.mock - -val Kosmos.communalInteractor by - Kosmos.Fixture { - CommunalInteractor( - applicationScope = applicationCoroutineScope, - communalRepository = communalRepository, - widgetRepository = communalWidgetRepository, - mediaRepository = communalMediaRepository, - communalPrefsRepository = communalPrefsRepository, - smartspaceRepository = smartspaceRepository, - keyguardInteractor = keyguardInteractor, - appWidgetHost = mock(CommunalAppWidgetHost::class.java), - editWidgetsActivityStarter = editWidgetsActivityStarter, - ) - } - -val Kosmos.editWidgetsActivityStarter by - Kosmos.Fixture<EditWidgetsActivityStarter> { mock(EditWidgetsActivityStarter::class.java) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt index 294b5ba474c3..ec17c488e297 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.domain.interactor +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 5564767e0715..d376f126e2c9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor @@ -33,6 +34,7 @@ val Kosmos.keyguardRootViewModel by Fixture { deviceEntryInteractor = deviceEntryInteractor, dozeParameters = dozeParameters, keyguardInteractor = keyguardInteractor, + communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, notificationsKeyguardInteractor = notificationsKeyguardInteractor, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index cc0449d7e7bb..321f94468a2e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -26,6 +26,7 @@ import com.android.systemui.classifier.falsingCollector import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -72,6 +73,7 @@ class KosmosJavaAdapter( val powerInteractor by lazy { kosmos.powerInteractor } val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } + val communalInteractor by lazy { kosmos.communalInteractor } init { kosmos.applicationContext = testCase.context diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt new file mode 100644 index 000000000000..7cc5d6b6243a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.shadeController by Kosmos.Fixture { mock<ShadeController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt new file mode 100644 index 000000000000..1ceab68604f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.shadeViewController by Kosmos.Fixture { mock<ShadeViewController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt new file mode 100644 index 000000000000..4dcd2208b152 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.shadeAnimationRepository by Kosmos.Fixture { ShadeAnimationRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt new file mode 100644 index 000000000000..57b272f10c67 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.data.repository.shadeAnimationRepository + +var Kosmos.shadeAnimationInteractor: ShadeAnimationInteractor by + Kosmos.Fixture { ShadeAnimationInteractorEmptyImpl(shadeAnimationRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt new file mode 100644 index 000000000000..a75d2bc4aaf6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.notifications.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.settings.data.repository.secureSettingsRepository + +val Kosmos.notificationSettingsRepository by + Kosmos.Fixture { + NotificationSettingsRepository( + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, + secureSettingsRepository = secureSettingsRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt new file mode 100644 index 000000000000..17b4603e17b9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.notifications.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository + +val Kosmos.notificationSettingsInteractor by + Kosmos.Fixture { NotificationSettingsInteractor(notificationSettingsRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt new file mode 100644 index 000000000000..552b09e933de --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.settings.data.repository + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.secureSettingsRepository: SecureSettingsRepository by + Kosmos.Fixture { fakeSecureSettingsRepository } +val Kosmos.fakeSecureSettingsRepository by Kosmos.Fixture { FakeSecureSettingsRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt new file mode 100644 index 000000000000..7b912aef3011 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationClickNotifier by Kosmos.Fixture { mock<NotificationClickNotifier>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt new file mode 100644 index 000000000000..8d30049bfb09 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationPresenter by Kosmos.Fixture { mock<NotificationPresenter>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt new file mode 100644 index 000000000000..554bdbe0c382 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationRemoteInputManager by + Kosmos.Fixture { mock<NotificationRemoteInputManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt new file mode 100644 index 000000000000..e8ca3b8234e8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationShadeWindowController by + Kosmos.Fixture { mock<NotificationShadeWindowController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt new file mode 100644 index 000000000000..c337ac201b3d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.statusBarNotificationActivityStarter + +var Kosmos.notificationActivityStarter: NotificationActivityStarter by + Kosmos.Fixture { statusBarNotificationActivityStarter } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt new file mode 100644 index 000000000000..c3db34bdddb7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationLaunchAnimatorControllerProvider by + Kosmos.Fixture { mock<NotificationLaunchAnimatorControllerProvider>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt new file mode 100644 index 000000000000..1f45fbbcf927 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection + +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos + +var Kosmos.notifLiveDataStore: NotifLiveDataStore by + Kosmos.Fixture { NotifLiveDataStoreImpl(fakeExecutor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt new file mode 100644 index 000000000000..358d2519556b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.visualStabilityCoordinator by Kosmos.Fixture { mock<VisualStabilityCoordinator>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt new file mode 100644 index 000000000000..a5c956155351 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.provider + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.launchFullScreenIntentProvider by Kosmos.Fixture { LaunchFullScreenIntentProvider() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt new file mode 100644 index 000000000000..edce5d58b351 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.render + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.notifLiveDataStore +import com.android.systemui.statusbar.notification.collection.notifPipeline +import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor + +var Kosmos.notificationVisibilityProvider: NotificationVisibilityProvider by + Kosmos.Fixture { + NotificationVisibilityProviderImpl( + activeNotificationsInteractor, + notifLiveDataStore, + notifPipeline, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt new file mode 100644 index 000000000000..1e3897ba46c6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.statusbar.notification.collection.coordinator.visualStabilityCoordinator +import com.android.systemui.statusbar.notification.collection.inflation.OnUserInteractionCallbackImpl +import com.android.systemui.statusbar.notification.collection.notifCollection +import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider +import com.android.systemui.statusbar.policy.headsUpManager + +var Kosmos.onUserInteractionCallback: OnUserInteractionCallback by + Kosmos.Fixture { + OnUserInteractionCallbackImpl( + notificationVisibilityProvider, + notifCollection, + headsUpManager, + statusBarStateController, + visualStabilityCoordinator, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 5ef9a8e8edd8..db4050905200 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel import com.android.systemui.kosmos.Kosmos @@ -33,7 +36,10 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, shadeInteractor = shadeInteractor, + communalInteractor = communalInteractor, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt new file mode 100644 index 000000000000..6ddc9df930f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.app.keyguardManager +import android.content.applicationContext +import android.os.fakeExecutorHandler +import android.service.dream.dreamManager +import com.android.internal.logging.metricsLogger +import com.android.internal.widget.lockPatternUtils +import com.android.systemui.activityIntentHelper +import com.android.systemui.animation.activityLaunchAnimator +import com.android.systemui.assist.assistManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.activityStarter +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.settings.userTracker +import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor +import com.android.systemui.shade.shadeController +import com.android.systemui.shade.shadeViewController +import com.android.systemui.statusbar.notification.collection.provider.launchFullScreenIntentProvider +import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider +import com.android.systemui.statusbar.notification.notificationLaunchAnimatorControllerProvider +import com.android.systemui.statusbar.notification.row.onUserInteractionCallback +import com.android.systemui.statusbar.notificationClickNotifier +import com.android.systemui.statusbar.notificationLockscreenUserManager +import com.android.systemui.statusbar.notificationPresenter +import com.android.systemui.statusbar.notificationRemoteInputManager +import com.android.systemui.statusbar.notificationShadeWindowController +import com.android.systemui.statusbar.policy.headsUpManager +import com.android.systemui.statusbar.policy.keyguardStateController +import com.android.systemui.wmshell.bubblesManager +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@OptIn(ExperimentalCoroutinesApi::class) +val Kosmos.statusBarNotificationActivityStarter by + Kosmos.Fixture { + StatusBarNotificationActivityStarter( + applicationContext, + applicationContext.displayId, + fakeExecutorHandler, + fakeExecutor, + notificationVisibilityProvider, + headsUpManager, + activityStarter, + notificationClickNotifier, + statusBarKeyguardViewManager, + keyguardManager, + dreamManager, + Optional.of(bubblesManager), + { assistManager }, + notificationRemoteInputManager, + notificationLockscreenUserManager, + shadeController, + keyguardStateController, + lockPatternUtils, + statusBarRemoteInputCallback, + activityIntentHelper, + metricsLogger, + statusBarNotificationActivityStarterLogger, + onUserInteractionCallback, + notificationPresenter, + shadeViewController, + notificationShadeWindowController, + activityLaunchAnimator, + shadeAnimationInteractor, + notificationLaunchAnimatorControllerProvider, + launchFullScreenIntentProvider, + powerInteractor, + userTracker, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt new file mode 100644 index 000000000000..31cfc979a11a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.statusBarNotificationActivityStarterLogger by + Kosmos.Fixture { StatusBarNotificationActivityStarterLogger(logcatLogBuffer()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt new file mode 100644 index 000000000000..475d7fa6ef4b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.statusBarRemoteInputCallback by Kosmos.Fixture { mock<StatusBarRemoteInputCallback>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index a9c8ec7dcb7d..32d572ef9dee 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -35,6 +35,7 @@ class FakeMobileConnectionRepository( override val isRoaming = MutableStateFlow(false) override val operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null) override val isInService = MutableStateFlow(false) + override val isNonTerrestrial = MutableStateFlow(false) override val isGsm = MutableStateFlow(false) override val cdmaLevel = MutableStateFlow(0) override val primaryLevel = MutableStateFlow(0) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt new file mode 100644 index 000000000000..0e909c498a2b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.keyguardStateController by Kosmos.Fixture { mock<KeyguardStateController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt index e208add32efa..5476d5509c0c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/ui/SystemBarUtilsStateKosmos.kt @@ -17,7 +17,15 @@ package com.android.systemui.statusbar.ui import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.policy.configurationController -val Kosmos.systemBarUtilsState by - Kosmos.Fixture { SystemBarUtilsState(configurationController, systemBarUtilsProxy) } +val Kosmos.systemBarUtilsState by Fixture { + SystemBarUtilsState( + testDispatcher, + testDispatcher, + configurationController, + systemBarUtilsProxy, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt new file mode 100644 index 000000000000..1d05d62a02e4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.wmshell + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.bubblesManager by Kosmos.Fixture { mock<BubblesManager>() } diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 3d8d7b738233..d656892062d1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4413,25 +4413,50 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Override public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) { mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY); + final ComponentName componentName = info.getComponentName(); // Warning is not required if the service is already enabled. synchronized (mLock) { final AccessibilityUserState userState = getCurrentUserStateLocked(); - if (userState.getEnabledServicesLocked().contains(info.getComponentName())) { + if (userState.getEnabledServicesLocked().contains(componentName)) { return false; } } // Warning is not required if the service is already assigned to a shortcut. for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) { if (getAccessibilityShortcutTargets(shortcutType).contains( - info.getComponentName().flattenToString())) { + componentName.flattenToString())) { return false; } } + // Warning is not required if the service is preinstalled and in the + // trustedAccessibilityServices allowlist. + if (android.view.accessibility.Flags.skipAccessibilityWarningDialogForTrustedServices() + && isAccessibilityServicePreinstalledAndTrusted(info)) { + return false; + } + // Warning is required by default. return true; } + private boolean isAccessibilityServicePreinstalledAndTrusted(AccessibilityServiceInfo info) { + final ComponentName componentName = info.getComponentName(); + final boolean isPreinstalled = + info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp(); + if (isPreinstalled) { + final String[] trustedAccessibilityServices = + mContext.getResources().getStringArray( + R.array.config_trustedAccessibilityServices); + if (Arrays.stream(trustedAccessibilityServices) + .map(ComponentName::unflattenFromString) + .anyMatch(componentName::equals)) { + return true; + } + } + return false; + } + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; diff --git a/services/autofill/bugfixes.aconfig b/services/autofill/bugfixes.aconfig index b5130a1c4cfc..ced10fbeff0c 100644 --- a/services/autofill/bugfixes.aconfig +++ b/services/autofill/bugfixes.aconfig @@ -34,3 +34,10 @@ flag { description: "Mitigation for autofill providers miscalculating view visibility" bug: "291795358" } + +flag { + name: "remote_fill_service_use_weak_reference" + namespace: "autofill" + description: "Use weak reference to address binder leak problem" + bug: "307972253" +} diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java index 6d0915bf7d92..5c93991bef8c 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java @@ -17,6 +17,7 @@ package com.android.server.autofill; import static android.service.autofill.FillRequest.INVALID_REQUEST_ID; +import static android.service.autofill.Flags.remoteFillServiceUseWeakReference; import static com.android.server.autofill.Helper.sVerbose; @@ -44,6 +45,7 @@ import com.android.internal.infra.AbstractRemoteService; import com.android.internal.infra.ServiceConnector; import com.android.internal.os.IResultReceiver; +import java.lang.ref.WeakReference; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -65,6 +67,8 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { private final Object mLock = new Object(); private CompletableFuture<FillResponse> mPendingFillRequest; private int mPendingFillRequestId = INVALID_REQUEST_ID; + private AtomicReference<IFillCallback> mFillCallback; + private AtomicReference<ISaveCallback> mSaveCallback; private final ComponentName mComponentName; private final boolean mIsCredentialAutofillService; @@ -150,6 +154,89 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { } } + static class IFillCallbackDelegate extends IFillCallback.Stub { + private WeakReference<IFillCallback> mCallbackWeakRef; + + IFillCallbackDelegate(IFillCallback callback) { + mCallbackWeakRef = new WeakReference(callback); + } + + @Override + public void onCancellable(ICancellationSignal cancellation) throws RemoteException { + IFillCallback callback = mCallbackWeakRef.get(); + if (callback != null) { + callback.onCancellable(cancellation); + } + } + + @Override + public void onSuccess(FillResponse response) throws RemoteException { + IFillCallback callback = mCallbackWeakRef.get(); + if (callback != null) { + callback.onSuccess(response); + } + } + + @Override + public void onFailure(int requestId, CharSequence message) throws RemoteException { + IFillCallback callback = mCallbackWeakRef.get(); + if (callback != null) { + callback.onFailure(requestId, message); + } + } + } + + static class ISaveCallbackDelegate extends ISaveCallback.Stub { + + private WeakReference<ISaveCallback> mCallbackWeakRef; + + ISaveCallbackDelegate(ISaveCallback callback) { + mCallbackWeakRef = new WeakReference(callback); + } + + @Override + public void onSuccess(IntentSender intentSender) throws RemoteException { + ISaveCallback callback = mCallbackWeakRef.get(); + if (callback != null) { + callback.onSuccess(intentSender); + } + } + + @Override + public void onFailure(CharSequence message) throws RemoteException { + ISaveCallback callback = mCallbackWeakRef.get(); + if (callback != null) { + callback.onFailure(message); + } + } + } + + /** + * Wraps an {@link IFillCallback} object using weak reference. + * + * This prevents lingering allocation issue by breaking the chain of strong references from + * Binder to {@link callback}. Since {@link callback} is not held by Binder anymore, it needs + * to be held by {@link mFillCallback} so it's not deallocated prematurely. + */ + private IFillCallback maybeWrapWithWeakReference(IFillCallback callback) { + if (remoteFillServiceUseWeakReference()) { + mFillCallback = new AtomicReference<>(callback); + return new IFillCallbackDelegate(callback); + } + return callback; + } + + /** + * Wraps an {@link ISaveCallback} object using weak reference. + */ + private ISaveCallback maybeWrapWithWeakReference(ISaveCallback callback) { + if (remoteFillServiceUseWeakReference()) { + mSaveCallback = new AtomicReference<>(callback); + return new ISaveCallbackDelegate(callback); + } + return callback; + } + public void onFillCredentialRequest(@NonNull FillRequest request, IAutoFillManagerClient autofillCallback) { if (sVerbose) { @@ -164,29 +251,30 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { } CompletableFuture<FillResponse> fillRequest = new CompletableFuture<>(); - remoteService.onFillCredentialRequest(request, new IFillCallback.Stub() { - @Override - public void onCancellable(ICancellationSignal cancellation) { - CompletableFuture<FillResponse> future = futureRef.get(); - if (future != null && future.isCancelled()) { - dispatchCancellationSignal(cancellation); - } else { - cancellationSink.set(cancellation); - } - } - - @Override - public void onSuccess(FillResponse response) { - fillRequest.complete(response); - } - - @Override - public void onFailure(int requestId, CharSequence message) { - String errorMessage = message == null ? "" : String.valueOf(message); - fillRequest.completeExceptionally( - new RuntimeException(errorMessage)); - } - }, autofillCallback); + remoteService.onFillCredentialRequest( + request, maybeWrapWithWeakReference(new IFillCallback.Stub() { + @Override + public void onCancellable(ICancellationSignal cancellation) { + CompletableFuture<FillResponse> future = futureRef.get(); + if (future != null && future.isCancelled()) { + dispatchCancellationSignal(cancellation); + } else { + cancellationSink.set(cancellation); + } + } + + @Override + public void onSuccess(FillResponse response) { + fillRequest.complete(response); + } + + @Override + public void onFailure(int requestId, CharSequence message) { + String errorMessage = message == null ? "" : String.valueOf(message); + fillRequest.completeExceptionally( + new RuntimeException(errorMessage)); + } + }), autofillCallback); return fillRequest; }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS); futureRef.set(connectThenFillRequest); @@ -235,29 +323,30 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { } CompletableFuture<FillResponse> fillRequest = new CompletableFuture<>(); - remoteService.onFillRequest(request, new IFillCallback.Stub() { - @Override - public void onCancellable(ICancellationSignal cancellation) { - CompletableFuture<FillResponse> future = futureRef.get(); - if (future != null && future.isCancelled()) { - dispatchCancellationSignal(cancellation); - } else { - cancellationSink.set(cancellation); - } - } - - @Override - public void onSuccess(FillResponse response) { - fillRequest.complete(response); - } - - @Override - public void onFailure(int requestId, CharSequence message) { - String errorMessage = message == null ? "" : String.valueOf(message); - fillRequest.completeExceptionally( - new RuntimeException(errorMessage)); - } - }); + remoteService.onFillRequest( + request, maybeWrapWithWeakReference(new IFillCallback.Stub() { + @Override + public void onCancellable(ICancellationSignal cancellation) { + CompletableFuture<FillResponse> future = futureRef.get(); + if (future != null && future.isCancelled()) { + dispatchCancellationSignal(cancellation); + } else { + cancellationSink.set(cancellation); + } + } + + @Override + public void onSuccess(FillResponse response) { + fillRequest.complete(response); + } + + @Override + public void onFailure(int requestId, CharSequence message) { + String errorMessage = message == null ? "" : String.valueOf(message); + fillRequest.completeExceptionally( + new RuntimeException(errorMessage)); + } + })); return fillRequest; }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS); futureRef.set(connectThenFillRequest); @@ -294,7 +383,7 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { if (sVerbose) Slog.v(TAG, "calling onSaveRequest()"); CompletableFuture<IntentSender> save = new CompletableFuture<>(); - service.onSaveRequest(request, new ISaveCallback.Stub() { + service.onSaveRequest(request, maybeWrapWithWeakReference(new ISaveCallback.Stub() { @Override public void onSuccess(IntentSender intentSender) { save.complete(intentSender); @@ -304,7 +393,7 @@ final class RemoteFillService extends ServiceConnector.Impl<IAutoFillService> { public void onFailure(CharSequence message) { save.completeExceptionally(new RuntimeException(String.valueOf(message))); } - }); + })); return save; }).orTimeout(TIMEOUT_REMOTE_REQUEST_MILLIS, TimeUnit.MILLISECONDS) .whenComplete((res, err) -> Handler.getMain().post(() -> { diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 42ab05fdd231..4d42f154a392 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -58,11 +58,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.autofill.AutofillManager; import android.widget.ImageView; import android.widget.RemoteViews; +import android.widget.ScrollView; import android.widget.TextView; import com.android.internal.R; @@ -370,9 +372,23 @@ final class SaveUi { params.windowAnimations = R.style.AutofillSaveAnimation; params.setTrustedOverlay(); + ScrollView scrollView = view.findViewById(R.id.autofill_sheet_scroll_view); + + View divider = view.findViewById(R.id.autofill_sheet_divider); + + ViewTreeObserver observer = scrollView.getViewTreeObserver(); + observer.addOnGlobalLayoutListener(() -> adjustDividerVisibility(scrollView, divider)); + + scrollView.getViewTreeObserver() + .addOnScrollChangedListener(() -> adjustDividerVisibility(scrollView, divider)); show(); } + private void adjustDividerVisibility(ScrollView scrollView, View divider) { + boolean canScrollDown = scrollView.canScrollVertically(1); // 1 to check scrolling down + divider.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE); + } + private boolean applyCustomDescription(@NonNull Context context, @NonNull View saveUiView, @NonNull ValueFinder valueFinder, @NonNull SaveInfo info) { final CustomDescription customDescription = info.getCustomDescription(); diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig index 549fa36597b7..4022e3378954 100644 --- a/services/backup/flags.aconfig +++ b/services/backup/flags.aconfig @@ -10,6 +10,15 @@ flag { } flag { + name: "enable_metrics_system_backup_agents" + namespace: "backup" + description: "Enable SystemBackupAgent to collect B&R agent metrics by passing an instance of " + "the logger to each BackupHelper." + bug: "296844513" + is_fixed_read_only: true +} + +flag { name: "enable_max_size_writes_to_pipes" namespace: "onboarding" description: "Enables the write buffer to pipes to be of maximum size." diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 50e18628852d..84e1d9062fd5 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -247,6 +247,11 @@ public class CompanionDeviceManagerService extends SystemService { final Context context = getContext(); mPersistentStore = new PersistentDataStore(); + mAssociationRequestsProcessor = new AssociationRequestsProcessor( + /* cdmService */ this, mAssociationStore); + mBackupRestoreProcessor = new BackupRestoreProcessor( + /* cdmService */ this, mAssociationStore, mPersistentStore, + mSystemDataTransferRequestStore, mAssociationRequestsProcessor); loadAssociationsFromDisk(); mAssociationStore.registerListener(mAssociationStoreChangeListener); @@ -254,17 +259,12 @@ public class CompanionDeviceManagerService extends SystemService { mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(mUserManager, mAssociationStore, mDevicePresenceCallback); - mAssociationRequestsProcessor = new AssociationRequestsProcessor( - /* cdmService */this, mAssociationStore); mCompanionAppController = new CompanionApplicationController( context, mAssociationStore, mDevicePresenceMonitor); mTransportManager = new CompanionTransportManager(context, mAssociationStore); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); - mBackupRestoreProcessor = new BackupRestoreProcessor( - /* cdmService */ this, mAssociationStore, mPersistentStore, - mSystemDataTransferRequestStore, mAssociationRequestsProcessor); // TODO(b/279663946): move context sync to a dedicated system service mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager); diff --git a/services/core/Android.bp b/services/core/Android.bp index fdcd27da5bdc..3164e083af0f 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -211,6 +211,7 @@ java_library_static { "com_android_wm_shell_flags_lib", "com.android.server.utils_aconfig-java", "service-jobscheduler-deviceidle.flags-aconfig-java", + "backup_flags_lib", "policy_flags_lib", ], javac_shard_size: 50, diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java index 3483c1a1404a..a493d7a57500 100644 --- a/services/core/java/com/android/server/SystemConfig.java +++ b/services/core/java/com/android/server/SystemConfig.java @@ -95,6 +95,7 @@ public class SystemConfig { private static final int ALLOW_OVERRIDE_APP_RESTRICTIONS = 0x100; private static final int ALLOW_IMPLICIT_BROADCASTS = 0x200; private static final int ALLOW_VENDOR_APEX = 0x400; + private static final int ALLOW_SIGNATURE_PERMISSIONS = 0x800; private static final int ALLOW_ALL = ~0; // property for runtime configuration differentiation @@ -597,7 +598,7 @@ public class SystemConfig { // Vendors are only allowed to customize these int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PRIVAPP_PERMISSIONS - | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX; + | ALLOW_SIGNATURE_PERMISSIONS | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX; if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.O_MR1) { // For backward compatibility vendorPermissionFlag |= (ALLOW_PERMISSIONS | ALLOW_APP_CONFIGS); @@ -649,9 +650,9 @@ public class SystemConfig { // TODO(b/157203468): ALLOW_HIDDENAPI_WHITELISTING must be removed because we prohibited // the use of hidden APIs from the product partition. int productPermissionFlag = ALLOW_FEATURES | ALLOW_LIBS | ALLOW_PERMISSIONS - | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_HIDDENAPI_WHITELISTING - | ALLOW_ASSOCIATIONS | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS - | ALLOW_VENDOR_APEX; + | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_SIGNATURE_PERMISSIONS + | ALLOW_HIDDENAPI_WHITELISTING | ALLOW_ASSOCIATIONS + | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS | ALLOW_VENDOR_APEX; if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.R) { // TODO(b/157393157): This must check product interface enforcement instead of // DEVICE_INITIAL_SDK_INT for the devices without product interface enforcement. @@ -772,6 +773,8 @@ public class SystemConfig { final boolean allowAppConfigs = (permissionFlag & ALLOW_APP_CONFIGS) != 0; final boolean allowPrivappPermissions = (permissionFlag & ALLOW_PRIVAPP_PERMISSIONS) != 0; + final boolean allowSignaturePermissions = (permissionFlag & ALLOW_SIGNATURE_PERMISSIONS) + != 0; final boolean allowOemPermissions = (permissionFlag & ALLOW_OEM_PERMISSIONS) != 0; final boolean allowApiWhitelisting = (permissionFlag & ALLOW_HIDDENAPI_WHITELISTING) != 0; @@ -1246,6 +1249,38 @@ public class SystemConfig { XmlUtils.skipCurrentTag(parser); } } break; + case "signature-permissions": { + if (allowSignaturePermissions) { + // signature permissions from system, apex, vendor, product and + // system_ext partitions are stored separately. This is to + // prevent xml files in the vendor partition from granting + // permissions to signature apps in the system partition and vice versa. + boolean vendor = permFile.toPath().startsWith( + Environment.getVendorDirectory().toPath() + "/") + || permFile.toPath().startsWith( + Environment.getOdmDirectory().toPath() + "/"); + boolean product = permFile.toPath().startsWith( + Environment.getProductDirectory().toPath() + "/"); + boolean systemExt = permFile.toPath().startsWith( + Environment.getSystemExtDirectory().toPath() + "/"); + if (vendor) { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getVendorSignatureAppAllowlist()); + } else if (product) { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getProductSignatureAppAllowlist()); + } else if (systemExt) { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getSystemExtSignatureAppAllowlist()); + } else { + readSignatureAppPermissions(parser, + mPermissionAllowlist.getSignatureAppAllowlist()); + } + } else { + logNotAllowedInPartition(name, permFile, parser); + XmlUtils.skipCurrentTag(parser); + } + } break; case "oem-permissions": { if (allowOemPermissions) { readOemPermissions(parser); @@ -1655,6 +1690,12 @@ public class SystemConfig { readPermissionAllowlist(parser, allowlist, "privapp-permissions"); } + private void readSignatureAppPermissions(@NonNull XmlPullParser parser, + @NonNull ArrayMap<String, ArrayMap<String, Boolean>> allowlist) + throws IOException, XmlPullParserException { + readPermissionAllowlist(parser, allowlist, "signature-permissions"); + } + private void readInstallInUserType(XmlPullParser parser, Map<String, Set<String>> doInstallMap, Map<String, Set<String>> nonInstallMap) diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index c9bd0b4c4648..86894fd9b405 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4830,11 +4830,7 @@ public class ActivityManagerService extends IActivityManager.Stub if (!mConstants.mEnableWaitForFinishAttachApplication) { finishAttachApplicationInner(startSeq, callingUid, pid); } - - // Temporarily disable sending BOOT_COMPLETED to see if this was impacting perf tests - if (false) { - maybeSendBootCompletedLocked(app); - } + maybeSendBootCompletedLocked(app); } catch (Exception e) { // We need kill the process group here. (b/148588589) Slog.wtf(TAG, "Exception thrown during bind of " + app, e); diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index c96c2ff4f2eb..3487ae3c6e6c 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -151,6 +151,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Properties; +import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -653,7 +654,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub try { future.get(); return; - } catch (ExecutionException e) { + } catch (ExecutionException | CancellationException e) { return; } catch (InterruptedException e) { // Keep looping diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 2cac7a020005..db0f03f9edc6 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -1244,9 +1244,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } private void deliveryTimeout(@NonNull BroadcastProcessQueue queue) { + final int cookie = traceBegin("deliveryTimeout"); synchronized (mService) { deliveryTimeoutLocked(queue); } + traceEnd(cookie); } private void deliveryTimeoutLocked(@NonNull BroadcastProcessQueue queue) { diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 4cbee2b89bb2..9f7c07e0af35 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -18,9 +18,6 @@ package com.android.server.audio; import static android.Manifest.permission.MODIFY_AUDIO_SETTINGS_PRIVILEGED; import static android.app.BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT; -import static android.media.audio.Flags.autoPublicVolumeApiHardening; -import static android.media.audio.Flags.automaticBtDeviceType; -import static android.media.audio.Flags.focusFreezeTestApi; import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET; import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER; import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP; @@ -33,6 +30,9 @@ import static android.media.AudioManager.RINGER_MODE_NORMAL; import static android.media.AudioManager.RINGER_MODE_SILENT; import static android.media.AudioManager.RINGER_MODE_VIBRATE; import static android.media.AudioManager.STREAM_SYSTEM; +import static android.media.audio.Flags.autoPublicVolumeApiHardening; +import static android.media.audio.Flags.automaticBtDeviceType; +import static android.media.audio.Flags.focusFreezeTestApi; import static android.media.audiopolicy.Flags.enableFadeManagerConfiguration; import static android.os.Process.FIRST_APPLICATION_UID; import static android.os.Process.INVALID_UID; @@ -116,7 +116,6 @@ import android.media.AudioProfile; import android.media.AudioRecordingConfiguration; import android.media.AudioRoutesInfo; import android.media.AudioSystem; -import android.media.AudioTrack; import android.media.BluetoothProfileConnectionInfo; import android.media.FadeManagerConfiguration; import android.media.IAudioDeviceVolumeDispatcher; @@ -144,7 +143,7 @@ import android.media.IStrategyNonDefaultDevicesDispatcher; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.IStreamAliasingDispatcher; import android.media.IVolumeController; -import android.media.LoudnessCodecConfigurator; +import android.media.LoudnessCodecController; import android.media.LoudnessCodecInfo; import android.media.MediaCodec; import android.media.MediaMetrics; @@ -10737,34 +10736,35 @@ public class AudioService extends IAudioService.Stub mLoudnessCodecHelper.unregisterLoudnessCodecUpdatesDispatcher(dispatcher); } - /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */ + /** @see LoudnessCodecController#create(int) */ @Override - public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) { - mLoudnessCodecHelper.startLoudnessCodecUpdates(piid, codecInfoList); + public void startLoudnessCodecUpdates(int sessionId) { + mLoudnessCodecHelper.startLoudnessCodecUpdates(sessionId); } - /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */ + /** @see LoudnessCodecController#release() */ @Override - public void stopLoudnessCodecUpdates(int piid) { - mLoudnessCodecHelper.stopLoudnessCodecUpdates(piid); + public void stopLoudnessCodecUpdates(int sessionId) { + mLoudnessCodecHelper.stopLoudnessCodecUpdates(sessionId); } - /** @see LoudnessCodecConfigurator#addMediaCodec(MediaCodec) */ + /** @see LoudnessCodecController#addMediaCodec(MediaCodec) */ @Override - public void addLoudnessCodecInfo(int piid, int mediaCodecHash, LoudnessCodecInfo codecInfo) { - mLoudnessCodecHelper.addLoudnessCodecInfo(piid, mediaCodecHash, codecInfo); + public void addLoudnessCodecInfo(int sessionId, int mediaCodecHash, + LoudnessCodecInfo codecInfo) { + mLoudnessCodecHelper.addLoudnessCodecInfo(sessionId, mediaCodecHash, codecInfo); } - /** @see LoudnessCodecConfigurator#removeMediaCodec(MediaCodec) */ + /** @see LoudnessCodecController#removeMediaCodec(MediaCodec) */ @Override - public void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) { - mLoudnessCodecHelper.removeLoudnessCodecInfo(piid, codecInfo); + public void removeLoudnessCodecInfo(int sessionId, LoudnessCodecInfo codecInfo) { + mLoudnessCodecHelper.removeLoudnessCodecInfo(sessionId, codecInfo); } - /** @see LoudnessCodecConfigurator#getLoudnessCodecParams(AudioTrack, MediaCodec) */ + /** @see LoudnessCodecController#getLoudnessCodecParams(MediaCodec) */ @Override - public PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) { - return mLoudnessCodecHelper.getLoudnessParams(piid, codecInfo); + public PersistableBundle getLoudnessParams(LoudnessCodecInfo codecInfo) { + return mLoudnessCodecHelper.getLoudnessParams(codecInfo); } //========================================================================================== diff --git a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java index 9b0afc4282a2..01f770b1e89f 100644 --- a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java +++ b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java @@ -30,6 +30,8 @@ import static android.media.audio.Flags.automaticBtDeviceType; import android.annotation.IntDef; import android.annotation.NonNull; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager.AudioDeviceCategory; import android.media.AudioPlaybackConfiguration; @@ -44,7 +46,6 @@ import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.SystemProperties; import android.util.Log; -import android.util.SparseArray; import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; @@ -59,7 +60,9 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -75,9 +78,9 @@ public class LoudnessCodecHelper { /** * Property containing a string to set for a custom built in speaker SPL range as defined by * CTA2075. The options that can be set are: - * - "small": for max SPL with test signal < 75 dB, - * - "medium": for max SPL with test signal between 70 and 90 dB, - * - "large": for max SPL with test signal > 85 dB. + * - "small": for max SPL with test signal < 75 dB, + * - "medium": for max SPL with test signal between 70 and 90 dB, + * - "large": for max SPL with test signal > 85 dB. */ private static final String SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE = "audio.loudness.builtin-speaker-spl-range-size"; @@ -99,11 +102,13 @@ public class LoudnessCodecHelper { SPL_RANGE_LARGE }) @Retention(RetentionPolicy.SOURCE) - public @interface DeviceSplRange {} + public @interface DeviceSplRange { + } private static final class LoudnessRemoteCallbackList extends RemoteCallbackList<ILoudnessCodecUpdatesDispatcher> { private final LoudnessCodecHelper mLoudnessCodecHelper; + LoudnessRemoteCallbackList(LoudnessCodecHelper loudnessCodecHelper) { mLoudnessCodecHelper = loudnessCodecHelper; } @@ -133,9 +138,15 @@ public class LoudnessCodecHelper { private final Object mLock = new Object(); - /** Contains for each started piid the set corresponding to unique registered audio codecs. */ + /** Contains for each started track id the known started piids. */ + @GuardedBy("mLock") + private final HashMap<LoudnessTrackId, Set<Integer>> mStartedConfigPiids = + new HashMap<>(); + + /** Contains for each LoudnessTrackId a set of started coudec infos. */ @GuardedBy("mLock") - private final SparseArray<Set<LoudnessCodecInfo>> mStartedPiids = new SparseArray<>(); + private final HashMap<LoudnessTrackId, Set<LoudnessCodecInfo>> mStartedConfigInfo = + new HashMap<>(); /** Contains the current device id assignment for each piid. */ @GuardedBy("mLock") @@ -169,10 +180,12 @@ public class LoudnessCodecHelper { mMetadataType = metadataType; return this; } + Builder setIsDownmixing(boolean isDownmixing) { mIsDownmixing = isDownmixing; return this; } + Builder setDeviceSplRange(@DeviceSplRange int deviceSplRange) { mDeviceSplRange = deviceSplRange; return this; @@ -185,8 +198,8 @@ public class LoudnessCodecHelper { } private LoudnessCodecInputProperties(int metadataType, - boolean isDownmixing, - @DeviceSplRange int deviceSplRange) { + boolean isDownmixing, + @DeviceSplRange int deviceSplRange) { mMetadataType = metadataType; mIsDownmixing = isDownmixing; mDeviceSplRange = deviceSplRange; @@ -273,6 +286,50 @@ public class LoudnessCodecHelper { } } + /** + * Contains the properties necessary to identify the tracks that are receiving annotated + * loudness data. + **/ + @VisibleForTesting + static final class LoudnessTrackId { + private final int mSessionId; + + private final int mPid; + + private LoudnessTrackId(int sessionId, int pid) { + mSessionId = sessionId; + mPid = pid; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + // type check and cast + if (getClass() != obj.getClass()) { + return false; + } + final LoudnessTrackId lti = (LoudnessTrackId) obj; + return mSessionId == lti.mSessionId && mPid == lti.mPid; + } + + @Override + public int hashCode() { + return Objects.hash(mSessionId, mPid); + } + + @Override + public String toString() { + return "Loudness track id:" + + " session ID: " + mSessionId + + " pid: " + mPid; + } + } + @GuardedBy("mLock") private final HashMap<LoudnessCodecInputProperties, PersistableBundle> mCachedProperties = new HashMap<>(); @@ -290,120 +347,160 @@ public class LoudnessCodecHelper { mLoudnessUpdateDispatchers.unregister(dispatcher); } - void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) { + void startLoudnessCodecUpdates(int sessionId) { + int pid = Binder.getCallingPid(); if (DEBUG) { - Log.d(TAG, "startLoudnessCodecUpdates: piid " + piid + " codecInfos " + codecInfoList); + Log.d(TAG, + "startLoudnessCodecUpdates: sessionId " + sessionId + " pid " + pid); } + final LoudnessTrackId newConfig = new LoudnessTrackId(sessionId, pid); + HashSet<Integer> newPiids; synchronized (mLock) { - if (mStartedPiids.contains(piid)) { - Log.w(TAG, "Already started loudness updates for piid " + piid); + if (mStartedConfigInfo.containsKey(newConfig)) { + Log.w(TAG, "Already started loudness updates for config: " + newConfig); return; } - Set<LoudnessCodecInfo> infoSet = new HashSet<>(codecInfoList); - mStartedPiids.put(piid, infoSet); - - int pid = Binder.getCallingPid(); - mPiidToPidCache.put(piid, pid); - sLogger.enqueue(LoudnessEvent.getStartPiid(piid, pid)); + mStartedConfigInfo.put(newConfig, new HashSet<>()); + newPiids = new HashSet<>(); + mStartedConfigPiids.put(newConfig, newPiids); } try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { mAudioService.getActivePlaybackConfigurations().stream().filter( - conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent( - this::updateCodecParametersForConfiguration); + conf -> conf.getSessionId() == sessionId + && conf.getClientPid() == pid).forEach(apc -> { + int piid = apc.getPlayerInterfaceId(); + synchronized (mLock) { + newPiids.add(piid); + mPiidToPidCache.put(piid, pid); + sLogger.enqueue(LoudnessEvent.getStartPiid(piid, pid)); + } + }); } } - void stopLoudnessCodecUpdates(int piid) { + void stopLoudnessCodecUpdates(int sessionId) { + int pid = Binder.getCallingPid(); if (DEBUG) { - Log.d(TAG, "stopLoudnessCodecUpdates: piid " + piid); + Log.d(TAG, + "stopLoudnessCodecUpdates: sessionId " + sessionId + " pid " + pid); } + final LoudnessTrackId config = new LoudnessTrackId(sessionId, pid); synchronized (mLock) { - if (!mStartedPiids.contains(piid)) { - Log.w(TAG, "Loudness updates are already stopped for piid " + piid); + if (!mStartedConfigInfo.containsKey(config)) { + Log.w(TAG, "Loudness updates are already stopped config: " + config); return; } - mStartedPiids.remove(piid); - sLogger.enqueue(LoudnessEvent.getStopPiid(piid, mPiidToPidCache.get(piid, -1))); - mPiidToDeviceIdCache.delete(piid); - mPiidToPidCache.delete(piid); + final Set<Integer> startedPiidSet = mStartedConfigPiids.get(config); + if (startedPiidSet == null) { + Log.e(TAG, "Loudness updates are already stopped config: " + config); + return; + } + for (Integer piid : startedPiidSet) { + sLogger.enqueue(LoudnessEvent.getStopPiid(piid, mPiidToPidCache.get(piid, -1))); + mPiidToDeviceIdCache.delete(piid); + mPiidToPidCache.delete(piid); + } + mStartedConfigPiids.remove(config); + mStartedConfigInfo.remove(config); } } - void addLoudnessCodecInfo(int piid, int mediaCodecHash, LoudnessCodecInfo info) { + void addLoudnessCodecInfo(int sessionId, int mediaCodecHash, + LoudnessCodecInfo info) { + int pid = Binder.getCallingPid(); if (DEBUG) { - Log.d(TAG, "addLoudnessCodecInfo: piid " + piid + " mcHash " + mediaCodecHash + " info " - + info); + Log.d(TAG, "addLoudnessCodecInfo: sessionId " + sessionId + + " mcHash " + mediaCodecHash + " info " + info + " pid " + pid); } + final LoudnessTrackId config = new LoudnessTrackId(sessionId, pid); Set<LoudnessCodecInfo> infoSet; + Set<Integer> piids; synchronized (mLock) { - if (!mStartedPiids.contains(piid)) { - Log.w(TAG, "Cannot add new loudness info for stopped piid " + piid); + if (!mStartedConfigInfo.containsKey(config) || !mStartedConfigPiids.containsKey( + config)) { + Log.w(TAG, "Cannot add new loudness info for stopped config " + config); return; } - infoSet = mStartedPiids.get(piid); + piids = mStartedConfigPiids.get(config); + infoSet = mStartedConfigInfo.get(config); infoSet.add(info); } try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - mAudioService.getActivePlaybackConfigurations().stream().filter( - conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent( - apc -> { - final AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo(); - if (deviceInfo != null) { - PersistableBundle updateBundle = new PersistableBundle(); - synchronized (mLock) { - updateBundle.putPersistableBundle( - Integer.toString(mediaCodecHash), - getCodecBundle_l(deviceInfo, info)); - } - if (!updateBundle.isDefinitelyEmpty()) { - dispatchNewLoudnessParameters(piid, updateBundle); - } - } - }); + final PersistableBundle updateBundle = new PersistableBundle(); + Optional<AudioPlaybackConfiguration> apc = + mAudioService.getActivePlaybackConfigurations().stream().filter( + conf -> conf.getSessionId() == sessionId + && conf.getClientPid() == pid).findFirst(); + if (apc.isEmpty()) { + if (DEBUG) { + Log.d(TAG, + "No APCs found when adding loudness codec info. Using AudioAttributes" + + " routing for initial update"); + } + updateBundle.putPersistableBundle(Integer.toString(mediaCodecHash), + getLoudnessParams(info)); + } else { + final AudioDeviceInfo deviceInfo = apc.get().getAudioDeviceInfo(); + if (deviceInfo != null) { + synchronized (mLock) { + // found a piid that matches the configuration + piids.add(apc.get().getPlayerInterfaceId()); + + updateBundle.putPersistableBundle( + Integer.toString(mediaCodecHash), + getCodecBundle_l(deviceInfo.getInternalType(), + deviceInfo.getAddress(), info)); + } + } + } + if (!updateBundle.isDefinitelyEmpty()) { + dispatchNewLoudnessParameters(sessionId, updateBundle); + } } } - void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) { + void removeLoudnessCodecInfo(int sessionId, LoudnessCodecInfo codecInfo) { if (DEBUG) { - Log.d(TAG, "removeLoudnessCodecInfo: piid " + piid + " info " + codecInfo); + Log.d(TAG, "removeLoudnessCodecInfo: session ID" + sessionId + " info " + codecInfo); } + + int pid = Binder.getCallingPid(); + final LoudnessTrackId config = new LoudnessTrackId(sessionId, pid); synchronized (mLock) { - if (!mStartedPiids.contains(piid)) { - Log.w(TAG, "Cannot remove loudness info for stopped piid " + piid); + if (!mStartedConfigInfo.containsKey(config) || !mStartedConfigPiids.containsKey( + config)) { + Log.w(TAG, "Cannot remove loudness info for stopped config " + config); return; } - final Set<LoudnessCodecInfo> infoSet = mStartedPiids.get(piid); - infoSet.remove(codecInfo); + final Set<LoudnessCodecInfo> codecInfos = mStartedConfigInfo.get(config); + if (!codecInfos.remove(codecInfo)) { + Log.w(TAG, "Could not find to remove codecInfo " + codecInfo); + } } } - PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) { + PersistableBundle getLoudnessParams(LoudnessCodecInfo codecInfo) { if (DEBUG) { - Log.d(TAG, "getLoudnessParams: piid " + piid + " codecInfo " + codecInfo); - } - try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { - final List<AudioPlaybackConfiguration> configs = - mAudioService.getActivePlaybackConfigurations(); - - for (final AudioPlaybackConfiguration apc : configs) { - if (apc.getPlayerInterfaceId() == piid) { - final AudioDeviceInfo info = apc.getAudioDeviceInfo(); - if (info == null) { - Log.i(TAG, "Player with piid " + piid + " is not assigned any device"); - break; - } - synchronized (mLock) { - return getCodecBundle_l(info, codecInfo); - } - } + Log.d(TAG, "getLoudnessParams: codecInfo " + codecInfo); + } + final ArrayList<AudioDeviceAttributes> devicesForAttributes = + mAudioService.getDevicesForAttributesInt(new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .build(), /*forVolume=*/false); + if (!devicesForAttributes.isEmpty()) { + final AudioDeviceAttributes audioDeviceAttribute = devicesForAttributes.get(0); + synchronized (mLock) { + return getCodecBundle_l(audioDeviceAttribute.getInternalType(), + audioDeviceAttribute.getAddress(), codecInfo); } } @@ -444,13 +541,21 @@ public class LoudnessCodecHelper { continue; } mPiidToDeviceIdCache.put(piid, deviceInfo.getId()); - if (mStartedPiids.contains(piid)) { + final LoudnessTrackId config = new LoudnessTrackId(apc.getSessionId(), + apc.getClientPid()); + if (mStartedConfigInfo.containsKey(config) && mStartedConfigPiids.containsKey( + config)) { + if (DEBUG) { + Log.d(TAG, "Updating config: " + config + " with APC " + apc); + } updateApcList.add(apc); + // update the started piid set + mStartedConfigPiids.get(config).add(piid); } } } - updateApcList.forEach(apc -> updateCodecParametersForConfiguration(apc)); + updateApcList.forEach(this::updateCodecParametersForConfiguration); } /** Updates and dispatches the new loudness parameters for all its registered codecs. */ @@ -458,13 +563,18 @@ public class LoudnessCodecHelper { // Registered clients pw.println("\nRegistered clients:\n"); synchronized (mLock) { - for (int i = 0; i < mStartedPiids.size(); ++i) { - int piid = mStartedPiids.keyAt(i); - int pid = mPiidToPidCache.get(piid, -1); - final Set<LoudnessCodecInfo> codecInfos = mStartedPiids.get(piid); - pw.println(String.format("Player piid %d pid %d active codec types %s\n", piid, - pid, codecInfos.stream().map(Object::toString).collect( - Collectors.joining(", ")))); + for (Map.Entry<LoudnessTrackId, Set<Integer>> entry : mStartedConfigPiids.entrySet()) { + for (Integer piid : entry.getValue()) { + int pid = mPiidToPidCache.get(piid, -1); + final Set<LoudnessCodecInfo> codecInfos = mStartedConfigInfo.get( + entry.getKey()); + if (codecInfos != null) { + pw.println( + String.format("Player piid %d pid %d active codec types %s\n", piid, + pid, codecInfos.stream().map(Object::toString).collect( + Collectors.joining(", ")))); + } + } } pw.println(); } @@ -481,10 +591,12 @@ public class LoudnessCodecHelper { if (DEBUG) { Log.d(TAG, "Removing piid " + piid); } - mStartedPiids.delete(piid); mPiidToDeviceIdCache.delete(piid); } } + + mStartedConfigPiids.entrySet().removeIf(entry -> entry.getKey().mPid == pid); + mStartedConfigInfo.entrySet().removeIf(entry -> entry.getKey().mPid == pid); } } @@ -499,46 +611,55 @@ public class LoudnessCodecHelper { } final PersistableBundle allBundles = new PersistableBundle(); - final int piid = apc.getPlayerInterfaceId(); synchronized (mLock) { - final Set<LoudnessCodecInfo> codecInfos = mStartedPiids.get(piid); - + final LoudnessTrackId config = new LoudnessTrackId(apc.getSessionId(), + apc.getClientPid()); + final Set<LoudnessCodecInfo> codecInfos = mStartedConfigInfo.get(config); final AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo(); + if (codecInfos != null && deviceInfo != null) { for (LoudnessCodecInfo info : codecInfos) { - allBundles.putPersistableBundle(Integer.toString(info.hashCode()), - getCodecBundle_l(deviceInfo, info)); + if (info != null) { + allBundles.putPersistableBundle(Integer.toString(info.hashCode()), + getCodecBundle_l(deviceInfo.getInternalType(), + deviceInfo.getAddress(), info)); + } } } } if (!allBundles.isDefinitelyEmpty()) { - dispatchNewLoudnessParameters(piid, allBundles); + dispatchNewLoudnessParameters(apc.getSessionId(), allBundles); } } - private void dispatchNewLoudnessParameters(int piid, PersistableBundle bundle) { + private void dispatchNewLoudnessParameters(int sessionId, + PersistableBundle bundle) { if (DEBUG) { - Log.d(TAG, "dispatchNewLoudnessParameters: piid " + piid + " bundle: " + bundle); + Log.d(TAG, + "dispatchNewLoudnessParameters: sessionId " + sessionId + " bundle: " + bundle); } final int nbDispatchers = mLoudnessUpdateDispatchers.beginBroadcast(); for (int i = 0; i < nbDispatchers; ++i) { try { mLoudnessUpdateDispatchers.getBroadcastItem(i) - .dispatchLoudnessCodecParameterChange(piid, bundle); + .dispatchLoudnessCodecParameterChange(sessionId, bundle); } catch (RemoteException e) { - Log.e(TAG, "Error dispatching for piid: " + piid + " bundle: " + bundle , e); + Log.e(TAG, "Error dispatching for sessionId " + sessionId + " bundle: " + bundle, + e); } } mLoudnessUpdateDispatchers.finishBroadcast(); } @GuardedBy("mLock") - private PersistableBundle getCodecBundle_l(AudioDeviceInfo deviceInfo, - LoudnessCodecInfo codecInfo) { + private PersistableBundle getCodecBundle_l(int internalDeviceType, + String address, + LoudnessCodecInfo codecInfo) { LoudnessCodecInputProperties.Builder builder = new LoudnessCodecInputProperties.Builder(); - LoudnessCodecInputProperties prop = builder.setDeviceSplRange(getDeviceSplRange(deviceInfo)) + LoudnessCodecInputProperties prop = builder.setDeviceSplRange( + getDeviceSplRange(internalDeviceType, address)) .setIsDownmixing(codecInfo.isDownmixing) .setMetadataType(codecInfo.metadataType) .build(); @@ -552,14 +673,15 @@ public class LoudnessCodecHelper { } @DeviceSplRange - private int getDeviceSplRange(AudioDeviceInfo deviceInfo) { - final int internalDeviceType = deviceInfo.getInternalType(); - final @AudioDeviceCategory int deviceCategory; - if (automaticBtDeviceType()) { - deviceCategory = mAudioService.getBluetoothAudioDeviceCategory(deviceInfo.getAddress()); - } else { - deviceCategory = mAudioService.getBluetoothAudioDeviceCategory_legacy( - deviceInfo.getAddress(), AudioSystem.isBluetoothLeDevice(internalDeviceType)); + private int getDeviceSplRange(int internalDeviceType, String address) { + @AudioDeviceCategory int deviceCategory; + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + if (automaticBtDeviceType()) { + deviceCategory = mAudioService.getBluetoothAudioDeviceCategory(address); + } else { + deviceCategory = mAudioService.getBluetoothAudioDeviceCategory_legacy( + address, AudioSystem.isBluetoothLeDevice(internalDeviceType)); + } } if (internalDeviceType == AudioSystem.DEVICE_OUT_SPEAKER) { final String splRange = SystemProperties.get( @@ -595,20 +717,28 @@ public class LoudnessCodecHelper { private static String splRangeToString(@DeviceSplRange int splRange) { switch (splRange) { - case SPL_RANGE_LARGE: return "large"; - case SPL_RANGE_MEDIUM: return "medium"; - case SPL_RANGE_SMALL: return "small"; - default: return "unknown"; + case SPL_RANGE_LARGE: + return "large"; + case SPL_RANGE_MEDIUM: + return "medium"; + case SPL_RANGE_SMALL: + return "small"; + default: + return "unknown"; } } @DeviceSplRange private static int stringToSplRange(String splRange) { switch (splRange) { - case "large": return SPL_RANGE_LARGE; - case "medium": return SPL_RANGE_MEDIUM; - case "small": return SPL_RANGE_SMALL; - default: return SPL_RANGE_UNKNOWN; + case "large": + return SPL_RANGE_LARGE; + case "medium": + return SPL_RANGE_MEDIUM; + case "small": + return SPL_RANGE_SMALL; + default: + return SPL_RANGE_UNKNOWN; } } } diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index de4979a0c826..5b9469bc5610 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -20,7 +20,8 @@ import android.app.IWallpaperManager; import android.app.backup.BackupAgentHelper; import android.app.backup.BackupAnnotations.BackupDestination; import android.app.backup.BackupDataInput; -import android.app.backup.BackupHelper; +import android.app.backup.BackupHelperWithLogger; +import android.app.backup.BackupRestoreEventLogger; import android.app.backup.FullBackup; import android.app.backup.FullBackupDataOutput; import android.app.backup.WallpaperBackupHelper; @@ -33,9 +34,10 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.util.Slog; - import com.google.android.collect.Sets; +import com.android.server.backup.Flags; + import java.io.File; import java.io.IOException; import java.util.Set; @@ -107,10 +109,12 @@ public class SystemBackupAgent extends BackupAgentHelper { private int mUserId = UserHandle.USER_SYSTEM; private boolean mIsProfileUser = false; + private BackupRestoreEventLogger mLogger; @Override public void onCreate(UserHandle user, @BackupDestination int backupDestination) { super.onCreate(user, backupDestination); + mLogger = this.getBackupRestoreEventLogger(); mUserId = user.getIdentifier(); if (mUserId != UserHandle.USER_SYSTEM) { @@ -209,9 +213,12 @@ public class SystemBackupAgent extends BackupAgentHelper { } } - private void addHelperIfEligibleForUser(String keyPrefix, BackupHelper helper) { + private void addHelperIfEligibleForUser(String keyPrefix, BackupHelperWithLogger helper) { if (isHelperEligibleForUser(keyPrefix)) { addHelper(keyPrefix, helper); + if (Flags.enableMetricsSystemBackupAgents()) { + helper.setLogger(mLogger); + } } } diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContext.java b/services/core/java/com/android/server/biometrics/log/BiometricContext.java index f8a98675c0c7..7f04628ff6de 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricContext.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricContext.java @@ -87,10 +87,30 @@ public interface BiometricContext { * * @param context context that will be modified when changed * @param consumer callback when the context is modified + * + * @deprecated instead use {@link BiometricContext#subscribe(OperationContextExt, Consumer, + * Consumer, AuthenticateOptions)} + * TODO (b/294161627): Delete this API once Flags.DE_HIDL is removed. */ + @Deprecated void subscribe(@NonNull OperationContextExt context, @NonNull Consumer<OperationContext> consumer); + /** + * Subscribe to context changes and start the HAL operation. + * + * Note that this method only notifies for properties that are visible to the HAL. + * + * @param context context that will be modified when changed + * @param startHalConsumer callback to start HAL operation after subscription is done + * @param updateContextConsumer callback when the context is modified + * @param options authentication options for updating the context + */ + void subscribe(@NonNull OperationContextExt context, + @NonNull Consumer<OperationContext> startHalConsumer, + @NonNull Consumer<OperationContext> updateContextConsumer, + @Nullable AuthenticateOptions options); + /** Unsubscribe from context changes. */ void unsubscribe(@NonNull OperationContextExt context); diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java index 535b7b743625..d8dfa60bdb51 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java @@ -238,6 +238,19 @@ public final class BiometricContextProvider implements BiometricContext { } @Override + public void subscribe(@NonNull OperationContextExt context, + @NonNull Consumer<OperationContext> startHalConsumer, + @NonNull Consumer<OperationContext> updateContextConsumer, + @Nullable AuthenticateOptions options) { + mSubscribers.put(updateContext(context, context.isCrypto()), updateContextConsumer); + if (options != null) { + startHalConsumer.accept(context.toAidlContext(options)); + } else { + startHalConsumer.accept(context.toAidlContext()); + } + } + + @Override public void unsubscribe(@NonNull OperationContextExt context) { mSubscribers.remove(context); } diff --git a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java index 0045d44af9a1..da4e5154d49e 100644 --- a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java +++ b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java @@ -102,6 +102,24 @@ public class OperationContextExt { * @return the underlying AIDL context */ @NonNull + public OperationContext toAidlContext(@NonNull AuthenticateOptions options) { + if (options instanceof FaceAuthenticateOptions) { + return toAidlContext((FaceAuthenticateOptions) options); + } + if (options instanceof FingerprintAuthenticateOptions) { + return toAidlContext((FingerprintAuthenticateOptions) options); + } + throw new IllegalStateException("Authenticate options are invalid."); + } + + /** + * Gets the subset of the context that can be shared with the HAL and updates + * it with the given options. + * + * @param options authenticate options + * @return the underlying AIDL context + */ + @NonNull public OperationContext toAidlContext(@NonNull FaceAuthenticateOptions options) { mAidlContext.authenticateReason = AuthenticateReason .faceAuthenticateReason(getAuthReason(options)); diff --git a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java index 0f01510bd908..b0649b90c466 100644 --- a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java @@ -22,6 +22,7 @@ import android.content.Context; import android.hardware.biometrics.BiometricAuthenticator; import android.os.IBinder; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.OperationContextExt; @@ -93,6 +94,9 @@ public abstract class HalClientMonitor<T> extends BaseClientMonitor { } protected OperationContextExt getOperationContext() { + if (Flags.deHidl()) { + return mOperationContext; + } return getBiometricContext().updateContext(mOperationContext, isCryptoOperation()); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java index 470dc4b7172c..22e399c6747b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java @@ -37,6 +37,7 @@ import android.util.Slog; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -153,7 +154,11 @@ public class FaceAuthenticationClient 0 /* vendorCode */); mCallback.onClientFinished(this, false /* success */); } else { - mCancellationSignal = doAuthenticate(); + if (Flags.deHidl()) { + startAuthenticate(); + } else { + mCancellationSignal = doAuthenticate(); + } } } catch (RemoteException e) { Slog.e(TAG, "Remote exception when requesting auth", e); @@ -182,6 +187,33 @@ public class FaceAuthenticationClient } } + private void startAuthenticate() throws RemoteException { + final AidlSession session = getFreshDaemon(); + + if (session.hasContextMethods()) { + final OperationContextExt opContext = getOperationContext(); + getBiometricContext().subscribe(opContext, ctx -> { + try { + mCancellationSignal = session.getSession().authenticateWithContext( + mOperationId, ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting auth", e); + onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, + 0 /* vendorCode */); + mCallback.onClientFinished(this, false /* success */); + } + }, ctx -> { + try { + session.getSession().onContextChanged(ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify context changed", e); + } + }, getOptions()); + } else { + mCancellationSignal = session.getSession().authenticate(mOperationId); + } + } + @Override protected void stopHalOperation() { unsubscribeBiometricContext(); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java index a529fb9779a7..5ddddda4e201 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceDetectClient.java @@ -29,6 +29,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.OperationContextExt; @@ -111,7 +112,11 @@ public class FaceDetectClient extends AcquisitionClient<AidlSession> implements } try { - mCancellationSignal = doDetectInteraction(); + if (Flags.deHidl()) { + startDetect(); + } else { + mCancellationSignal = doDetectInteraction(); + } } catch (RemoteException e) { Slog.e(TAG, "Remote exception when requesting face detect", e); mCallback.onClientFinished(this, false /* success */); @@ -138,6 +143,30 @@ public class FaceDetectClient extends AcquisitionClient<AidlSession> implements } } + private void startDetect() throws RemoteException { + final AidlSession session = getFreshDaemon(); + + if (session.hasContextMethods()) { + final OperationContextExt opContext = getOperationContext(); + getBiometricContext().subscribe(opContext, ctx -> { + try { + mCancellationSignal = session.getSession().detectInteractionWithContext(ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting face detect", e); + mCallback.onClientFinished(this, false /* success */); + } + }, ctx -> { + try { + session.getSession().onContextChanged(ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify context changed", e); + } + }, mOptions); + } else { + mCancellationSignal = session.getSession().detectInteraction(); + } + } + @Override public void onInteractionDetected() { vibrateSuccess(); diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java index 0af6e40434ef..f5c452992674 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java @@ -36,6 +36,7 @@ import android.util.Slog; import android.view.Surface; import com.android.internal.R; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.HardwareAuthTokenUtils; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; @@ -187,7 +188,11 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { features[i] = featureList.get(i); } - mCancellationSignal = doEnroll(features); + if (Flags.deHidl()) { + startEnroll(features); + } else { + mCancellationSignal = doEnroll(features); + } } catch (RemoteException | IllegalArgumentException e) { Slog.e(TAG, "Exception when requesting enroll", e); onError(BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS, 0 /* vendorCode */); @@ -231,6 +236,48 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { } } + private void startEnroll(byte[] features) throws RemoteException { + final AidlSession session = getFreshDaemon(); + final HardwareAuthToken hat = + HardwareAuthTokenUtils.toHardwareAuthToken(mHardwareAuthToken); + + if (session.hasContextMethods()) { + final OperationContextExt opContext = getOperationContext(); + getBiometricContext().subscribe(opContext, ctx -> { + try { + if (session.supportsFaceEnrollOptions()) { + FaceEnrollOptions options = new FaceEnrollOptions(); + options.hardwareAuthToken = hat; + options.enrollmentType = EnrollmentType.DEFAULT; + options.features = features; + options.nativeHandlePreview = null; + options.context = ctx; + options.surfacePreview = mPreviewSurface; + mCancellationSignal = session.getSession().enrollWithOptions(options); + } else { + mCancellationSignal = session.getSession().enrollWithContext( + hat, EnrollmentType.DEFAULT, features, mHwPreviewHandle, ctx); + } + } catch (RemoteException e) { + Slog.e(TAG, "Exception when requesting enroll", e); + onError(BiometricFaceConstants.FACE_ERROR_UNABLE_TO_PROCESS, + 0 /* vendorCode */); + mCallback.onClientFinished(this, false /* success */); + } + }, ctx -> { + try { + session.getSession().onContextChanged(ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify context changed", e); + } + }, null /* options */); + } else { + mCancellationSignal = session.getSession().enroll(hat, EnrollmentType.DEFAULT, features, + mHwPreviewHandle); + } + } + + @Override protected void stopHalOperation() { unsubscribeBiometricContext(); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 145885de5c32..f7e812330ece 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -44,6 +44,7 @@ import android.provider.Settings; import android.util.Slog; import com.android.internal.R; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.CallbackWithProbe; @@ -297,7 +298,11 @@ public class FingerprintAuthenticationClient } try { - mCancellationSignal = doAuthenticate(); + if (Flags.deHidl()) { + startAuthentication(); + } else { + mCancellationSignal = doAuthenticate(); + } } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); onError( @@ -353,6 +358,51 @@ public class FingerprintAuthenticationClient return cancel; } + private void startAuthentication() { + final AidlSession session = getFreshDaemon(); + final OperationContextExt opContext = getOperationContext(); + + getBiometricContext().subscribe(opContext, ctx -> { + try { + if (session.hasContextMethods()) { + mCancellationSignal = session.getSession().authenticateWithContext(mOperationId, + ctx); + } else { + mCancellationSignal = session.getSession().authenticate(mOperationId); + } + + if (getBiometricContext().isAwake()) { + mALSProbeCallback.getProbe().enable(); + } + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); + onError( + BiometricFingerprintConstants.FINGERPRINT_ERROR_HW_UNAVAILABLE, + 0 /* vendorCode */); + mSensorOverlays.hide(getSensorId()); + if (sidefpsControllerRefactor()) { + mAuthenticationStateListeners.onAuthenticationStopped(); + } + mCallback.onClientFinished(this, false /* success */); + } + }, ctx -> { + if (session.hasContextMethods()) { + try { + session.getSession().onContextChanged(ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify context changed", e); + } + } + + final boolean isAwake = getBiometricContext().isAwake(); + if (isAwake) { + mALSProbeCallback.getProbe().enable(); + } else { + mALSProbeCallback.getProbe().disable(); + } + }, getOptions()); + } + @Override protected void stopHalOperation() { mSensorOverlays.hide(getSensorId()); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java index 3aab7b300a87..a7fb7741fee1 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java @@ -31,6 +31,7 @@ import android.os.RemoteException; import android.util.Slog; import com.android.server.biometrics.BiometricsProto; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.OperationContextExt; @@ -105,7 +106,11 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession> this); try { - mCancellationSignal = doDetectInteraction(); + if (Flags.deHidl()) { + startDetectInteraction(); + } else { + mCancellationSignal = doDetectInteraction(); + } } catch (RemoteException e) { Slog.e(TAG, "Remote exception when requesting finger detect", e); mSensorOverlays.hide(getSensorId()); @@ -139,6 +144,32 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession> } } + private void startDetectInteraction() throws RemoteException { + final AidlSession session = getFreshDaemon(); + + if (session.hasContextMethods()) { + final OperationContextExt opContext = getOperationContext(); + getBiometricContext().subscribe(opContext, ctx -> { + try { + mCancellationSignal = session.getSession().detectInteractionWithContext( + ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to start detect interaction", e); + mSensorOverlays.hide(getSensorId()); + mCallback.onClientFinished(this, false /* success */); + } + }, ctx -> { + try { + session.getSession().onContextChanged(ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify context changed", e); + } + }, mOptions); + } else { + mCancellationSignal = session.getSession().detectInteraction(); + } + } + @Override public void onInteractionDetected() { vibrateSuccess(); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index bf5011de1e59..3fb9223249b4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -39,6 +39,7 @@ import android.os.RemoteException; import android.util.Slog; import android.view.accessibility.AccessibilityManager; +import com.android.server.biometrics.Flags; import com.android.server.biometrics.HardwareAuthTokenUtils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; @@ -200,7 +201,11 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement BiometricNotificationUtils.cancelBadCalibrationNotification(getContext()); try { - mCancellationSignal = doEnroll(); + if (Flags.deHidl()) { + startEnroll(); + } else { + mCancellationSignal = doEnroll(); + } } catch (RemoteException e) { Slog.e(TAG, "Remote exception when requesting enroll", e); onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS, @@ -237,6 +242,35 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement } } + private void startEnroll() throws RemoteException { + final AidlSession session = getFreshDaemon(); + final HardwareAuthToken hat = + HardwareAuthTokenUtils.toHardwareAuthToken(mHardwareAuthToken); + + if (session.hasContextMethods()) { + final OperationContextExt opContext = getOperationContext(); + getBiometricContext().subscribe(opContext, ctx -> { + try { + mCancellationSignal = session.getSession().enrollWithContext( + hat, ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when requesting enroll", e); + onError(BiometricFingerprintConstants.FINGERPRINT_ERROR_UNABLE_TO_PROCESS, + 0 /* vendorCode */); + mCallback.onClientFinished(this, false /* success */); + } + }, ctx -> { + try { + session.getSession().onContextChanged(ctx); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to notify context changed", e); + } + }, null /* options */); + } else { + mCancellationSignal = session.getSession().enroll(hat); + } + } + @Override protected void stopHalOperation() { mSensorOverlays.hide(getSensorId()); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index d34661d4d6ac..34e75c087864 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -47,6 +47,7 @@ import android.media.AudioDeviceInfo; import android.media.AudioProfile; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager.TvInputCallback; +import android.os.Handler; import android.util.Slog; import android.util.SparseBooleanArray; @@ -97,9 +98,15 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { private boolean mSystemAudioMute = false; // If true, do not do routing control/send active source for internal source. - // Set to true when the device was woken up by <Text/Image View On>. + // Set to true for a short duration when the device is woken up by <Text/Image View On>. private boolean mSkipRoutingControl; + // Handler for posting a runnable to set `mSkipRoutingControl` to false after a delay + private final Handler mSkipRoutingControlHandler; + + // Runnable that sets `mSkipRoutingControl` to false + private final Runnable mResetSkipRoutingControlRunnable = () -> mSkipRoutingControl = false; + // Message buffer used to buffer selected messages to process later. <Active Source> // from a source device, for instance, needs to be buffered if the device is not // discovered yet. The buffered commands are taken out and when they are ready to @@ -162,6 +169,7 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL) == HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED; mStandbyHandler = new HdmiCecStandbyModeHandler(service, this); + mSkipRoutingControlHandler = new Handler(service.getServiceLooper()); } @Override @@ -184,7 +192,14 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { mService.getHdmiCecNetwork().addCecSwitch( mService.getHdmiCecNetwork().getPhysicalAddress()); // TV is a CEC switch too. mTvInputs.clear(); + mSkipRoutingControl = (reason == HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE); + mSkipRoutingControlHandler.removeCallbacks(mResetSkipRoutingControlRunnable); + if (mSkipRoutingControl) { + mSkipRoutingControlHandler.postDelayed(mResetSkipRoutingControlRunnable, + HdmiConfig.TIMEOUT_MS); + } + launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC && reason != HdmiControlService.INITIATED_BY_BOOT_UP); resetSelectRequestBuffer(); diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index fb4943a9f4ca..67c23fc4db12 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1328,8 +1328,7 @@ public class InputManagerService extends IInputManager.Stub mPointerIconDisplayContext = null; } - updateAdditionalDisplayInputProperties(displayId, - AdditionalDisplayInputProperties::reset); + updateAdditionalDisplayInputProperties(displayId, AdditionalDisplayInputProperties::reset); // TODO(b/320763728): Rely on WindowInfosListener to determine when a display has been // removed in InputDispatcher instead of this callback. @@ -1812,8 +1811,6 @@ public class InputManagerService extends IInputManager.Stub mPointerIconType = icon.getType(); mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null; - if (!mCurrentDisplayProperties.pointerIconVisible) return false; - return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken); } } @@ -3478,6 +3475,10 @@ public class InputManagerService extends IInputManager.Stub private void applyAdditionalDisplayInputPropertiesLocked( AdditionalDisplayInputProperties properties) { // Handle changes to each of the individual properties. + // TODO(b/293587049): This approach for updating pointer display properties is only for when + // PointerChoreographer is disabled. Remove this logic when PointerChoreographer is + // permanently enabled. + if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) { mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible; if (properties.pointerIconVisible) { @@ -3496,7 +3497,6 @@ public class InputManagerService extends IInputManager.Stub != mCurrentDisplayProperties.mousePointerAccelerationEnabled) { mCurrentDisplayProperties.mousePointerAccelerationEnabled = properties.mousePointerAccelerationEnabled; - mNative.setMousePointerAccelerationEnabled(properties.mousePointerAccelerationEnabled); } } @@ -3509,7 +3509,16 @@ public class InputManagerService extends IInputManager.Stub properties = new AdditionalDisplayInputProperties(); mAdditionalDisplayInputProperties.put(displayId, properties); } + final boolean oldPointerIconVisible = properties.pointerIconVisible; + final boolean oldMouseAccelerationEnabled = properties.mousePointerAccelerationEnabled; updater.accept(properties); + if (oldPointerIconVisible != properties.pointerIconVisible) { + mNative.setPointerIconVisibility(displayId, properties.pointerIconVisible); + } + if (oldMouseAccelerationEnabled != properties.mousePointerAccelerationEnabled) { + mNative.setMousePointerAccelerationEnabled(displayId, + properties.mousePointerAccelerationEnabled); + } if (properties.allDefaults()) { mAdditionalDisplayInputProperties.remove(displayId); } diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index c3ef80f3624a..6f5202029998 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -119,7 +119,7 @@ interface NativeInputManagerService { void setPointerSpeed(int speed); - void setMousePointerAccelerationEnabled(boolean enabled); + void setMousePointerAccelerationEnabled(int displayId, boolean enabled); void setTouchpadPointerSpeed(int speed); @@ -190,6 +190,8 @@ interface NativeInputManagerService { boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId, @NonNull IBinder inputToken); + void setPointerIconVisibility(int displayId, boolean visible); + void requestPointerCapture(IBinder windowToken, boolean enabled); boolean canDispatchToDisplay(int deviceId, int displayId); @@ -352,7 +354,7 @@ interface NativeInputManagerService { public native void setPointerSpeed(int speed); @Override - public native void setMousePointerAccelerationEnabled(boolean enabled); + public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled); @Override public native void setTouchpadPointerSpeed(int speed); @@ -452,6 +454,9 @@ interface NativeInputManagerService { int pointerId, IBinder inputToken); @Override + public native void setPointerIconVisibility(int displayId, boolean visible); + + @Override public native void requestPointerCapture(IBinder windowToken, boolean enabled); @Override diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java index c76ca2beda96..fba71fd0ff9e 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java @@ -114,7 +114,7 @@ final class AdditionalSubtypeUtils { * @param userId The user ID to be associated with. */ static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + InputMethodMap methodMap, @UserIdInt int userId) { final File inputMethodDir = getInputMethodDir(userId); if (allSubtypes.isEmpty()) { @@ -143,7 +143,7 @@ final class AdditionalSubtypeUtils { @VisibleForTesting static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - ArrayMap<String, InputMethodInfo> methodMap, AtomicFile subtypesFile) { + InputMethodMap methodMap, AtomicFile subtypesFile) { // Safety net for the case that this function is called before methodMap is set. final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0; FileOutputStream fos = null; diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java index 4b85d099b600..62adb25954b4 100644 --- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java +++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java @@ -20,7 +20,6 @@ import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.util.ArrayMap; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -44,24 +43,23 @@ final class HardwareKeyboardShortcutController { return mUserId; } - HardwareKeyboardShortcutController( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) { mUserId = userId; reset(methodMap); } @GuardedBy("ImfLock.class") - void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) { + void reset(@NonNull InputMethodMap methodMap) { mSubtypeHandles.clear(); - final InputMethodSettings settings = new InputMethodSettings(methodMap, mUserId); - final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodListLocked(); + final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId); + final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList(); for (int i = 0; i < inputMethods.size(); ++i) { final InputMethodInfo imi = inputMethods.get(i); if (!imi.shouldShowInInputMethodPicker()) { continue; } final List<InputMethodSubtype> subtypes = - settings.getEnabledInputMethodSubtypeListLocked(imi, true); + settings.getEnabledInputMethodSubtypeList(imi, true); if (subtypes.isEmpty()) { mSubtypeHandles.add(InputMethodSubtypeHandle.of(imi, null)); } else { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java index 542165d06a92..6339686629f5 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java @@ -23,7 +23,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.Slog; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -204,7 +203,7 @@ final class InputMethodInfoUtils { */ @Nullable static InputMethodInfo chooseSystemVoiceIme( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @NonNull InputMethodMap methodMap, @Nullable String systemSpeechRecognizerPackageName, @Nullable String currentDefaultVoiceImeId) { if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 8448fc233ae2..beb68d3566c3 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -314,10 +314,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Nullable private VirtualDeviceManagerInternal mVdmInternal = null; - // All known input methods. - final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>(); - private final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>(); - // Mapping from deviceId to the device-specific imeId for that device. @GuardedBy("ImfLock.class") private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>(); @@ -330,7 +326,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController; /** - * Tracks how many times {@link #mMethodMap} was updated. + * Tracks how many times {@link #mSettings} was updated. */ @GuardedBy("ImfLock.class") private int mMethodMapUpdateCount = 0; @@ -472,7 +468,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) { - return mMethodMap.get(imeId); + return mSettings.getMethodMap().get(imeId); } /** @@ -1265,10 +1261,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } String curInputMethodId = mSettings.getSelectedInputMethod(); - final int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + final int numImes = methodList.size(); if (curInputMethodId != null) { for (int i = 0; i < numImes; i++) { - InputMethodInfo imi = mMethodList.get(i); + InputMethodInfo imi = methodList.get(i); if (imi.getId().equals(curInputMethodId)) { for (String pkg : packages) { if (imi.getPackageName().equals(pkg)) { @@ -1339,7 +1336,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onPackageDataCleared(String packageName, int uid) { boolean changed = false; - for (InputMethodInfo imi : mMethodList) { + for (InputMethodInfo imi : mSettings.getMethodList()) { if (imi.getPackageName().equals(packageName)) { mAdditionalSubtypeMap.remove(imi.getId()); changed = true; @@ -1347,7 +1344,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (changed) { AdditionalSubtypeUtils.save( - mAdditionalSubtypeMap, mMethodMap, mSettings.getCurrentUserId()); + mAdditionalSubtypeMap, mSettings.getMethodMap(), + mSettings.getCurrentUserId()); mChangedPackages.add(packageName); } } @@ -1405,10 +1403,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub InputMethodInfo curIm = null; String curInputMethodId = mSettings.getSelectedInputMethod(); - final int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + final int numImes = methodList.size(); if (curInputMethodId != null) { for (int i = 0; i < numImes; i++) { - InputMethodInfo imi = mMethodList.get(i); + InputMethodInfo imi = methodList.get(i); final String imiId = imi.getId(); if (imiId.equals(curInputMethodId)) { curIm = imi; @@ -1426,7 +1425,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + imi.getComponent()); mAdditionalSubtypeMap.remove(imi.getId()); AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, - mMethodMap, + mSettings.getMethodMap(), mSettings.getCurrentUserId()); } } @@ -1584,7 +1583,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (userId != currentUserId) { return; } - mSettings = new InputMethodSettings(mMethodMap, userId); + mSettings = InputMethodSettings.createEmptyMap(userId); if (mSystemReady) { // We need to rebuild IMEs. buildInputMethodListLocked(false /* resetDefaultEnabledIme */); @@ -1658,14 +1657,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mLastSwitchUserId = userId; // mSettings should be created before buildInputMethodListLocked - mSettings = new InputMethodSettings(mMethodMap, userId); + mSettings = InputMethodSettings.createEmptyMap(userId); AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId); mSwitchingController = - InputMethodSubtypeSwitchingController.createInstanceLocked(context, mMethodMap, - userId); + InputMethodSubtypeSwitchingController.createInstanceLocked(context, + mSettings.getMethodMap(), userId); mHardwareKeyboardShortcutController = - new HardwareKeyboardShortcutController(mMethodMap, userId); + new HardwareKeyboardShortcutController(mSettings.getMethodMap(), + mSettings.getCurrentUserId()); mMenuController = new InputMethodMenuController(this); mBindingController = bindingControllerForTesting != null @@ -1723,11 +1723,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private void resetDefaultImeLocked(Context context) { // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = getSelectedMethodIdLocked(); - if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) { + if (selectedMethodId != null + && !mSettings.getMethodMap().get(selectedMethodId).isSystem()) { return; } final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( - context, mSettings.getEnabledInputMethodListLocked()); + context, mSettings.getEnabledInputMethodList()); if (suitableImes.isEmpty()) { Slog.i(TAG, "No default found"); return; @@ -1791,7 +1792,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // ContentObserver should be registered again when the user is changed mSettingsObserver.registerContentObserverLocked(newUserId); - mSettings = new InputMethodSettings(mMethodMap, newUserId); + mSettings = InputMethodSettings.createEmptyMap(newUserId); // Additional subtypes should be reset when the user is changed AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId); final String defaultImiId = mSettings.getSelectedInputMethod(); @@ -1822,7 +1823,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (initialUserSwitch) { InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, newUserId), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); } if (DEBUG) { @@ -1899,7 +1900,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateFromSettingsLocked(true); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); } } } @@ -2001,9 +2002,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. //TODO(b/210039666): use cache. - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - final InputMethodInfo imi = methodMap.get(settings.getSelectedInputMethod()); + final InputMethodSettings settings = queryMethodMapForUser(userId); + final InputMethodInfo imi = settings.getMethodMap().get( + settings.getSelectedInputMethod()); return imi != null && imi.supportsStylusHandwriting(); } } @@ -2023,23 +2024,19 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness, int callingUid) { - final ArrayList<InputMethodInfo> methodList; final InputMethodSettings settings; if (userId == mSettings.getCurrentUserId() && directBootAwareness == DirectBootAwareness.AUTO) { - // Create a copy. - methodList = new ArrayList<>(mMethodList); settings = mSettings; } else { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, directBootAwareness); - settings = new InputMethodSettings(methodMap, userId); + settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + directBootAwareness); } + // Create a copy. + final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList()); // filter caller's access to input methods methodList.removeIf(imi -> !canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)); @@ -2052,12 +2049,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final ArrayList<InputMethodInfo> methodList; final InputMethodSettings settings; if (userId == mSettings.getCurrentUserId()) { - methodList = mSettings.getEnabledInputMethodListLocked(); + methodList = mSettings.getEnabledInputMethodList(); settings = mSettings; } else { - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - settings = new InputMethodSettings(methodMap, userId); - methodList = settings.getEnabledInputMethodListLocked(); + settings = queryMethodMapForUser(userId); + methodList = settings.getEnabledInputMethodList(); } // filter caller's access to input methods methodList.removeIf(imi -> @@ -2120,27 +2116,26 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputMethodInfo imi; String selectedMethodId = getSelectedMethodIdLocked(); if (imiId == null && selectedMethodId != null) { - imi = mMethodMap.get(selectedMethodId); + imi = mSettings.getMethodMap().get(selectedMethodId); } else { - imi = mMethodMap.get(imiId); + imi = mSettings.getMethodMap().get(imiId); } if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { return Collections.emptyList(); } - return mSettings.getEnabledInputMethodSubtypeListLocked( + return mSettings.getEnabledInputMethodSubtypeList( imi, allowsImplicitlyEnabledSubtypes); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodInfo imi = methodMap.get(imiId); + final InputMethodSettings settings = queryMethodMapForUser(userId); + final InputMethodInfo imi = settings.getMethodMap().get(imiId); if (imi == null) { return Collections.emptyList(); } - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); if (!canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)) { return Collections.emptyList(); } - return settings.getEnabledInputMethodSubtypeListLocked( + return settings.getEnabledInputMethodSubtypeList( imi, allowsImplicitlyEnabledSubtypes); } @@ -2343,7 +2338,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } String curId = getCurIdLocked(); - final InputMethodInfo curInputMethodInfo = mMethodMap.get(curId); + final InputMethodInfo curInputMethodInfo = mSettings.getMethodMap().get(curId); final boolean suppressesSpellChecker = curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker(); final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions = @@ -2559,7 +2554,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVirtualDeviceMethodMap.get(mDeviceIdToShowIme, currentMethodId); if (Objects.equals(deviceMethodId, currentMethodId)) { return currentMethodId; - } else if (!mMethodMap.containsKey(deviceMethodId)) { + } else if (!mSettings.getMethodMap().containsKey(deviceMethodId)) { if (DEBUG) { Slog.v(TAG, "Disabling IME on virtual device with id " + mDeviceIdToShowIme + " because its custom input method is not available: " + deviceMethodId); @@ -2601,7 +2596,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) { return false; } - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null) { return false; } @@ -3023,7 +3018,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } - List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilterLocked( + List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilter( InputMethodInfo::shouldShowInInputMethodPicker); final int numImes = imes.size(); if (numImes > 2) return true; @@ -3035,7 +3030,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = imes.get(i); final List<InputMethodSubtype> subtypes = - mSettings.getEnabledInputMethodSubtypeListLocked(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); final int subtypeCount = subtypes.size(); if (subtypeCount == 0) { ++nonAuxCount; @@ -3187,7 +3182,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext, mSettings.getCurrentUserId()); - List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked(); + List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); for (int i = 0; i < enabled.size(); i++) { // We allow the user to select "disabled until used" apps, so if they // are enabling one of those here we now need to make it enabled. @@ -3234,17 +3229,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // TODO: Instantiate mSwitchingController for each user. if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(mMethodMap); + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(mMethodMap); + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mMethodMap, mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getCurrentUserId()); } sendOnNavButtonFlagsChangedLocked(); } @@ -3268,7 +3263,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") void setInputMethodLocked(String id, int subtypeId, int deviceId) { - InputMethodInfo info = mMethodMap.get(id); + InputMethodInfo info = mSettings.getMethodMap().get(id); if (info == null) { throw getExceptionForUnknownImeId(id); } @@ -4017,7 +4012,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodInfo imi = mMethodMap.get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); @@ -4035,7 +4030,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodInfo imi = mMethodMap.get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); @@ -4055,10 +4050,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return false; } - final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked(); + final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtype(); final InputMethodInfo lastImi; if (lastIme != null) { - lastImi = mMethodMap.get(lastIme.first); + lastImi = mSettings.getMethodMap().get(lastIme.first); } else { lastImi = null; } @@ -4082,7 +4077,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // This is a safety net. If the currentSubtype can't be added to the history // and the framework couldn't find the last ime, we will make the last ime be // the most applicable enabled keyboard subtype of the system imes. - final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked(); + final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); if (enabled != null) { final int enabledCount = enabled.size(); final String locale; @@ -4097,7 +4092,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final InputMethodInfo imi = enabled.get(i); if (imi.getSubtypeCount() > 0 && imi.isSystem()) { InputMethodSubtype keyboardSubtype = - SubtypeUtils.findLastResortApplicableSubtypeLocked( + SubtypeUtils.findLastResortApplicableSubtype( SubtypeUtils.getSubtypes(imi), SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (keyboardSubtype != null) { @@ -4139,7 +4134,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) { final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( - onlyCurrentIme, mMethodMap.get(getSelectedMethodIdLocked()), mCurrentSubtype); + onlyCurrentIme, mSettings.getMethodMap().get(getSelectedMethodIdLocked()), + mCurrentSubtype); if (nextSubtype == null) { return false; } @@ -4155,8 +4151,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( - false /* onlyCurrentIme */, mMethodMap.get(getSelectedMethodIdLocked()), - mCurrentSubtype); + false /* onlyCurrentIme */, + mSettings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype); return nextSubtype != null; } } @@ -4169,12 +4165,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { if (mSettings.getCurrentUserId() == userId) { - return mSettings.getLastInputMethodSubtypeLocked(); + return mSettings.getLastInputMethodSubtype(); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - return settings.getLastInputMethodSubtypeLocked(); + final InputMethodSettings settings = queryMethodMapForUser(userId); + return settings.getLastInputMethodSubtype(); } } @@ -4218,14 +4213,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, DirectBootAwareness.AUTO); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); } @@ -4253,8 +4245,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { final boolean currentUser = (mSettings.getCurrentUserId() == userId); final InputMethodSettings settings = currentUser - ? mSettings - : new InputMethodSettings(queryMethodMapForUser(userId), userId); + ? mSettings : queryMethodMapForUser(userId); if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) { return; } @@ -4626,7 +4617,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (mSettings.getCurrentUserId() != mSwitchingController.getUserId()) { return; } - final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked()); + final InputMethodInfo imi = + mSettings.getMethodMap().get(getSelectedMethodIdLocked()); if (imi != null) { mSwitchingController.onUserActionLocked(imi, mCurrentSubtype); } @@ -4684,8 +4676,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } else { // Called with current IME's token. - if (mMethodMap.get(id) != null - && mSettings.getEnabledInputMethodListWithFilterLocked( + if (mSettings.getMethodMap().get(id) != null + && mSettings.getEnabledInputMethodListWithFilter( (info) -> info.getId().equals(id)).isEmpty()) { throw new IllegalStateException("Requested IME is not enabled: " + id); } @@ -4866,7 +4858,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController .getSortedInputMethodAndSubtypeList( showAuxSubtypes, isScreenLocked, true /* forImeMenu */, - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), + mSettings.getCurrentUserId()); mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, lastInputMethodId, lastInputMethodSubtypeId, imList); } @@ -5050,7 +5043,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean chooseNewDefaultIMELocked() { final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME( - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); if (imi != null) { if (DEBUG) { Slog.d(TAG, "New default IME was selected: " + imi.getId()); @@ -5062,17 +5055,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } - static void queryInputMethodServicesInternal(Context context, + @NonNull + static InputMethodSettings queryInputMethodServicesInternal(Context context, @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, @DirectBootAwareness int directBootAwareness) { final Context userAwareContext = context.getUserId() == userId ? context : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); - methodList.clear(); - methodMap.clear(); - final int directBootAwarenessFlags; switch (directBootAwareness) { case DirectBootAwareness.ANY: @@ -5095,24 +5085,23 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub new Intent(InputMethod.SERVICE_INTERFACE), PackageManager.ResolveInfoFlags.of(flags)); - methodList.ensureCapacity(services.size()); - methodMap.ensureCapacity(services.size()); - // Note: This is a temporary solution for Bug 261723412. If there is any better solution, // we should remove this data dependency. final List<String> enabledInputMethodList = InputMethodUtils.getEnabledInputMethodIdsForFiltering(context, userId); - filterInputMethodServices(additionalSubtypeMap, methodMap, methodList, - enabledInputMethodList, userAwareContext, services); + final InputMethodMap methodMap = filterInputMethodServices( + additionalSubtypeMap, enabledInputMethodList, userAwareContext, services); + return InputMethodSettings.create(methodMap, userId); } - static void filterInputMethodServices( + @NonNull + static InputMethodMap filterInputMethodServices( ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, List<String> enabledInputMethodList, Context userAwareContext, List<ResolveInfo> services) { final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>(); + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(services.size()); for (int i = 0; i < services.size(); ++i) { ResolveInfo ri = services.get(i); @@ -5141,7 +5130,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub imiPackageCount.put(packageName, 1 + imiPackageCount.getOrDefault(packageName, 0)); - methodList.add(imi); methodMap.put(imi.getId(), imi); if (DEBUG) { Slog.d(TAG, "Found an input method " + imi); @@ -5153,6 +5141,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.wtf(TAG, "Unable to load input method " + imeId, e); } } + return InputMethodMap.of(methodMap); } @GuardedBy("ImfLock.class") @@ -5168,8 +5157,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mMethodMapUpdateCount++; mMyPackageMonitor.clearKnownImePackageNamesLocked(); - queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(), - mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO); + mSettings = queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(), + mAdditionalSubtypeMap, DirectBootAwareness.AUTO); // Construct the set of possible IME packages for onPackageChanged() to avoid false // negatives when the package state remains to be the same but only the component state is @@ -5196,11 +5185,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!resetDefaultEnabledIme) { boolean enabledImeFound = false; boolean enabledNonAuxImeFound = false; - final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodListLocked(); + final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodList(); final int numImes = enabledImes.size(); for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = enabledImes.get(i); - if (mMethodList.contains(imi)) { + if (mSettings.getMethodMap().containsKey(imi.getId())) { enabledImeFound = true; if (!imi.isAuxiliaryIme()) { enabledNonAuxImeFound = true; @@ -5224,7 +5213,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) { final ArrayList<InputMethodInfo> defaultEnabledIme = - InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList, + InputMethodInfoUtils.getDefaultEnabledImes(mContext, mSettings.getMethodList(), reenableMinimumNonAuxSystemImes); final int numImes = defaultEnabledIme.size(); for (int i = 0; i < numImes; ++i) { @@ -5238,7 +5227,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final String defaultImiId = mSettings.getSelectedInputMethod(); if (!TextUtils.isEmpty(defaultImiId)) { - if (!mMethodMap.containsKey(defaultImiId)) { + if (!mSettings.getMethodMap().containsKey(defaultImiId)) { Slog.w(TAG, "Default IME is uninstalled. Choose new default IME."); if (chooseNewDefaultIMELocked()) { updateInputMethodsFromSettingsLocked(true); @@ -5253,23 +5242,23 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // TODO: Instantiate mSwitchingController for each user. if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(mMethodMap); + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getCurrentUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(mMethodMap); + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mMethodMap, mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getCurrentUserId()); } sendOnNavButtonFlagsChangedLocked(); // Notify InputMethodListListeners of the new installed InputMethods. - final List<InputMethodInfo> inputMethodList = new ArrayList<>(mMethodList); + final List<InputMethodInfo> inputMethodList = mSettings.getMethodList(); mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED, mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget(); } @@ -5290,7 +5279,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer); final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod(); final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme( - mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId); + mSettings.getMethodMap(), systemSpeechRecognizer, currentDefaultVoiceImeId); if (newSystemVoiceIme == null) { if (DEBUG) { Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked," @@ -5341,9 +5330,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } else { final List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings - .getEnabledInputMethodsAndSubtypeListLocked(); + .getEnabledInputMethodsAndSubtypeList(); StringBuilder builder = new StringBuilder(); - if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + if (mSettings.buildAndPutEnabledInputMethodsStrRemovingId( builder, enabledInputMethodsList, id)) { if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { // Disabled input method is currently selected, switch to another one. @@ -5357,7 +5346,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // new default one but only update the settings. InputMethodInfo newDefaultIme = InputMethodInfoUtils.getMostApplicableDefaultIME( - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); mSettings.putSelectedDefaultDeviceInputMethod( newDefaultIme == null ? "" : newDefaultIme.getId()); } @@ -5402,11 +5391,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) { - InputMethodInfo imi = mMethodMap.get(newDefaultIme); + InputMethodInfo imi = mSettings.getMethodMap().get(newDefaultIme); int lastSubtypeId = NOT_A_SUBTYPE_ID; // newDefaultIme is empty when there is no candidate for the selected IME. if (imi != null && !TextUtils.isEmpty(newDefaultIme)) { - String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme); + String subtypeHashCode = mSettings.getLastSubtypeForInputMethod(newDefaultIme); if (subtypeHashCode != null) { try { lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, @@ -5437,8 +5426,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return getCurrentInputMethodSubtypeLocked(); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryMethodMapForUser(userId); return settings.getCurrentInputMethodSubtypeForNonCurrentUsers(); } } @@ -5460,7 +5448,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return null; } final boolean subtypeIsSelected = mSettings.isSubtypeSelected(); - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null || imi.getSubtypeCount() == 0) { return null; } @@ -5472,7 +5460,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the most applicable subtype from explicitly or implicitly enabled // subtypes. List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - mSettings.getEnabledInputMethodSubtypeListLocked(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); // If there is only one explicitly or implicitly enabled subtype, // just returns it. if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { @@ -5480,11 +5468,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { final String locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId()) .get(0).toString(); - mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (mCurrentSubtype == null) { - mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } } @@ -5501,46 +5489,42 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) { + final InputMethodSettings settings; if (userId == mSettings.getCurrentUserId()) { - return mMethodMap.get(mSettings.getSelectedInputMethod()); + settings = mSettings; + } else { + final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = + new ArrayMap<>(); + AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); } - - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, DirectBootAwareness.AUTO); - InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - return methodMap.get(settings.getSelectedInputMethod()); + return settings.getMethodMap().get(settings.getSelectedInputMethod()); } - private ArrayMap<String, InputMethodInfo> queryMethodMapForUser(@UserIdInt int userId) { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); + private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - methodMap, methodList, DirectBootAwareness.AUTO); - return methodMap; + return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + DirectBootAwareness.AUTO); } @GuardedBy("ImfLock.class") private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { if (userId == mSettings.getCurrentUserId()) { - if (!mMethodMap.containsKey(imeId) - || !mSettings.getEnabledInputMethodListLocked() - .contains(mMethodMap.get(imeId))) { + if (!mSettings.getMethodMap().containsKey(imeId) + || !mSettings.getEnabledInputMethodList() + .contains(mSettings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID); return true; } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - if (!methodMap.containsKey(imeId) - || !settings.getEnabledInputMethodListLocked().contains(methodMap.get(imeId))) { + final InputMethodSettings settings = queryMethodMapForUser(userId); + if (!settings.getMethodMap().containsKey(imeId) + || !settings.getEnabledInputMethodList().contains( + settings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } settings.putSelectedInputMethod(imeId); @@ -5578,7 +5562,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void switchKeyboardLayoutLocked(int direction) { - final InputMethodInfo currentImi = mMethodMap.get(getSelectedMethodIdLocked()); + final InputMethodInfo currentImi = mSettings.getMethodMap().get( + getSelectedMethodIdLocked()); if (currentImi == null) { return; } @@ -5590,7 +5575,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (nextSubtypeHandle == null) { return; } - final InputMethodInfo nextImi = mMethodMap.get(nextSubtypeHandle.getImeId()); + final InputMethodInfo nextImi = mSettings.getMethodMap().get(nextSubtypeHandle.getImeId()); if (nextImi == null) { return; } @@ -5670,15 +5655,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) { synchronized (ImfLock.class) { if (userId == mSettings.getCurrentUserId()) { - if (!mMethodMap.containsKey(imeId)) { + if (!mSettings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } setInputMethodEnabledLocked(imeId, enabled); return true; } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - if (!methodMap.containsKey(imeId)) { + final InputMethodSettings settings = queryMethodMapForUser(userId); + if (!settings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } if (enabled) { @@ -5689,9 +5673,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub settings.putEnabledInputMethodsStr(newEnabledImeIdsStr); } } else { - settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + settings.buildAndPutEnabledInputMethodsStrRemovingId( new StringBuilder(), - settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId); + settings.getEnabledInputMethodsAndSubtypeList(), imeId); } return true; } @@ -5997,10 +5981,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { p.println("Current Input Method Manager state:"); - int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + int numImes = methodList.size(); p.println(" Input Methods: mMethodMapUpdateCount=" + mMethodMapUpdateCount); for (int i = 0; i < numImes; i++) { - InputMethodInfo info = mMethodList.get(i); + InputMethodInfo info = methodList.get(i); p.println(" InputMethod #" + i + ":"); info.dump(p, " "); } @@ -6047,7 +6032,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub p.println(" mSwitchingController:"); mSwitchingController.dump(p); p.println(" mSettings:"); - mSettings.dumpLocked(p, " "); + mSettings.dump(p, " "); p.println(" mStartInputHistory:"); mStartInputHistory.dump(pw, " "); @@ -6416,16 +6401,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub boolean failedToEnableUnknownIme = false; boolean previouslyEnabled = false; if (userId == mSettings.getCurrentUserId()) { - if (enabled && !mMethodMap.containsKey(imeId)) { + if (enabled && !mSettings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled); } } else { - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryMethodMapForUser(userId); if (enabled) { - if (!methodMap.containsKey(imeId)) { + if (!settings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { final String enabledImeIdsStr = settings.getEnabledInputMethodsStr(); @@ -6438,9 +6422,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } else { previouslyEnabled = - settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + settings.buildAndPutEnabledInputMethodsStrRemovingId( new StringBuilder(), - settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId); + settings.getEnabledInputMethodsAndSubtypeList(), imeId); } } if (failedToEnableUnknownIme) { @@ -6537,9 +6521,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mBindingController.unbindCurrentMethod(); // Enable default IMEs, disable others - var toDisable = mSettings.getEnabledInputMethodListLocked(); + var toDisable = mSettings.getEnabledInputMethodList(); var defaultEnabled = InputMethodInfoUtils.getDefaultEnabledImes( - mContext, mMethodList); + mContext, mSettings.getMethodList()); toDisable.removeAll(defaultEnabled); for (InputMethodInfo info : toDisable) { setInputMethodEnabledLocked(info.getId(), false); @@ -6554,22 +6538,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateInputMethodsFromSettingsLocked(true /* enabledMayChange */); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, mSettings.getCurrentUserId()), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); nextIme = mSettings.getSelectedInputMethod(); - nextEnabledImes = mSettings.getEnabledInputMethodListLocked(); + nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - methodMap, methodList, DirectBootAwareness.AUTO); - final InputMethodSettings settings = new InputMethodSettings( - methodMap, userId); + final InputMethodSettings settings = queryInputMethodServicesInternal( + mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext, - methodList); + settings.getMethodList()); nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME( nextEnabledImes).getId(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMap.java b/services/core/java/com/android/server/inputmethod/InputMethodMap.java new file mode 100644 index 000000000000..a8e5e2ef4f72 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodMap.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import android.annotation.AnyThread; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.view.inputmethod.InputMethodInfo; + +import java.util.List; + +/** + * A map from IME ID to {@link InputMethodInfo}, which is guaranteed to be immutable thus + * thread-safe. + */ +final class InputMethodMap { + private static final ArrayMap<String, InputMethodInfo> EMPTY_MAP = + new ArrayMap<>(); + + private final ArrayMap<String, InputMethodInfo> mMap; + + static InputMethodMap emptyMap() { + return new InputMethodMap(EMPTY_MAP); + } + + static InputMethodMap of(@NonNull ArrayMap<String, InputMethodInfo> map) { + return new InputMethodMap(map); + } + + private InputMethodMap(@NonNull ArrayMap<String, InputMethodInfo> map) { + mMap = map.isEmpty() ? EMPTY_MAP : new ArrayMap<>(map); + } + + @AnyThread + @Nullable + InputMethodInfo get(@Nullable String imeId) { + return mMap.get(imeId); + } + + @AnyThread + @NonNull + List<InputMethodInfo> values() { + return List.copyOf(mMap.values()); + } + + @AnyThread + @Nullable + InputMethodInfo valueAt(int index) { + return mMap.valueAt(index); + } + + @AnyThread + boolean containsKey(@Nullable String imeId) { + return mMap.containsKey(imeId); + } + + @AnyThread + @IntRange(from = 0) + int size() { + return mMap.size(); + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index 9ddb428018c0..abd7688b60cc 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -16,6 +16,7 @@ package com.android.server.inputmethod; +import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -58,7 +59,9 @@ final class InputMethodSettings { private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR; - private final ArrayMap<String, InputMethodInfo> mMethodMap; + private final InputMethodMap mMethodMap; + private final List<InputMethodInfo> mMethodList; + @UserIdInt private final int mCurrentUserId; @@ -73,8 +76,17 @@ final class InputMethodSettings { } } - InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + static InputMethodSettings createEmptyMap(@UserIdInt int userId) { + return new InputMethodSettings(InputMethodMap.emptyMap(), userId); + } + + static InputMethodSettings create(InputMethodMap methodMap, @UserIdInt int userId) { + return new InputMethodSettings(methodMap, userId); + } + + private InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId) { mMethodMap = methodMap; + mMethodList = methodMap.values(); mCurrentUserId = userId; String ime = getSelectedInputMethod(); String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); @@ -84,6 +96,18 @@ final class InputMethodSettings { } } + @AnyThread + @NonNull + InputMethodMap getMethodMap() { + return mMethodMap; + } + + @AnyThread + @NonNull + List<InputMethodInfo> getMethodList() { + return mMethodList; + } + private void putString(@NonNull String key, @Nullable String str) { SecureSettingsWrapper.putString(key, str, mCurrentUserId); } @@ -101,32 +125,32 @@ final class InputMethodSettings { return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId); } - ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() { - return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */); + ArrayList<InputMethodInfo> getEnabledInputMethodList() { + return getEnabledInputMethodListWithFilter(null /* matchingCondition */); } @NonNull - ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked( + ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilter( @Nullable Predicate<InputMethodInfo> matchingCondition) { - return createEnabledInputMethodListLocked( - getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition); + return createEnabledInputMethodList( + getEnabledInputMethodsAndSubtypeList(), matchingCondition); } - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( + List<InputMethodSubtype> getEnabledInputMethodSubtypeList( InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) { List<InputMethodSubtype> enabledSubtypes = - getEnabledInputMethodSubtypeListLocked(imi); + getEnabledInputMethodSubtypeList(imi); if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) { - enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypes( SystemLocaleWrapper.get(mCurrentUserId), imi); } return InputMethodSubtype.sort(imi, enabledSubtypes); } - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) { - final List<Pair<String, ArrayList<String>>> imsList = - getEnabledInputMethodsAndSubtypeListLocked(); - final List<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); + List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi) { + List<Pair<String, ArrayList<String>>> imsList = + getEnabledInputMethodsAndSubtypeList(); + ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); if (imi != null) { for (int i = 0; i < imsList.size(); ++i) { final Pair<String, ArrayList<String>> imsPair = imsList.get(i); @@ -149,7 +173,7 @@ final class InputMethodSettings { return enabledSubtypes; } - List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { + List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeList() { final String enabledInputMethodsStr = getEnabledInputMethodsStr(); final TextUtils.SimpleStringSplitter inputMethodSplitter = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); @@ -181,7 +205,7 @@ final class InputMethodSettings { * * @return the specified id was removed or not. */ - boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( + boolean buildAndPutEnabledInputMethodsStrRemovingId( StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { boolean isRemoved = false; boolean needsAppendSeparator = false; @@ -209,7 +233,7 @@ final class InputMethodSettings { return isRemoved; } - private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked( + private ArrayList<InputMethodInfo> createEnabledInputMethodList( List<Pair<String, ArrayList<String>>> imsList, Predicate<InputMethodInfo> matchingCondition) { final ArrayList<InputMethodInfo> res = new ArrayList<>(); @@ -271,7 +295,7 @@ final class InputMethodSettings { } private void addSubtypeToHistory(String imeId, String subtypeId) { - final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory(); for (int i = 0; i < subtypeHistory.size(); ++i) { final Pair<String, String> ime = subtypeHistory.get(i); if (ime.first.equals(imeId)) { @@ -303,14 +327,14 @@ final class InputMethodSettings { } } - Pair<String, String> getLastInputMethodAndSubtypeLocked() { + Pair<String, String> getLastInputMethodAndSubtype() { // Gets the first one from the history - return getLastSubtypeForInputMethodLockedInternal(null); + return getLastSubtypeForInputMethodInternal(null); } @Nullable - InputMethodSubtype getLastInputMethodSubtypeLocked() { - final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked(); + InputMethodSubtype getLastInputMethodSubtype() { + final Pair<String, String> lastIme = getLastInputMethodAndSubtype(); // TODO: Handle the case of the last IME with no subtypes if (lastIme == null || TextUtils.isEmpty(lastIme.first) || TextUtils.isEmpty(lastIme.second)) { @@ -331,8 +355,8 @@ final class InputMethodSettings { } } - String getLastSubtypeForInputMethodLocked(String imeId) { - Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); + String getLastSubtypeForInputMethod(String imeId) { + Pair<String, String> ime = getLastSubtypeForInputMethodInternal(imeId); if (ime != null) { return ime.second; } else { @@ -340,10 +364,10 @@ final class InputMethodSettings { } } - private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { + private Pair<String, String> getLastSubtypeForInputMethodInternal(String imeId) { final List<Pair<String, ArrayList<String>>> enabledImes = - getEnabledInputMethodsAndSubtypeListLocked(); - final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + getEnabledInputMethodsAndSubtypeList(); + final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory(); for (int i = 0; i < subtypeHistory.size(); ++i) { final Pair<String, String> imeAndSubtype = subtypeHistory.get(i); final String imeInTheHistory = imeAndSubtype.first; @@ -351,7 +375,7 @@ final class InputMethodSettings { if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { final String subtypeInTheHistory = imeAndSubtype.second; final String subtypeHashCode = - getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( + getEnabledSubtypeHashCodeForInputMethodAndSubtype( enabledImes, imeInTheHistory, subtypeInTheHistory); if (!TextUtils.isEmpty(subtypeHashCode)) { if (DEBUG) { @@ -368,7 +392,7 @@ final class InputMethodSettings { return null; } - private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, + private String getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId); for (int i = 0; i < enabledImes.size(); ++i) { @@ -383,7 +407,7 @@ final class InputMethodSettings { // are enabled implicitly, so needs to treat them to be enabled. if (imi != null && imi.getSubtypeCount() > 0) { List<InputMethodSubtype> implicitlyEnabledSubtypes = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList, + SubtypeUtils.getImplicitlyApplicableSubtypes(localeList, imi); final int numSubtypes = implicitlyEnabledSubtypes.size(); for (int j = 0; j < numSubtypes; ++j) { @@ -420,7 +444,7 @@ final class InputMethodSettings { return null; } - private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { + private List<Pair<String, String>> loadInputMethodAndSubtypeHistory() { ArrayList<Pair<String, String>> imsList = new ArrayList<>(); final String subtypeHistoryStr = getSubtypeHistoryStr(); if (TextUtils.isEmpty(subtypeHistoryStr)) { @@ -583,7 +607,7 @@ final class InputMethodSettings { // If there are no selected subtypes, the framework will try to find the most applicable // subtype from explicitly or implicitly enabled subtypes. final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - getEnabledInputMethodSubtypeListLocked(imi, true); + getEnabledInputMethodSubtypeList(imi, true); // If there is only one explicitly or implicitly enabled subtype, just returns it. if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) { return null; @@ -592,13 +616,13 @@ final class InputMethodSettings { return explicitlyOrImplicitlyEnabledSubtypes.get(0); } final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString(); - final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (subtype != null) { return subtype; } - return SubtypeUtils.findLastResortApplicableSubtypeLocked( + return SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } @@ -690,7 +714,7 @@ final class InputMethodSettings { return sb.toString(); } - void dumpLocked(final Printer pw, final String prefix) { + void dump(final Printer pw, final String prefix) { pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index 834ba20b84fd..1379d166e805 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -23,7 +23,6 @@ import android.annotation.UserIdInt; import android.content.Context; import android.os.UserHandle; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.ArraySet; import android.util.Printer; import android.util.Slog; @@ -158,15 +157,15 @@ final class InputMethodSubtypeSwitchingController { static List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( boolean includeAuxiliarySubtypes, boolean isScreenLocked, boolean forImeMenu, - @NonNull Context context, @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @NonNull Context context, @NonNull InputMethodMap methodMap, @UserIdInt int userId) { final Context userAwareContext = context.getUserId() == userId ? context : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag(); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = InputMethodSettings.create(methodMap, userId); - final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodListLocked(); + final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList(); if (imis.isEmpty()) { return new ArrayList<>(); } @@ -184,7 +183,7 @@ final class InputMethodSubtypeSwitchingController { continue; } final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = - settings.getEnabledInputMethodSubtypeListLocked(imi, true); + settings.getEnabledInputMethodSubtypeList(imi, true); final ArraySet<String> enabledSubtypeSet = new ArraySet<>(); for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); @@ -479,7 +478,7 @@ final class InputMethodSubtypeSwitchingController { private ControllerImpl mController; private InputMethodSubtypeSwitchingController(@NonNull Context context, - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + @NonNull InputMethodMap methodMap, @UserIdInt int userId) { mContext = context; mUserId = userId; mController = ControllerImpl.createFrom(null, @@ -491,7 +490,7 @@ final class InputMethodSubtypeSwitchingController { @NonNull public static InputMethodSubtypeSwitchingController createInstanceLocked( @NonNull Context context, - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + @NonNull InputMethodMap methodMap, @UserIdInt int userId) { return new InputMethodSubtypeSwitchingController(context, methodMap, userId); } @@ -511,8 +510,7 @@ final class InputMethodSubtypeSwitchingController { mController.onUserActionLocked(imi, subtype); } - public void resetCircularListLocked( - @NonNull ArrayMap<String, InputMethodInfo> methodMap) { + public void resetCircularListLocked(@NonNull InputMethodMap methodMap) { mController = ControllerImpl.createFrom(mController, getSortedInputMethodAndSubtypeList( false /* includeAuxiliarySubtypes */, false /* isScreenLocked */, diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java index 95df99855dcf..3d5c867768ac 100644 --- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java @@ -26,7 +26,6 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; @@ -52,7 +51,7 @@ final class SubtypeUtils { "EnabledWhenDefaultIsNotAsciiCapable"; // A temporary workaround for the performance concerns in - // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo). + // #getImplicitlyApplicableSubtypes(Resources, InputMethodInfo). // TODO: Optimize all the critical paths including this one. // TODO(b/235661780): Make the cache supports multi-users. private static final Object sCacheLock = new Object(); @@ -121,9 +120,8 @@ final class SubtypeUtils { private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = source -> source != null ? source.getLocaleObject() : null; - @VisibleForTesting @NonNull - static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( + static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypes( @NonNull LocaleList systemLocales, InputMethodInfo imi) { synchronized (sCacheLock) { // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because @@ -133,11 +131,11 @@ final class SubtypeUtils { } } - // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl(). - // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive + // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesImpl(). + // TODO: Refactor getImplicitlyApplicableSubtypesImpl() so that it can receive // LocaleList rather than Resource. final ArrayList<InputMethodSubtype> result = - getImplicitlyApplicableSubtypesLockedImpl(systemLocales, imi); + getImplicitlyApplicableSubtypesImpl(systemLocales, imi); synchronized (sCacheLock) { // Both LocaleList and InputMethodInfo are immutable. No need to copy them here. sCachedSystemLocales = systemLocales; @@ -147,7 +145,7 @@ final class SubtypeUtils { return result; } - private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl( + private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesImpl( @NonNull LocaleList systemLocales, InputMethodInfo imi) { final List<InputMethodSubtype> subtypes = getSubtypes(imi); final String systemLocale = systemLocales.get(0).toString(); @@ -215,7 +213,7 @@ final class SubtypeUtils { } if (applicableSubtypes.isEmpty()) { - InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( + InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtype( subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); if (lastResortKeyboardSubtype != null) { applicableSubtypes.add(lastResortKeyboardSubtype); @@ -244,7 +242,7 @@ final class SubtypeUtils { * * @return the most applicable subtypeId */ - static InputMethodSubtype findLastResortApplicableSubtypeLocked( + static InputMethodSubtype findLastResortApplicableSubtype( List<InputMethodSubtype> subtypes, String mode, @NonNull String locale, boolean canIgnoreLocaleAsLastResort) { if (subtypes == null || subtypes.isEmpty()) { diff --git a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java index 9c4225dc2542..39df5be0f2fa 100644 --- a/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java +++ b/services/core/java/com/android/server/location/gnss/GnssLocationProvider.java @@ -1384,7 +1384,8 @@ public class GnssLocationProvider extends AbstractLocationProvider implements try { reportLocation(LocationResult.wrap(location).validate()); } catch (BadLocationException e) { - throw new IllegalArgumentException(e); + Log.e(TAG, "Dropping invalid location: " + e); + return; } if (mStarted) { @@ -1759,7 +1760,8 @@ public class GnssLocationProvider extends AbstractLocationProvider implements try { reportLocation(LocationResult.wrap(locations).validate()); } catch (BadLocationException e) { - throw new IllegalArgumentException(e); + Log.e(TAG, "Dropping invalid locations: " + e); + return; } } diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 28a1c7ad0540..85a131579497 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -2336,6 +2336,11 @@ class MediaRouter2ServiceImpl { mSystemProvider.getDefaultRoute()); } + private static String getPackageNameFromNullableRecord( + @Nullable RouterRecord routerRecord) { + return routerRecord != null ? routerRecord.mPackageName : "<null router record>"; + } + private static String toLoggingMessage( String source, String providerId, ArrayList<MediaRoute2Info> routes) { String routesString = @@ -2573,8 +2578,17 @@ class MediaRouter2ServiceImpl { RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId); if (matchingRecord != routerRecord) { - Slog.w(TAG, "Ignoring " + description + " route from non-matching router. " - + "packageName=" + routerRecord.mPackageName + " route=" + route); + Slog.w( + TAG, + "Ignoring " + + description + + " route from non-matching router." + + " routerRecordPackageName=" + + getPackageNameFromNullableRecord(routerRecord) + + " matchingRecordPackageName=" + + getPackageNameFromNullableRecord(matchingRecord) + + " route=" + + route); return false; } @@ -2613,9 +2627,15 @@ class MediaRouter2ServiceImpl { @Nullable RouterRecord routerRecord, @NonNull String uniqueSessionId) { final RouterRecord matchingRecord = mSessionToRouterMap.get(uniqueSessionId); if (matchingRecord != routerRecord) { - Slog.w(TAG, "Ignoring releasing session from non-matching router. packageName=" - + (routerRecord == null ? null : routerRecord.mPackageName) - + " uniqueSessionId=" + uniqueSessionId); + Slog.w( + TAG, + "Ignoring releasing session from non-matching router." + + " routerRecordPackageName=" + + getPackageNameFromNullableRecord(routerRecord) + + " matchingRecordPackageName=" + + getPackageNameFromNullableRecord(matchingRecord) + + " uniqueSessionId=" + + uniqueSessionId); return; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 308d441fb871..425659e38492 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -282,6 +282,7 @@ import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; import android.service.notification.ZenModeConfig; import android.service.notification.ZenModeProto; +import android.service.notification.ZenPolicy; import android.telecom.TelecomManager; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; @@ -5968,6 +5969,20 @@ public class NotificationManagerService extends SystemService { } } + /** + * Gets the device-default zen policy as a ZenPolicy. + */ + @Override + public ZenPolicy getDefaultZenPolicy() { + enforceSystemOrSystemUI("INotificationManager.getDefaultZenPolicy"); + final long identity = Binder.clearCallingIdentity(); + try { + return mZenModeHelper.getDefaultZenPolicy(); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public List<String> getEnabledNotificationListenerPackages() { checkCallerIsSystem(); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 153af13b61b4..c067fa068b12 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -604,6 +604,14 @@ public class ZenModeHelper { ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); if (rule == null) { rule = newImplicitZenRule(callingPkg); + + // For new implicit rules, create a policy matching the current global + // (manual rule) settings, for consistency with the policy that + // would apply if changing the global interruption filter. We only do this + // for newly created rules, as existing rules have a pre-existing policy + // (whether initialized here or set via app or user). + rule.zenPolicy = mConfig.toZenPolicy(); + newConfig.automaticRules.put(rule.id, rule); } // If the user has changed the rule's *zenMode*, then don't let app overwrite it. @@ -615,6 +623,7 @@ public class ZenModeHelper { rule.condition = new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_activated), Condition.STATE_TRUE); + setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, "applyGlobalZenModeAsImplicitZenRule", callingUid); } @@ -643,8 +652,10 @@ public class ZenModeHelper { return; } ZenModeConfig newConfig = mConfig.copy(); + boolean isNew = false; ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); if (rule == null) { + isNew = true; rule = newImplicitZenRule(callingPkg); rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; newConfig.automaticRules.put(rule.id, rule); @@ -652,10 +663,20 @@ public class ZenModeHelper { // If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it. // We allow the update if the user has only changed other aspects of the rule. if (rule.zenPolicyUserModifiedFields == 0) { + ZenPolicy newZenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy); + if (isNew) { + // For new rules only, fill anything underspecified in the new policy with + // values from the global configuration, for consistency with the policy that + // would take effect if changing the global policy. + // Note that NotificationManager.Policy cannot have any unset priority + // categories, but *can* have unset visual effects, which is why we do this. + newZenPolicy = mConfig.toZenPolicy().overwrittenWith(newZenPolicy); + } updatePolicy( rule, - ZenAdapters.notificationPolicyToZenPolicy(policy), - /* updateBitmask= */ false); + newZenPolicy, + /* updateBitmask= */ false, + isNew); setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, "applyGlobalPolicyAsImplicitZenRule", callingUid); @@ -685,6 +706,11 @@ public class ZenModeHelper { } ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg)); if (implicitRule != null && implicitRule.zenPolicy != null) { + // toNotificationPolicy takes defaults from mConfig, and technically, those are not + // the defaults that would apply if any fields were unset. However, all rules should + // have all fields set in their ZenPolicy objects upon rule creation, so in + // practice, this is only filling in the areChannelsBypassingDnd field, which is a + // state rather than a part of the policy. return mConfig.toNotificationPolicy(implicitRule.zenPolicy); } else { return getNotificationPolicy(); @@ -1072,7 +1098,8 @@ public class ZenModeHelper { rule.zenMode = newZenMode; // Updates the bitmask and values for all policy fields, based on the origin. - updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask); + updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask, isNew); + // Updates the bitmask and values for all device effect fields, based on the origin. updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(), origin == UPDATE_ORIGIN_APP, updateBitmask); @@ -1111,14 +1138,19 @@ public class ZenModeHelper { /** * Modifies the {@link ZenPolicy} associated to a new or updated ZenRule. * - * <p>The new policy is {@code newPolicy}, while the user-modified bitmask is updated to reflect - * the changes being applied (if applicable, i.e. if the update is from the user). + * <p>The update takes any set fields in {@code newPolicy} as new policy settings for the + * provided {@code ZenRule}, keeping any pre-existing settings from {@code zenRule.zenPolicy} + * for any unset policy fields in {@code newPolicy}. The user-modified bitmask is updated to + * reflect the changes being applied (if applicable, i.e. if the update is from the user). */ private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy, - boolean updateBitmask) { + boolean updateBitmask, boolean isNew) { if (newPolicy == null) { - // TODO: b/319242206 - Treat as newPolicy == default policy and continue below. - zenRule.zenPolicy = null; + if (isNew) { + // Newly created rule with no provided policy; fill in with the default. + zenRule.zenPolicy = mDefaultConfig.toZenPolicy(); + } + // Otherwise, a null policy means no policy changes, so we can stop here. return; } @@ -1127,6 +1159,16 @@ public class ZenModeHelper { ZenPolicy oldPolicy = zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy(); + // If this is updating a rule rather than creating a new one, keep any fields from the + // old policy if they are unspecified in the new policy. For newly created rules, oldPolicy + // has been set to the default settings above, so any unspecified fields in a newly created + // policy are filled with default values. Then use the fully-specified version of the new + // policy for comparison below. + // + // Although we do not expect a policy update from the user to contain any unset fields, + // filling in fields here also guards against any unset fields counting as a "diff" when + // comparing fields for bitmask editing below. + newPolicy = oldPolicy.overwrittenWith(newPolicy); zenRule.zenPolicy = newPolicy; if (updateBitmask) { @@ -1452,11 +1494,27 @@ public class ZenModeHelper { } allRulesDisabled &= !automaticRule.enabled; + + // Upon upgrading to a version with modes_api enabled, keep all behaviors of + // rules with null ZenPolicies explicitly as a copy of the global policy. + if (Flags.modesApi() && config.version < ZenModeConfig.XML_VERSION_MODES_API) { + // Keep the manual ("global") policy that from config. + ZenPolicy manualRulePolicy = config.toZenPolicy(); + if (automaticRule.zenPolicy == null) { + automaticRule.zenPolicy = manualRulePolicy; + } else { + // newPolicy is a policy with all unset fields in the rule's zenPolicy + // set to their values from the values in config. Then convert that back + // to ZenPolicy to store with the automatic zen rule. + automaticRule.zenPolicy = + manualRulePolicy.overwrittenWith(automaticRule.zenPolicy); + } + } } } if (!hasDefaultRules && allRulesDisabled - && (forRestore || config.version < ZenModeConfig.XML_VERSION)) { + && (forRestore || config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE)) { // reset zen automatic rules to default on restore or upgrade if: // - doesn't already have default rules and // - all previous automatic rules were disabled @@ -1473,7 +1531,7 @@ public class ZenModeHelper { // Resolve user id for settings. userId = userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId; - if (config.version < ZenModeConfig.XML_VERSION) { + if (config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE) { Settings.Secure.putIntForUser(mContext.getContentResolver(), Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1, userId); } else { @@ -1600,6 +1658,14 @@ public class ZenModeHelper { return mConsolidatedPolicy.copy(); } + /** + * Returns a copy of the device default policy as a ZenPolicy object. + */ + @VisibleForTesting + protected ZenPolicy getDefaultZenPolicy() { + return mDefaultConfig.toZenPolicy(); + } + @GuardedBy("mConfigLock") private boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent, @ConfigChangeOrigin int origin, String reason, int callingUid) { @@ -1783,7 +1849,7 @@ public class ZenModeHelper { } @GuardedBy("mConfigLock") - private void applyCustomPolicy(ZenPolicy policy, ZenRule rule) { + private void applyCustomPolicy(ZenPolicy policy, ZenRule rule, boolean useManualConfig) { if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) { policy.apply(new ZenPolicy.Builder() .disallowAllSounds() @@ -1797,8 +1863,22 @@ public class ZenModeHelper { } else if (rule.zenPolicy != null) { policy.apply(rule.zenPolicy); } else { - // active rule with no specified policy inherits the default settings - policy.apply(mConfig.toZenPolicy()); + if (Flags.modesApi()) { + if (useManualConfig) { + // manual rule is configured using the settings stored directly in mConfig + policy.apply(mConfig.toZenPolicy()); + } else { + // under modes_api flag, an active automatic rule with no specified policy + // inherits the device default settings as stored in mDefaultConfig. While the + // rule's policy fields should be set upon creation, this is a fallback to + // catch any that may have fallen through the cracks. + Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule); + policy.apply(mDefaultConfig.toZenPolicy()); + } + } else { + // active rule with no specified policy inherits the global config settings + policy.apply(mConfig.toZenPolicy()); + } } } @@ -1810,7 +1890,7 @@ public class ZenModeHelper { ZenPolicy policy = new ZenPolicy(); ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder(); if (mConfig.manualRule != null) { - applyCustomPolicy(policy, mConfig.manualRule); + applyCustomPolicy(policy, mConfig.manualRule, true); if (Flags.modesApi()) { deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects); } @@ -1822,7 +1902,7 @@ public class ZenModeHelper { // policy. This is relevant in case some other active rule has a more // restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy! if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) { - applyCustomPolicy(policy, automaticRule); + applyCustomPolicy(policy, automaticRule, false); } if (Flags.modesApi()) { deviceEffectsBuilder.add(automaticRule.zenDeviceEffects); @@ -1830,6 +1910,14 @@ public class ZenModeHelper { } } + // While mConfig.toNotificationPolicy fills in any unset fields from the provided + // config (which, in this case is the manual "global" config), under modes API changes, + // we should have no remaining unset fields: the manual policy gets every field from + // the global policy, and each automatic rule has all policy fields filled in on + // creation or update. + // However, the piece of information that comes from mConfig that we must keep is the + // areChannelsBypassingDnd bit, which is a state, rather than a policy, and even when + // all policy fields are set, this state comes to the new policy from this call. Policy newPolicy = mConfig.toNotificationPolicy(policy); if (!Objects.equals(mConsolidatedPolicy, newPolicy)) { mConsolidatedPolicy = newPolicy; diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java index 25a39cc8456f..86d05d92c95b 100644 --- a/services/core/java/com/android/server/om/IdmapManager.java +++ b/services/core/java/com/android/server/om/IdmapManager.java @@ -257,7 +257,7 @@ final class IdmapManager { private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage, @NonNull AndroidPackage overlayPackage, int userId) { String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName(); - if (targetOverlayableName != null) { + if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) { try { OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget( targetPackage.getPackageName(), targetOverlayableName, userId); diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index b9464d96a019..a61b03fdbb39 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -32,6 +32,7 @@ import static android.os.Process.INVALID_UID; import static android.os.Trace.TRACE_TAG_RRO; import static android.os.Trace.traceBegin; import static android.os.Trace.traceEnd; + import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException; import android.annotation.NonNull; @@ -362,7 +363,7 @@ public final class OverlayManagerService extends SystemService { defaultPackages.add(packageName); } } - return defaultPackages.toArray(new String[defaultPackages.size()]); + return defaultPackages.toArray(new String[0]); } private final class OverlayManagerPackageMonitor extends PackageMonitor { @@ -1143,9 +1144,10 @@ public final class OverlayManagerService extends SystemService { }; private static final class PackageManagerHelperImpl implements PackageManagerHelper { - private static class PackageStateUsers { + private static final class PackageStateUsers { private PackageState mPackageState; - private final Set<Integer> mInstalledUsers = new ArraySet<>(); + private Boolean mDefinesOverlayable = null; + private final ArraySet<Integer> mInstalledUsers = new ArraySet<>(); private PackageStateUsers(@NonNull PackageState packageState) { this.mPackageState = packageState; } @@ -1160,7 +1162,7 @@ public final class OverlayManagerService extends SystemService { // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>(); - private final Set<Integer> mInitializedUsers = new ArraySet<>(); + private final ArraySet<Integer> mInitializedUsers = new ArraySet<>(); PackageManagerHelperImpl(Context context) { mContext = context; @@ -1176,8 +1178,7 @@ public final class OverlayManagerService extends SystemService { */ @NonNull public ArrayMap<String, PackageState> initializeForUser(final int userId) { - if (!mInitializedUsers.contains(userId)) { - mInitializedUsers.add(userId); + if (mInitializedUsers.add(userId)) { mPackageManagerInternal.forEachPackageState((packageState -> { if (packageState.getPkg() != null && packageState.getUserStateOrDefault(userId).isInstalled()) { @@ -1196,13 +1197,11 @@ public final class OverlayManagerService extends SystemService { return userPackages; } - @Override - @Nullable - public PackageState getPackageStateForUser(@NonNull final String packageName, + private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName, final int userId) { final PackageStateUsers pkg = mCache.get(packageName); if (pkg != null && pkg.mInstalledUsers.contains(userId)) { - return pkg.mPackageState; + return pkg; } try { if (!mPackageManager.isPackageAvailable(packageName, userId)) { @@ -1216,8 +1215,14 @@ public final class OverlayManagerService extends SystemService { return addPackageUser(packageName, userId); } - @NonNull - private PackageState addPackageUser(@NonNull final String packageName, + @Override + public PackageState getPackageStateForUser(@NonNull final String packageName, + final int userId) { + final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId); + return pkg != null ? pkg.mPackageState : null; + } + + private PackageStateUsers addPackageUser(@NonNull final String packageName, final int user) { final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName); if (pkg == null) { @@ -1229,20 +1234,20 @@ public final class OverlayManagerService extends SystemService { } @NonNull - private PackageState addPackageUser(@NonNull final PackageState pkg, + private PackageStateUsers addPackageUser(@NonNull final PackageState pkg, final int user) { PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName()); if (pkgUsers == null) { pkgUsers = new PackageStateUsers(pkg); mCache.put(pkg.getPackageName(), pkgUsers); - } else { + } else if (pkgUsers.mPackageState != pkg) { pkgUsers.mPackageState = pkg; + pkgUsers.mDefinesOverlayable = null; } pkgUsers.mInstalledUsers.add(user); - return pkgUsers.mPackageState; + return pkgUsers; } - @NonNull private void removePackageUser(@NonNull final String packageName, final int user) { final PackageStateUsers pkgUsers = mCache.get(packageName); @@ -1260,15 +1265,15 @@ public final class OverlayManagerService extends SystemService { } } - @Nullable public PackageState onPackageAdded(@NonNull final String packageName, final int userId) { - return addPackageUser(packageName, userId); + final var pu = addPackageUser(packageName, userId); + return pu != null ? pu.mPackageState : null; } - @Nullable public PackageState onPackageUpdated(@NonNull final String packageName, final int userId) { - return addPackageUser(packageName, userId); + final var pu = addPackageUser(packageName, userId); + return pu != null ? pu.mPackageState : null; } public void onPackageRemoved(@NonNull final String packageName, final int userId) { @@ -1308,22 +1313,30 @@ public final class OverlayManagerService extends SystemService { return (pkgs.length == 0) ? null : pkgs[0]; } - @Nullable @Override public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, @NonNull String targetOverlayableName, int userId) throws IOException { - var packageState = getPackageStateForUser(packageName, userId); - var pkg = packageState == null ? null : packageState.getAndroidPackage(); + final var psu = getRawPackageStateForUser(packageName, userId); + final var pkg = (psu == null || psu.mPackageState == null) + ? null : psu.mPackageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } + if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) { + return null; + } + ApkAssets apkAssets = null; try { apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), ApkAssets.PROPERTY_ONLY_OVERLAYABLES); - return apkAssets.getOverlayableInfo(targetOverlayableName); + if (psu.mDefinesOverlayable == null) { + psu.mDefinesOverlayable = apkAssets.definesOverlayable(); + } + return Boolean.FALSE.equals(psu.mDefinesOverlayable) + ? null : apkAssets.getOverlayableInfo(targetOverlayableName); } finally { if (apkAssets != null) { try { @@ -1337,24 +1350,29 @@ public final class OverlayManagerService extends SystemService { @Override public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) throws IOException { - var packageState = getPackageStateForUser(targetPackageName, userId); - var pkg = packageState == null ? null : packageState.getAndroidPackage(); + final var psu = getRawPackageStateForUser(targetPackageName, userId); + var pkg = (psu == null || psu.mPackageState == null) + ? null : psu.mPackageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } - ApkAssets apkAssets = null; - try { - apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath()); - return apkAssets.definesOverlayable(); - } finally { - if (apkAssets != null) { - try { - apkAssets.close(); - } catch (Throwable ignored) { + if (psu.mDefinesOverlayable == null) { + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), + ApkAssets.PROPERTY_ONLY_OVERLAYABLES); + psu.mDefinesOverlayable = apkAssets.definesOverlayable(); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { + } } } } + return psu.mDefinesOverlayable; } @Override @@ -1545,8 +1563,7 @@ public final class OverlayManagerService extends SystemService { final OverlayPaths frameworkOverlays = mImpl.getEnabledOverlayPaths("android", userId, false); for (final String targetPackageName : targetPackageNames) { - final OverlayPaths.Builder list = new OverlayPaths.Builder(); - list.addAll(frameworkOverlays); + final var list = new OverlayPaths.Builder(frameworkOverlays); if (!"android".equals(targetPackageName)) { list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true)); } @@ -1558,17 +1575,21 @@ public final class OverlayManagerService extends SystemService { final HashSet<String> invalidPackages = new HashSet<>(); pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages); - for (final String targetPackageName : targetPackageNames) { - if (DEBUG) { - Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=[" - + pendingChanges.get(targetPackageName) - + "] userId=" + userId); - } + if (DEBUG || !invalidPackages.isEmpty()) { + for (final String targetPackageName : targetPackageNames) { + if (DEBUG) { + Slog.d(TAG, + "-> Updating overlay: target=" + targetPackageName + " overlays=[" + + pendingChanges.get(targetPackageName) + + "] userId=" + userId); + } - if (invalidPackages.contains(targetPackageName)) { - Slog.e(TAG, TextUtils.formatSimple( - "Failed to change enabled overlays for %s user %d", targetPackageName, - userId)); + if (invalidPackages.contains(targetPackageName)) { + Slog.e(TAG, TextUtils.formatSimple( + "Failed to change enabled overlays for %s user %d", + targetPackageName, + userId)); + } } } return new ArrayList<>(updatedPackages); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index 972c78db9460..c1b6ccc7e25c 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -772,24 +772,20 @@ final class OverlayManagerServiceImpl { OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName, final int userId, boolean includeImmutableOverlays) { - final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName, - userId); - final OverlayPaths.Builder paths = new OverlayPaths.Builder(); - final int n = overlays.size(); - for (int i = 0; i < n; i++) { - final OverlayInfo oi = overlays.get(i); + final var paths = new OverlayPaths.Builder(); + mSettings.forEachMatching(userId, null, targetPackageName, oi -> { if (!oi.isEnabled()) { - continue; + return; } if (!includeImmutableOverlays && !oi.isMutable) { - continue; + return; } if (oi.isFabricated()) { paths.addNonApkPath(oi.baseCodePath); } else { paths.addApkPath(oi.baseCodePath); } - } + }); return paths.build(); } diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index eae614ac9e77..b8b49f3eed2f 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -47,6 +47,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @@ -182,6 +183,23 @@ final class OverlayManagerSettings { return CollectionUtils.map(items, SettingsItem::getOverlayInfo); } + void forEachMatching(int userId, String overlayName, String targetPackageName, + @NonNull Consumer<OverlayInfo> consumer) { + for (int i = 0, n = mItems.size(); i < n; i++) { + final SettingsItem item = mItems.get(i); + if (item.getUserId() != userId) { + continue; + } + if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) { + continue; + } + if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) { + continue; + } + consumer.accept(item.getOverlayInfo()); + } + } + ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) { final List<SettingsItem> items = selectWhereUser(userId); diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index f3df4244c47f..cc4c2b5bf893 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -1031,12 +1031,12 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, private void recomputeComponentVisibility( ArrayMap<String, ? extends PackageStateInternal> existingSettings) { final WatchedArraySet<String> protectedBroadcasts; - final WatchedArraySet<Integer> forceQueryable; + final ArraySet<Integer> forceQueryable; synchronized (mProtectedBroadcastsLock) { - protectedBroadcasts = mProtectedBroadcasts.snapshot(); + protectedBroadcasts = new WatchedArraySet<String>(mProtectedBroadcasts); } synchronized (mForceQueryableLock) { - forceQueryable = mForceQueryable.snapshot(); + forceQueryable = new ArraySet<Integer>(mForceQueryable.untrackedStorage()); } final ParallelComputeComponentVisibility computer = new ParallelComputeComponentVisibility( existingSettings, forceQueryable, protectedBroadcasts); diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java index 200734b37269..a02a1bc13b17 100644 --- a/services/core/java/com/android/server/pm/AppsFilterUtils.java +++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java @@ -198,12 +198,12 @@ final class AppsFilterUtils { private static final int MAX_THREADS = 4; private final ArrayMap<String, ? extends PackageStateInternal> mExistingSettings; - private final WatchedArraySet<Integer> mForceQueryable; + private final ArraySet<Integer> mForceQueryable; private final WatchedArraySet<String> mProtectedBroadcasts; ParallelComputeComponentVisibility( @NonNull ArrayMap<String, ? extends PackageStateInternal> existingSettings, - @NonNull WatchedArraySet<Integer> forceQueryable, + @NonNull ArraySet<Integer> forceQueryable, @NonNull WatchedArraySet<String> protectedBroadcasts) { mExistingSettings = existingSettings; mForceQueryable = forceQueryable; diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index c110fb67b54f..200b17bc2f97 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -16,7 +16,12 @@ package com.android.server.pm; +import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.app.Flags; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; import android.content.Context; @@ -27,6 +32,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; +import android.os.Binder; import android.os.Build; import android.os.Environment; import android.os.Handler; @@ -69,26 +75,29 @@ public class BackgroundInstallControlService extends SystemService { private static final String DISK_FILE_NAME = "states"; private static final String DISK_DIR_NAME = "bic"; - private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10; + private static final String ENFORCE_PERMISSION_ERROR_MSG = + "User is not permitted to call service: "; + private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10; private static final int MSG_USAGE_EVENT_RECEIVED = 0; private static final int MSG_PACKAGE_ADDED = 1; private static final int MSG_PACKAGE_REMOVED = 2; private final BinderService mBinderService; private final PackageManager mPackageManager; + // TODO migrate all internal PackageManager calls to PackageManagerInternal where possible. + // b/310983905 private final PackageManagerInternal mPackageManagerInternal; private final PermissionManagerServiceInternal mPermissionManager; private final Handler mHandler; private final File mDiskFile; - + private final Context mContext; private SparseSetArray<String> mBackgroundInstalledPackages = null; // User ID -> package name -> set of foreground time frame - private final SparseArrayMap<String, - TreeSet<ForegroundTimeFrame>> mInstallerForegroundTimeFrames = - new SparseArrayMap<>(); + private final SparseArrayMap<String, TreeSet<ForegroundTimeFrame>> + mInstallerForegroundTimeFrames = new SparseArrayMap<>(); public BackgroundInstallControlService(@NonNull Context context) { this(new InjectorImpl(context)); @@ -102,15 +111,13 @@ public class BackgroundInstallControlService extends SystemService { mPermissionManager = injector.getPermissionManager(); mHandler = new EventHandler(injector.getLooper(), this); mDiskFile = injector.getDiskFile(); + mContext = injector.getContext(); UsageStatsManagerInternal usageStatsManagerInternal = injector.getUsageStatsManagerInternal(); usageStatsManagerInternal.registerListener( (userId, event) -> - mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, - userId, - 0, - event).sendToTarget() - ); + mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, userId, 0, event) + .sendToTarget()); mBinderService = new BinderService(this); } @@ -124,12 +131,17 @@ public class BackgroundInstallControlService extends SystemService { @Override public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages( @PackageManager.PackageInfoFlagsBits long flags, int userId) { + if (Flags.bicClient()) { + mService.enforceCallerPermissions(); + } if (!Build.IS_DEBUGGABLE) { return mService.getBackgroundInstalledPackages(flags, userId); } // The debug.transparency.bg-install-apps (only works for debuggable builds) // is used to set mock list of background installed apps for testing. // The list of apps' names is delimited by ",". + // TODO: Remove after migrating test to new background install method using + // {@link BackgroundInstallControlCallbackHelperTest}.installPackage b/310983905 String propertyString = SystemProperties.get("debug.transparency.bg-install-apps"); if (TextUtils.isEmpty(propertyString)) { return mService.getBackgroundInstalledPackages(flags, userId); @@ -137,25 +149,36 @@ public class BackgroundInstallControlService extends SystemService { return mService.getMockBackgroundInstalledPackages(propertyString); } } + + } + + @RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES) + void enforceCallerPermissions() throws SecurityException { + mContext.enforceCallingOrSelfPermission(GET_BACKGROUND_INSTALLED_PACKAGES, + ENFORCE_PERMISSION_ERROR_MSG + GET_BACKGROUND_INSTALLED_PACKAGES); } @VisibleForTesting ParceledListSlice<PackageInfo> getBackgroundInstalledPackages( @PackageManager.PackageInfoFlagsBits long flags, int userId) { - List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser( + final long token = Binder.clearCallingIdentity(); + try { + List<PackageInfo> packages = mPackageManager.getInstalledPackagesAsUser( PackageManager.PackageInfoFlags.of(flags), userId); - initBackgroundInstalledPackages(); - - ListIterator<PackageInfo> iter = packages.listIterator(); - while (iter.hasNext()) { - String packageName = iter.next().packageName; - if (!mBackgroundInstalledPackages.contains(userId, packageName)) { - iter.remove(); + initBackgroundInstalledPackages(); + ListIterator<PackageInfo> iter = packages.listIterator(); + while (iter.hasNext()) { + String packageName = iter.next().packageName; + if (!mBackgroundInstalledPackages.contains(userId, packageName)) { + iter.remove(); + } } - } - return new ParceledListSlice<>(packages); + return new ParceledListSlice<>(packages); + } finally { + Binder.restoreCallingIdentity(token); + } } /** @@ -168,8 +191,9 @@ public class BackgroundInstallControlService extends SystemService { List<PackageInfo> mockPackages = new ArrayList<>(); for (String name : mockPackageNames) { try { - PackageInfo packageInfo = mPackageManager.getPackageInfo(name, - PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL)); + PackageInfo packageInfo = + mPackageManager.getPackageInfo( + name, PackageManager.PackageInfoFlags.of(PackageManager.MATCH_ALL)); mockPackages.add(packageInfo); } catch (PackageManager.NameNotFoundException e) { Slog.w(TAG, "Package's PackageInfo not found " + name); @@ -190,18 +214,16 @@ public class BackgroundInstallControlService extends SystemService { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_USAGE_EVENT_RECEIVED: { - mService.handleUsageEvent((UsageEvents.Event) msg.obj, msg.arg1 /* userId */); + case MSG_USAGE_EVENT_RECEIVED: + mService.handleUsageEvent( + (UsageEvents.Event) msg.obj, msg.arg1 /* userId */); break; - } - case MSG_PACKAGE_ADDED: { + case MSG_PACKAGE_ADDED: mService.handlePackageAdd((String) msg.obj, msg.arg1 /* userId */); break; - } - case MSG_PACKAGE_REMOVED: { + case MSG_PACKAGE_REMOVED: mService.handlePackageRemove((String) msg.obj, msg.arg1 /* userId */); break; - } default: Slog.w(TAG, "Unknown message: " + msg.what); } @@ -211,8 +233,9 @@ public class BackgroundInstallControlService extends SystemService { void handlePackageAdd(String packageName, int userId) { ApplicationInfo appInfo = null; try { - appInfo = mPackageManager.getApplicationInfoAsUser(packageName, - PackageManager.ApplicationInfoFlags.of(0), userId); + appInfo = + mPackageManager.getApplicationInfoAsUser( + packageName, PackageManager.ApplicationInfoFlags.of(0), userId); } catch (PackageManager.NameNotFoundException e) { Slog.w(TAG, "Package's appInfo not found " + packageName); return; @@ -231,15 +254,18 @@ public class BackgroundInstallControlService extends SystemService { // the installers without INSTALL_PACKAGES perm can't perform // the installation in background. So we can just filter out them. - if (mPermissionManager.checkPermission(installerPackageName, - android.Manifest.permission.INSTALL_PACKAGES, Context.DEVICE_ID_DEFAULT, - userId) != PackageManager.PERMISSION_GRANTED) { + if (mPermissionManager.checkPermission( + installerPackageName, + android.Manifest.permission.INSTALL_PACKAGES, + Context.DEVICE_ID_DEFAULT, + userId) + != PERMISSION_GRANTED) { return; } // convert up-time to current time. - final long installTimestamp = System.currentTimeMillis() - - (SystemClock.uptimeMillis() - appInfo.createTimestamp); + final long installTimestamp = + System.currentTimeMillis() - (SystemClock.uptimeMillis() - appInfo.createTimestamp); if (installedByAdb(initiatingPackageName) || wasForegroundInstallation(installerPackageName, userId, installTimestamp)) { @@ -257,8 +283,8 @@ public class BackgroundInstallControlService extends SystemService { return PackageManagerServiceUtils.isInstalledByAdb(initiatingPackageName); } - private boolean wasForegroundInstallation(String installerPackageName, - int userId, long installTimestamp) { + private boolean wasForegroundInstallation( + String installerPackageName, int userId, long installTimestamp) { TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames = mInstallerForegroundTimeFrames.get(userId, installerPackageName); @@ -347,12 +373,12 @@ public class BackgroundInstallControlService extends SystemService { for (int i = 0; i < mBackgroundInstalledPackages.size(); i++) { int userId = mBackgroundInstalledPackages.keyAt(i); for (String packageName : mBackgroundInstalledPackages.get(userId)) { - long token = protoOutputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + long token = + protoOutputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); protoOutputStream.write( BackgroundInstalledPackageProto.PACKAGE_NAME, packageName); - protoOutputStream.write( - BackgroundInstalledPackageProto.USER_ID, userId + 1); + protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, userId + 1); protoOutputStream.end(token); } } @@ -385,23 +411,28 @@ public class BackgroundInstallControlService extends SystemService { != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { continue; } - long token = protoInputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + long token = + protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); String packageName = null; int userId = UserHandle.USER_NULL; while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (protoInputStream.getFieldNumber()) { case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: - packageName = protoInputStream.readString( - BackgroundInstalledPackageProto.PACKAGE_NAME); + packageName = + protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); break; case (int) BackgroundInstalledPackageProto.USER_ID: - userId = protoInputStream.readInt( - BackgroundInstalledPackageProto.USER_ID) - 1; + userId = + protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) + - 1; break; default: - Slog.w(TAG, "Undefined field in proto: " - + protoInputStream.getFieldNumber()); + Slog.w( + TAG, + "Undefined field in proto: " + + protoInputStream.getFieldNumber()); } } protoInputStream.end(token); @@ -430,9 +461,12 @@ public class BackgroundInstallControlService extends SystemService { if (mInstallerForegroundTimeFrames.contains(userId, pkgName)) { return true; } - return mPermissionManager.checkPermission(pkgName, - android.Manifest.permission.INSTALL_PACKAGES, Context.DEVICE_ID_DEFAULT, - userId) == PackageManager.PERMISSION_GRANTED; + return mPermissionManager.checkPermission( + pkgName, + android.Manifest.permission.INSTALL_PACKAGES, + Context.DEVICE_ID_DEFAULT, + userId) + == PERMISSION_GRANTED; } @Override @@ -446,21 +480,22 @@ public class BackgroundInstallControlService extends SystemService { publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService); } - mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() { - @Override - public void onPackageAdded(String packageName, int uid) { - final int userId = UserHandle.getUserId(uid); - mHandler.obtainMessage(MSG_PACKAGE_ADDED, - userId, 0, packageName).sendToTarget(); - } + mPackageManagerInternal.getPackageList( + new PackageManagerInternal.PackageListObserver() { + @Override + public void onPackageAdded(String packageName, int uid) { + final int userId = UserHandle.getUserId(uid); + mHandler.obtainMessage(MSG_PACKAGE_ADDED, userId, 0, packageName) + .sendToTarget(); + } - @Override - public void onPackageRemoved(String packageName, int uid) { - final int userId = UserHandle.getUserId(uid); - mHandler.obtainMessage(MSG_PACKAGE_REMOVED, - userId, 0, packageName).sendToTarget(); - } - }); + @Override + public void onPackageRemoved(String packageName, int uid) { + final int userId = UserHandle.getUserId(uid); + mHandler.obtainMessage(MSG_PACKAGE_REMOVED, userId, 0, packageName) + .sendToTarget(); + } + }); } // The foreground time frame (ForegroundTimeFrame) represents the period @@ -516,7 +551,7 @@ public class BackgroundInstallControlService extends SystemService { } /** - * Dependency injector for {@link #BackgroundInstallControlService)}. + * Dependency injector for {@link BackgroundInstallControlService}. */ interface Injector { Context getContext(); @@ -532,6 +567,7 @@ public class BackgroundInstallControlService extends SystemService { Looper getLooper(); File getDiskFile(); + } private static final class InjectorImpl implements Injector { @@ -568,11 +604,11 @@ public class BackgroundInstallControlService extends SystemService { @Override public Looper getLooper() { - ServiceThread serviceThread = new ServiceThread(TAG, - android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); + ServiceThread serviceThread = + new ServiceThread( + TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); serviceThread.start(); return serviceThread.getLooper(); - } @Override diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4f9eab91f7a8..f311034a4dd2 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2900,6 +2900,12 @@ final class InstallPackageHelper { // code is loaded by a new Activity before ApplicationInfo changes have // propagated to all application threads. mPm.scheduleDeferredNoKillPostDelete(args); + if (Flags.improveInstallDontKill()) { + synchronized (mPm.mInstallLock) { + PackageManagerServiceUtils.linkSplitsToOldDirs(mPm.mInstaller, + packageName, pkgSetting.getPath(), pkgSetting.getOldPaths()); + } + } } else { mRemovePackageHelper.cleanUpResources(packageName, args.getCodeFile(), args.getInstructionSets()); diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index ac826afc1d22..b5346a351f38 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -1680,9 +1680,8 @@ public class LauncherAppsService extends SystemService { mContext, /* requestCode */ 0, intent, - PendingIntent.FLAG_ONE_SHOT - | PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_CANCEL_CURRENT, + PendingIntent.FLAG_IMMUTABLE + | FLAG_UPDATE_CURRENT, /* options */ null, user); return pi == null ? null : pi.getIntentSender(); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 09a91eda483a..1a20c8ddc32f 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -31,6 +31,7 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; import static android.content.pm.PackageManager.DELETE_ARCHIVE; import static android.content.pm.PackageManager.DELETE_KEEP_DATA; import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; +import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction; import static android.os.PowerExemptionManager.REASON_PACKAGE_UNARCHIVE; import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; @@ -66,8 +67,11 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; import android.graphics.drawable.LayerDrawable; import android.os.Binder; import android.os.Bundle; @@ -379,9 +383,8 @@ public class PackageArchiver { verifyNotSystemApp(ps.getFlags()); verifyInstalled(ps, userId); String responsibleInstallerPackage = getResponsibleInstallerPackage(ps); - verifyInstaller(responsibleInstallerPackage, userId); - ApplicationInfo installerInfo = snapshot.getApplicationInfo( - responsibleInstallerPackage, /* flags= */ 0, userId); + ApplicationInfo installerInfo = verifyInstaller( + snapshot, responsibleInstallerPackage, userId); verifyOptOutStatus(packageName, UserHandle.getUid(userId, UserHandle.getUid(userId, ps.getAppId()))); @@ -421,10 +424,10 @@ public class PackageArchiver { List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); for (int i = 0, size = mainActivities.size(); i < size; ++i) { var mainActivity = mainActivities.get(i); - Path iconPath = storeDrawable( - packageName, mainActivity.getIcon(), userId, i, iconSize); - Path monochromePath = storeDrawable( - packageName, mainActivity.getMonochromeIcon(), userId, i, iconSize); + Path iconPath = storeAdaptiveDrawable( + packageName, mainActivity.getIcon(), userId, i * 2 + 0, iconSize); + Path monochromePath = storeAdaptiveDrawable( + packageName, mainActivity.getMonochromeIcon(), userId, i * 2 + 1, iconSize); ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( mainActivity.getLabel().toString(), @@ -451,7 +454,8 @@ public class PackageArchiver { List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); for (int i = 0, size = mainActivities.size(); i < size; i++) { LauncherActivityInfo mainActivity = mainActivities.get(i); - Path iconPath = storeIcon(packageName, mainActivity, userId, i, iconSize); + Path iconPath = storeIcon(packageName, mainActivity, userId, i * 2 + 0, iconSize); + // i * 2 + 1 reserved for monochromeIcon ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( mainActivity.getLabel().toString(), @@ -495,8 +499,30 @@ public class PackageArchiver { return iconFile.toPath(); } - private void verifyInstaller(String installerPackageName, int userId) - throws PackageManager.NameNotFoundException { + /** + * Create an <a + * href="https://developer.android.com/develop/ui/views/launch/icon_design_adaptive"> + * adaptive icon</a> from an icon. + * This is necessary so the icon can be displayed properly by different launchers. + */ + private static Path storeAdaptiveDrawable(String packageName, @Nullable Drawable iconDrawable, + @UserIdInt int userId, int index, int iconSize) throws IOException { + if (iconDrawable == null) { + return null; + } + + // see BaseIconFactory#createShapedIconBitmap + float inset = getExtraInsetFraction(); + inset = inset / (1 + 2 * inset); + Drawable d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), + new InsetDrawable(iconDrawable/*d*/, inset, inset, inset, inset)); + + return storeDrawable(packageName, d, userId, index, iconSize); + } + + + private ApplicationInfo verifyInstaller(Computer snapshot, String installerPackageName, + int userId) throws PackageManager.NameNotFoundException { if (TextUtils.isEmpty(installerPackageName)) { throw new PackageManager.NameNotFoundException("No installer found"); } @@ -505,6 +531,12 @@ public class PackageArchiver { && !verifySupportsUnarchival(installerPackageName, userId)) { throw new PackageManager.NameNotFoundException("Installer does not support unarchival"); } + ApplicationInfo appInfo = snapshot.getApplicationInfo( + installerPackageName, /* flags=*/ 0, userId); + if (appInfo == null) { + throw new PackageManager.NameNotFoundException("Failed to obtain Installer info"); + } + return appInfo; } /** @@ -570,7 +602,7 @@ public class PackageArchiver { } try { - verifyInstaller(getResponsibleInstallerPackage(ps), userId); + verifyInstaller(snapshot, getResponsibleInstallerPackage(ps), userId); getLauncherActivityInfos(packageName, userId); } catch (PackageManager.NameNotFoundException e) { return false; diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 7bf9fe7aa7e2..cfafe7cc00df 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -788,6 +788,24 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + if (Flags.recoverabilityDetection()) { + if (params.rollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH + || params.rollbackImpactLevel + == PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL) { + if ((params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) { + throw new IllegalArgumentException( + "Can't set rollbackImpactLevel when rollback is not enabled"); + } + if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ROLLBACKS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Setting rollbackImpactLevel requires the MANAGE_ROLLBACKS permission"); + } + } else if (params.rollbackImpactLevel < 0) { + throw new IllegalArgumentException("rollbackImpactLevel can't be negative."); + } + } + boolean isApex = (params.installFlags & PackageManager.INSTALL_APEX) != 0; if (isApex) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGE_UPDATES) diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 117d03fd059b..27c3dad23450 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -54,6 +54,7 @@ import static com.android.internal.util.XmlUtils.writeStringAttribute; import static com.android.internal.util.XmlUtils.writeUriAttribute; import static com.android.server.pm.PackageInstallerService.prepareStageDir; import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerServiceUtils.isInstalledByAdb; import static com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata; @@ -1294,6 +1295,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { info.autoRevokePermissionsMode = params.autoRevokePermissionsMode; info.installFlags = params.installFlags; info.rollbackLifetimeMillis = params.rollbackLifetimeMillis; + info.rollbackImpactLevel = params.rollbackImpactLevel; info.isMultiPackage = params.isMultiPackage; info.isStaged = params.isStaged; info.rollbackDataPolicy = params.rollbackDataPolicy; @@ -1831,7 +1833,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { Os.link(path, sourcePath); // Grant READ access for APK to be read successfully - Os.chmod(sourcePath, 0644); + Os.chmod(sourcePath, DEFAULT_FILE_ACCESS_MODE); } catch (ErrnoException e) { e.rethrowAsIOException(); } @@ -1900,7 +1902,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // If file is app metadata then set permission to 0640 to deny user read access since it // might contain sensitive information. - int mode = name.equals(APP_METADATA_FILE_NAME) ? APP_METADATA_FILE_ACCESS_MODE : 0644; + int mode = name.equals(APP_METADATA_FILE_NAME) + ? APP_METADATA_FILE_ACCESS_MODE : DEFAULT_FILE_ACCESS_MODE; ParcelFileDescriptor targetPfd = openTargetInternal(target.getAbsolutePath(), O_CREAT | O_WRONLY, mode); Os.chmod(target.getAbsolutePath(), mode); @@ -4245,7 +4248,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { throw new IOException("Failed to copy " + fromFile + " to " + tmpFile); } try { - Os.chmod(tmpFile.getAbsolutePath(), 0644); + Os.chmod(tmpFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); } catch (ErrnoException e) { throw new IOException("Failed to chmod " + tmpFile); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index c5b5a761497d..609a703137d7 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -593,6 +593,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService static final String APP_METADATA_FILE_NAME = "app.metadata"; + static final int DEFAULT_FILE_ACCESS_MODE = 0644; + final Handler mHandler; final Handler mBackgroundHandler; @@ -4608,6 +4610,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService }); // Send UNSTOPPED broadcast if necessary if (wasStopped && Flags.stayStopped()) { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "unstoppedBroadcast"); final PackageManagerInternal pmi = mInjector.getLocalService(PackageManagerInternal.class); final int [] userIds = resolveUserIds(userId); @@ -4627,6 +4630,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_UNSTOPPED, packageName, extras, userIds, null /* instantUserIds */, broadcastAllowList, mHandler, null /* filterExtras */); + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } } } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index cd3416348153..85316920f363 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -30,6 +30,7 @@ import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION; import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION; import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING; import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX; import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX; import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME; @@ -69,6 +70,7 @@ import android.os.Debug; import android.os.Environment; import android.os.FileUtils; import android.os.Process; +import android.os.SELinux; import android.os.SystemProperties; import android.os.UserHandle; import android.os.incremental.IncrementalManager; @@ -129,10 +131,12 @@ import java.security.SecureRandom; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.zip.GZIPInputStream; @@ -853,7 +857,7 @@ public class PackageManagerServiceUtils { FileUtils.copy(fileIn, outputStream); // Flush anything in buffer before chmod, because any writes after chmod will fail. outputStream.flush(); - Os.fchmod(outputStream.getFD(), 0644); + Os.fchmod(outputStream.getFD(), DEFAULT_FILE_ACCESS_MODE); atomicFile.finishWrite(outputStream); return PackageManager.INSTALL_SUCCEEDED; } catch (IOException e) { @@ -1081,8 +1085,8 @@ public class PackageManagerServiceUtils { final File targetFile = new File(targetDir, targetName); final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(), - O_RDWR | O_CREAT, 0644); - Os.chmod(targetFile.getAbsolutePath(), 0644); + O_RDWR | O_CREAT, DEFAULT_FILE_ACCESS_MODE); + Os.chmod(targetFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); FileInputStream source = null; try { source = new FileInputStream(sourcePath); @@ -1552,4 +1556,72 @@ public class PackageManagerServiceUtils { public static boolean isInstalledByAdb(String initiatingPackageName) { return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName); } + + public static void linkSplitsToOldDirs(@NonNull Installer installer, + @NonNull String packageName, + @NonNull File newPath, + @Nullable Set<File> oldPaths) { + if (oldPaths == null || oldPaths.isEmpty()) { + return; + } + if (IncrementalManager.isIncrementalPath(newPath.getPath())) { + //TODO(b/291212866): handle incremental installs + return; + } + final File[] filesInNewPath = newPath.listFiles(); + if (filesInNewPath == null || filesInNewPath.length == 0) { + return; + } + final List<String> splitApkNames = new ArrayList<String>(); + for (int i = 0; i < filesInNewPath.length; i++) { + if (!filesInNewPath[i].isDirectory() && filesInNewPath[i].toString().endsWith(".apk")) { + splitApkNames.add(filesInNewPath[i].getName()); + } + } + final int numSplits = splitApkNames.size(); + if (numSplits == 0) { + return; + } + for (File oldPath : oldPaths) { + if (!oldPath.exists()) { + continue; + } + for (int i = 0; i < numSplits; i++) { + final String splitApkName = splitApkNames.get(i); + final File linkedSplit = new File(oldPath, splitApkName); + if (linkedSplit.exists()) { + if (DEBUG) { + Slog.d(PackageManagerService.TAG, "Skipping existing linked split <" + + linkedSplit + ">"); + } + continue; + } + final File sourceSplit = new File(newPath, splitApkName); + try { + installer.linkFile(packageName, splitApkName, + newPath.getAbsolutePath(), oldPath.getAbsolutePath()); + if (DEBUG) { + Slog.d(PackageManagerService.TAG, "Linked <" + + sourceSplit + "> to <" + linkedSplit + ">"); + } + } catch (Installer.InstallerException e) { + Slog.w(PackageManagerService.TAG, "Failed to link split <" + + sourceSplit + " > to <" + linkedSplit + ">", e); + continue; + } + try { + Os.chmod(linkedSplit.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); + } catch (ErrnoException e) { + Slog.w(PackageManagerService.TAG, "Failed to set mode for linked split <" + + linkedSplit + ">", e); + continue; + } + if (!SELinux.restorecon(linkedSplit)) { + Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked split <" + + linkedSplit + ">"); + } + } + } + //TODO(b/291212866): support native libs + } } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index ca00c84da724..5c9c8c6d249a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -28,6 +28,7 @@ import static android.content.pm.PackageManager.RESTRICTION_HIDE_NOTIFICATIONS; import static android.content.pm.PackageManager.RESTRICTION_NONE; import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import android.accounts.IAccountManager; @@ -297,6 +298,8 @@ class PackageManagerShellCommand extends ShellCommand { return runSetHiddenSetting(true); case "unhide": return runSetHiddenSetting(false); + case "unstop": + return runSetStoppedState(false); case "suspend": return runSuspend(true, 0); case "suspend-quarantine": @@ -2347,7 +2350,7 @@ class PackageManagerShellCommand extends ShellCommand { Streams.copy(inStream, outStream); } // Give read permissions to the other group. - Os.chmod(outputProfilePath, /*mode*/ 0644 ); + Os.chmod(outputProfilePath, /*mode*/ DEFAULT_FILE_ACCESS_MODE); } catch (IOException | ErrnoException e) { pw.println("Error when reading the profile fd: " + e.getMessage()); e.printStackTrace(pw); @@ -2662,6 +2665,26 @@ class PackageManagerShellCommand extends ShellCommand { return 0; } + private int runSetStoppedState(boolean state) throws RemoteException { + int userId = UserHandle.USER_SYSTEM; + String option = getNextOption(); + if (option != null && option.equals("--user")) { + userId = UserHandle.parseUserArg(getNextArgRequired()); + } + + String pkg = getNextArg(); + if (pkg == null) { + getErrPrintWriter().println("Error: no package specified"); + return 1; + } + final int translatedUserId = + translateUserId(userId, UserHandle.USER_NULL, "runSetStoppedState"); + mInterface.setPackageStoppedState(pkg, state, translatedUserId); + getOutPrintWriter().println("Package " + pkg + " new stopped state: " + + mInterface.isPackageStoppedForUser(pkg, translatedUserId)); + return 0; + } + private int runSetDistractingRestriction() { final PrintWriter pw = getOutPrintWriter(); int userId = UserHandle.USER_SYSTEM; @@ -3694,7 +3717,19 @@ class PackageManagerShellCommand extends ShellCommand { // remember to set it themselves. params.installerPackageName = "com.android.shell"; } - sessionParams.installFlags |= PackageManager.INSTALL_ENABLE_ROLLBACK; + int rollbackStrategy = PackageManager.ROLLBACK_DATA_POLICY_RESTORE; + try { + rollbackStrategy = Integer.parseInt(peekNextArg()); + if (rollbackStrategy < PackageManager.ROLLBACK_DATA_POLICY_RESTORE + || rollbackStrategy > PackageManager.ROLLBACK_DATA_POLICY_RETAIN) { + throw new IllegalArgumentException( + rollbackStrategy + " is not a valid rollback data policy."); + } + getNextArg(); // pop the argument + } catch (NumberFormatException e) { + // not followed by a number assume ROLLBACK_DATA_POLICY_RESTORE. + } + sessionParams.setEnableRollback(true, rollbackStrategy); break; case "--staged-ready-timeout": params.stagedReadyTimeoutMs = Long.parseLong(getNextArgRequired()); @@ -3729,6 +3764,11 @@ class PackageManagerShellCommand extends ShellCommand { } else if (staged) { sessionParams.setStaged(); } + if ((sessionParams.installFlags & PackageManager.INSTALL_APEX) != 0 + && (sessionParams.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0 + && sessionParams.rollbackDataPolicy == PackageManager.ROLLBACK_DATA_POLICY_WIPE) { + throw new IllegalArgumentException("Data policy 'wipe' is not supported for apex."); + } return params; } @@ -4807,7 +4847,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--install-reason 0/1/2/3/4] [--originating-uri URI]"); pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); - pw.println(" [--enable-rollback]"); + pw.println(" [--enable-rollback [0/1/2]]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]"); pw.println(" [--apex] [--non-staged] [--force-non-staged]"); pw.println(" [--staged-ready-timeout TIMEOUT] [--ignore-dexopt-profile]"); @@ -4831,6 +4871,8 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --abi: override the default ABI of the platform"); pw.println(" --instant: cause the app to be installed as an ephemeral install app"); pw.println(" --full: cause the app to be installed as a non-ephemeral full app"); + pw.println(" --enable-rollback: enable rollbacks for the upgrade."); + pw.println(" 0=restore (default), 1=wipe, 2=retain"); pw.println(" --install-location: force the install location:"); pw.println(" 0=auto, 1=internal only, 2=prefer external"); pw.println(" --install-reason: indicates why the app is being installed:"); @@ -4934,6 +4976,8 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" hide [--user USER_ID] PACKAGE_OR_COMPONENT"); pw.println(" unhide [--user USER_ID] PACKAGE_OR_COMPONENT"); pw.println(""); + pw.println(" unstop [--user USER_ID] PACKAGE"); + pw.println(""); pw.println(" suspend [--user USER_ID] PACKAGE [PACKAGE...]"); pw.println(" Suspends the specified package(s) (as user)."); pw.println(""); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c94111c31ef4..a6598d602d01 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -712,19 +712,24 @@ public class UserManagerService extends IUserManager.Stub { boolean isAutoLockOnDeviceLockSelected = autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK; if (isKeyguardLocked && isAutoLockOnDeviceLockSelected) { - int privateProfileUserId = getPrivateProfileUserId(); - if (privateProfileUserId != UserHandle.USER_NULL) { - Slog.i(LOG_TAG, "Auto-locking private space with user-id " - + privateProfileUserId); - setQuietModeEnabledAsync(privateProfileUserId, - /* enableQuietMode */true, /* target */ null, - mContext.getPackageName()); - } + autoLockPrivateSpace(); } } } @VisibleForTesting + void autoLockPrivateSpace() { + int privateProfileUserId = getPrivateProfileUserId(); + if (privateProfileUserId != UserHandle.USER_NULL) { + Slog.i(LOG_TAG, "Auto-locking private space with user-id " + + privateProfileUserId); + setQuietModeEnabledAsync(privateProfileUserId, + /* enableQuietMode */true, /* target */ null, + mContext.getPackageName()); + } + } + + @VisibleForTesting void setQuietModeEnabledAsync(@UserIdInt int userId, boolean enableQuietMode, IntentSender target, @Nullable String callingPackage) { if (android.multiuser.Flags.moveQuietModeOperationsToSeparateThread()) { @@ -1036,9 +1041,18 @@ public class UserManagerService extends IUserManager.Stub { } } + if (isAutoLockingPrivateSpaceOnRestartsEnabled()) { + autoLockPrivateSpace(); + } + markEphemeralUsersForRemoval(); } + private boolean isAutoLockingPrivateSpaceOnRestartsEnabled() { + return android.os.Flags.allowPrivateProfile() + && android.multiuser.Flags.enablePrivateSpaceAutolockOnRestarts(); + } + /** * This method retrieves the {@link UserManagerInternal} only for the purpose of * PackageManagerService construction. diff --git a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java index 3efac81d44e3..d138606369b9 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java +++ b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java @@ -26,6 +26,7 @@ import android.util.ArrayMap; public final class PermissionAllowlist { @NonNull private final ArrayMap<String, ArrayMap<String, Boolean>> mOemAppAllowlist = new ArrayMap<>(); + @NonNull private final ArrayMap<String, ArrayMap<String, Boolean>> mPrivilegedAppAllowlist = new ArrayMap<>(); @@ -43,6 +44,19 @@ public final class PermissionAllowlist { mApexPrivilegedAppAllowlists = new ArrayMap<>(); @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mSignatureAppAllowlist = + new ArrayMap<>(); + @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mVendorSignatureAppAllowlist = + new ArrayMap<>(); + @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mProductSignatureAppAllowlist = + new ArrayMap<>(); + @NonNull + private final ArrayMap<String, ArrayMap<String, Boolean>> mSystemExtSignatureAppAllowlist = + new ArrayMap<>(); + + @NonNull public ArrayMap<String, ArrayMap<String, Boolean>> getOemAppAllowlist() { return mOemAppAllowlist; } @@ -73,6 +87,26 @@ public final class PermissionAllowlist { return mApexPrivilegedAppAllowlists; } + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getSignatureAppAllowlist() { + return mSignatureAppAllowlist; + } + + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getVendorSignatureAppAllowlist() { + return mVendorSignatureAppAllowlist; + } + + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getProductSignatureAppAllowlist() { + return mProductSignatureAppAllowlist; + } + + @NonNull + public ArrayMap<String, ArrayMap<String, Boolean>> getSystemExtSignatureAppAllowlist() { + return mSystemExtSignatureAppAllowlist; + } + @Nullable public Boolean getOemAppAllowlistState(@NonNull String packageName, @NonNull String permissionName) { @@ -137,4 +171,44 @@ public final class PermissionAllowlist { } return permissions.get(permissionName); } + + @Nullable + public Boolean getSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } + + @Nullable + public Boolean getVendorSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mVendorSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } + + @Nullable + public Boolean getProductSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mProductSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } + + @Nullable + public Boolean getSystemExtSignatureAppAllowlistState(@NonNull String packageName, + @NonNull String permissionName) { + ArrayMap<String, Boolean> permissions = mSystemExtSignatureAppAllowlist.get(packageName); + if (permissions == null) { + return null; + } + return permissions.get(permissionName); + } } diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java index c73728393016..f7603b5cfb57 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java +++ b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java @@ -27,6 +27,8 @@ import com.android.server.pm.InstallSource; import com.android.server.pm.PackageKeySetData; import com.android.server.pm.permission.LegacyPermissionState; +import java.io.File; +import java.util.Set; import java.util.UUID; /** @@ -111,4 +113,7 @@ public interface PackageStateInternal extends PackageState { */ @Nullable String getAppMetadataFilePath(); + + @Nullable + Set<File> getOldPaths(); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index bf669fba82ce..0abf304c34ee 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -5634,7 +5634,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn", 0 /* cookie */); updateScreenOffSleepToken(false /* acquire */, false /* isSwappingDisplay */); - mDefaultDisplayPolicy.screenTurnedOn(screenOnListener); + mDefaultDisplayPolicy.screenTurningOn(screenOnListener); mBootAnimationDismissable = false; synchronized (mLock) { @@ -5676,6 +5676,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mKeyguardDelegate.onScreenTurnedOn(); } } + mDefaultDisplayPolicy.screenTurnedOn(); reportScreenStateToVrManager(true); } diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java index 5e8b4de3894e..7808c4ed50a4 100644 --- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java +++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java @@ -30,6 +30,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.pm.Signature; import android.os.Environment; +import android.permission.flags.Flags; import android.provider.Settings; import android.text.TextUtils; import android.util.ArrayMap; @@ -368,6 +369,7 @@ public class RoleServicePlatformHelperImpl implements RoleServicePlatformHelper dataOutputStream.writeUTF(profileOwner); dataOutputStream.writeInt(Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.DEVICE_DEMO_MODE, 0)); + dataOutputStream.writeBoolean(Flags.walletRoleEnabled()); dataOutputStream.flush(); } catch (IOException e) { // Never happens for MessageDigestOutputStream and DataOutputStream. diff --git a/services/core/java/com/android/server/policy/window_policy_flags.aconfig b/services/core/java/com/android/server/policy/window_policy_flags.aconfig index ed981e0aca74..2154a26acbaf 100644 --- a/services/core/java/com/android/server/policy/window_policy_flags.aconfig +++ b/services/core/java/com/android/server/policy/window_policy_flags.aconfig @@ -4,5 +4,5 @@ flag { name: "support_input_wakeup_delegate" namespace: "wear_frameworks" description: "Whether or not window policy allows injecting input wake-up delegate." - bug: "298055811" + bug: "319132073" }
\ No newline at end of file diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java index 871e98bf4ab3..4bf8a78a1f16 100644 --- a/services/core/java/com/android/server/power/ShutdownThread.java +++ b/services/core/java/com/android/server/power/ShutdownThread.java @@ -319,6 +319,11 @@ public final class ShutdownThread extends Thread { pd.setMax(100); pd.setProgress(0); pd.setIndeterminate(false); + boolean showPercent = context.getResources().getBoolean( + com.android.internal.R.bool.config_showPercentageTextDuringRebootToUpdate); + if (!showPercent) { + pd.setProgressPercentFormat(null); + } pd.setProgressNumberFormat(null); pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); pd.setMessage(context.getText( @@ -911,4 +916,4 @@ public final class ShutdownThread extends Thread { com.android.internal.R.string.config_defaultShutdownVibrationFile); } } -}
\ No newline at end of file +} diff --git a/services/core/java/com/android/server/rollback/README.md b/services/core/java/com/android/server/rollback/README.md index 08800dada564..f6bcbd057307 100644 --- a/services/core/java/com/android/server/rollback/README.md +++ b/services/core/java/com/android/server/rollback/README.md @@ -187,13 +187,22 @@ executed. ### Installing an App with Rollback Enabled -The `adb install` command accepts the `--enable-rollback` flag to install an app +The `adb install` command accepts the `--enable-rollback [0/1/2]` flag to install an app with rollback enabled. For example: ``` $ adb install --enable-rollback FooV2.apk ``` +The default rollback data policy is `ROLLBACK_DATA_POLICY_RESTORE` (0). To use +a different `RollbackDataPolicy`, like `ROLLBACK_DATA_POLICY_RETAIN` (1) or +`ROLLBACK_DATA_POLICY_WIPE` (2), provide the int value after +`--enable-rollback`. For example: + +``` +$ adb install --enable-rollback 1 FooV2.apk +``` + ### Triggering Rollback Manually If rollback is available for an application, the pm command can be used to @@ -217,7 +226,7 @@ $ adb shell dumpsys rollback -state: committed -timestamp: 2019-04-23T14:57:35.944Z -packages: - com.android.tests.rollback.testapp.B 2 -> 1 + com.android.tests.rollback.testapp.B 2 -> 1 [0] -causePackages: -committedSessionId: 1845805640 649899517: @@ -225,7 +234,7 @@ $ adb shell dumpsys rollback -timestamp: 2019-04-23T12:55:21.342Z -stagedSessionId: 343374391 -packages: - com.android.tests.rollback.testapex 2 -> 1 + com.android.tests.rollback.testapex 2 -> 1 [0] -causePackages: -committedSessionId: 2096717281 ``` @@ -233,7 +242,8 @@ $ adb shell dumpsys rollback The example above shows two recently committed rollbacks. The update of com.android.tests.rollback.testapp.B from version 1 to version 2 was rolled back, and the update of com.android.tests.rollback.testapex from version 1 to -version 2 was rolled back. +version 2 was rolled back. For each package the value inside '[' and ']' +indicates the `RollbackDataPolicy` for the rollback back. The state is 'available' or 'committed'. The timestamp gives the time when the rollback was first made available. If a stagedSessionId is present, then the diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java index a5b90f12a5cc..d1f91c89a04e 100644 --- a/services/core/java/com/android/server/rollback/Rollback.java +++ b/services/core/java/com/android/server/rollback/Rollback.java @@ -215,7 +215,8 @@ class Rollback { /* packages */ new ArrayList<>(), /* isStaged */ isStaged, /* causePackages */ new ArrayList<>(), - /* committedSessionId */ -1); + /* committedSessionId */ -1, + /* rollbackImpactLevel */ PackageManager.ROLLBACK_USER_IMPACT_LOW); mUserId = userId; mInstallerPackageName = installerPackageName; mBackupDir = backupDir; @@ -394,7 +395,8 @@ class Rollback { */ @WorkerThread boolean enableForPackage(String packageName, long newVersion, long installedVersion, - boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy) { + boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy, + @PackageManager.RollbackImpactLevel int rollbackImpactLevel) { assertInWorkerThread(); try { RollbackStore.backupPackageCodePath(this, packageName, sourceDir); @@ -415,6 +417,10 @@ class Rollback { isApex, false /* isApkInApex */, new ArrayList<>(), rollbackDataPolicy); info.getPackages().add(packageRollbackInfo); + + if (info.getRollbackImpactLevel() < rollbackImpactLevel) { + info.setRollbackImpactLevel(rollbackImpactLevel); + } return true; } @@ -961,7 +967,8 @@ class Rollback { for (PackageRollbackInfo pkg : info.getPackages()) { ipw.println(pkg.getPackageName() + " " + pkg.getVersionRolledBackFrom().getLongVersionCode() - + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode()); + + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode() + + " [" + pkg.getRollbackDataPolicy() + "]"); } ipw.decreaseIndent(); if (isCommitted()) { diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java index 13f114138261..359678b15213 100644 --- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java +++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java @@ -954,7 +954,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub implements Rollba ApplicationInfo appInfo = pkgInfo.applicationInfo; return rollback.enableForPackage(packageName, newPackage.getVersionCode(), pkgInfo.getLongVersionCode(), isApex, appInfo.sourceDir, - appInfo.splitSourceDirs, rollbackDataPolicy); + appInfo.splitSourceDirs, rollbackDataPolicy, session.rollbackImpactLevel); } @ExtThread diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index 0af137faf5b4..14539d544bf9 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -193,16 +193,27 @@ class RollbackStore { json.put("isStaged", rollback.isStaged()); json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages())); json.put("committedSessionId", rollback.getCommittedSessionId()); + if (Flags.recoverabilityDetection()) { + json.put("rollbackImpactLevel", rollback.getRollbackImpactLevel()); + } return json; } private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException { - return new RollbackInfo( + RollbackInfo rollbackInfo = new RollbackInfo( json.getInt("rollbackId"), packageRollbackInfosFromJson(json.getJSONArray("packages")), json.getBoolean("isStaged"), versionedPackagesFromJson(json.getJSONArray("causePackages")), json.getInt("committedSessionId")); + + if (Flags.recoverabilityDetection()) { + // to make it backward compatible. + rollbackInfo.setRollbackImpactLevel(json.optInt("rollbackImpactLevel", + PackageManager.ROLLBACK_USER_IMPACT_LOW)); + } + + return rollbackInfo; } /** diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java index b384725711c4..92b57645b9a3 100755 --- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java +++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java @@ -309,6 +309,24 @@ class TvInputHardwareManager implements TvInputHal.Callback { } } + public SparseArray<String> getHardwareInputIdMap() { + synchronized (mLock) { + return mHardwareInputIdMap.clone(); + } + } + + public SparseArray<String> getHdmiInputIdMap() { + synchronized (mLock) { + return mHdmiInputIdMap.clone(); + } + } + + public Map<String, TvInputInfo> getInputMap() { + synchronized (mLock) { + return Collections.unmodifiableMap(mInputMap); + } + } + public Map<String, List<String>> getHdmiParentInputMap() { synchronized (mLock) { return Collections.unmodifiableMap(mHdmiParentInputMap); diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 73fc8e9cfbc4..e434df7836c4 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -135,6 +135,7 @@ public final class TvInputManagerService extends SystemService { private static final int APP_TAG_SELF = TunedInfo.APP_TAG_SELF; private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS = "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"; + private static final long UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds // There are two different formats of DVB frontend devices. One is /dev/dvb%d.frontend%d, // another one is /dev/dvb/adapter%d/frontend%d. Followings are the patterns for selecting the @@ -174,7 +175,7 @@ public final class TvInputManagerService extends SystemService { @GuardedBy("mLock") private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>(); - private final WatchLogHandler mWatchLogHandler; + private final MessageHandler mMessageHandler; private final ActivityManager mActivityManager; @@ -187,8 +188,8 @@ public final class TvInputManagerService extends SystemService { super(context); mContext = context; - mWatchLogHandler = new WatchLogHandler(mContext.getContentResolver(), - IoThread.get().getLooper()); + mMessageHandler = + new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); mActivityManager = @@ -372,10 +373,10 @@ public final class TvInputManagerService extends SystemService { // service to populate the hardware list. serviceState = new ServiceState(component, userId); userState.serviceStateMap.put(component, serviceState); + updateServiceConnectionLocked(component, userId); } else { inputList.addAll(serviceState.hardwareInputMap.values()); } - updateServiceConnectionLocked(component, userId); } else { try { TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build(); @@ -510,6 +511,7 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") private void startProfileLocked(int userId) { mRunningProfiles.add(userId); buildTvInputListLocked(userId, null); @@ -538,8 +540,10 @@ public final class TvInputManagerService extends SystemService { mCurrentUserId = userId; buildTvInputListLocked(userId, null); buildTvContentRatingSystemListLocked(userId); - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_SWITCH_CONTENT_RESOLVER, - getContentResolverForUser(userId)).sendToTarget(); + mMessageHandler + .obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER, + getContentResolverForUser(userId)) + .sendToTarget(); } } @@ -593,7 +597,7 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in unregisterCallback", e); } } - mContext.unbindService(serviceState.connection); + unbindService(serviceState); it.remove(); } } @@ -661,7 +665,7 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in unregisterCallback", e); } } - mContext.unbindService(serviceState.connection); + unbindService(serviceState); } } userState.serviceStateMap.clear(); @@ -774,7 +778,8 @@ public final class TvInputManagerService extends SystemService { boolean shouldBind; if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) { - shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware; + shouldBind = !serviceState.sessionTokens.isEmpty() + || (serviceState.isHardware && serviceState.neverConnected); } else { // For a non-current user, // if sessionTokens is not empty, it contains recording sessions only @@ -783,31 +788,14 @@ public final class TvInputManagerService extends SystemService { shouldBind = !serviceState.sessionTokens.isEmpty(); } - if (serviceState.service == null && shouldBind) { - // This means that the service is not yet connected but its state indicates that we - // have pending requests. Then, connect the service. - if (serviceState.bound) { - // We have already bound to the service so we don't try to bind again until after we - // unbind later on. - return; - } - if (DEBUG) { - Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")"); - } - - Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component); - serviceState.bound = mContext.bindServiceAsUser( - i, serviceState.connection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, - new UserHandle(userId)); - } else if (serviceState.service != null && !shouldBind) { - // This means that the service is already connected but its state indicates that we have - // nothing to do with it. Then, disconnect the service. - if (DEBUG) { - Slog.d(TAG, "unbindService(service=" + component + ")"); + // only bind/unbind when necessary. + if (shouldBind && !serviceState.bound) { + bindService(serviceState, userId); + } else if (!shouldBind && serviceState.bound) { + unbindService(serviceState); + if (!serviceState.isHardware) { + userState.serviceStateMap.remove(component); } - mContext.unbindService(serviceState.connection); - userState.serviceStateMap.remove(component); } } @@ -829,7 +817,11 @@ public final class TvInputManagerService extends SystemService { sendSessionTokenToClientLocked(sessionState.client, sessionState.inputId, null, null, sessionState.seq); } - updateServiceConnectionLocked(serviceState.component, userId); + if (!serviceState.isHardware) { + updateServiceConnectionLocked(serviceState.component, userId); + } else { + updateHardwareServiceConnectionDelayed(userId); + } } @GuardedBy("mLock") @@ -948,13 +940,17 @@ public final class TvInputManagerService extends SystemService { if (serviceState != null) { serviceState.sessionTokens.remove(sessionToken); } - updateServiceConnectionLocked(sessionState.componentName, userId); + if (!serviceState.isHardware) { + updateServiceConnectionLocked(sessionState.componentName, userId); + } else { + updateHardwareServiceConnectionDelayed(userId); + } // Log the end of watch. SomeArgs args = SomeArgs.obtain(); args.arg1 = sessionToken; args.arg2 = System.currentTimeMillis(); - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_END, args).sendToTarget(); + mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_END, args).sendToTarget(); } @GuardedBy("mLock") @@ -1153,8 +1149,7 @@ public final class TvInputManagerService extends SystemService { ServiceState serviceState = userState.serviceStateMap.get(inputState.info.getComponent()); int oldState = inputState.state; inputState.state = state; - if (serviceState != null && serviceState.service == null - && (!serviceState.sessionTokens.isEmpty() || serviceState.isHardware)) { + if (serviceState != null && serviceState.reconnecting) { // We don't notify state change while reconnecting. It should remain disconnected. return; } @@ -1881,7 +1876,7 @@ public final class TvInputManagerService extends SystemService { args.arg3 = ContentUris.parseId(channelUri); args.arg4 = params; args.arg5 = sessionToken; - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args) + mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_START, args) .sendToTarget(); } catch (RemoteException | SessionNotFoundException e) { Slog.e(TAG, "error in tune", e); @@ -3327,16 +3322,21 @@ public final class TvInputManagerService extends SystemService { private final ComponentName component; private final boolean isHardware; private final Map<String, TvInputInfo> hardwareInputMap = new HashMap<>(); + private final List<TvInputHardwareInfo> hardwareDeviceRemovedBuffer = new ArrayList<>(); + private final List<HdmiDeviceInfo> hdmiDeviceRemovedBuffer = new ArrayList<>(); + private final List<HdmiDeviceInfo> hdmiDeviceUpdatedBuffer = new ArrayList<>(); private ITvInputService service; private ServiceCallback callback; private boolean bound; private boolean reconnecting; + private boolean neverConnected; private ServiceState(ComponentName component, int userId) { this.component = component; this.connection = new InputServiceConnection(component, userId); this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component); + this.neverConnected = true; } } @@ -3449,6 +3449,97 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") + private void bindService(ServiceState serviceState, int userId) { + if (serviceState.bound) { + // We have already bound to the service so we don't try to bind again until after we + // unbind later on. + // For hardware services, call updateHardwareServiceConnectionDelayed() to delay the + // possible unbinding. + if (serviceState.isHardware) { + updateHardwareServiceConnectionDelayed(userId); + } + return; + } + if (DEBUG) { + Slog.d(TAG, + "bindServiceAsUser(service=" + serviceState.component + ", userId=" + userId + + ")"); + } + Intent i = + new Intent(TvInputService.SERVICE_INTERFACE).setComponent(serviceState.component); + serviceState.bound = mContext.bindServiceAsUser(i, serviceState.connection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, + new UserHandle(userId)); + if (!serviceState.bound) { + Slog.e(TAG, "failed to bind " + serviceState.component + " for userId " + userId); + mContext.unbindService(serviceState.connection); + } + } + + @GuardedBy("mLock") + private void unbindService(ServiceState serviceState) { + if (!serviceState.bound) { + return; + } + if (DEBUG) { + Slog.d(TAG, "unbindService(service=" + serviceState.component + ")"); + } + mContext.unbindService(serviceState.connection); + serviceState.bound = false; + serviceState.service = null; + serviceState.callback = null; + } + + @GuardedBy("mLock") + private void updateHardwareTvInputServiceBindingLocked(int userId) { + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> services = + pm.queryIntentServicesAsUser(new Intent(TvInputService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId); + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) { + continue; + } + ComponentName component = new ComponentName(si.packageName, si.name); + if (hasHardwarePermission(pm, component)) { + updateServiceConnectionLocked(component, userId); + } + } + } + + private void updateHardwareServiceConnectionDelayed(int userId) { + mMessageHandler.removeMessages(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING); + SomeArgs args = SomeArgs.obtain(); + args.arg1 = userId; + Message msg = + mMessageHandler.obtainMessage(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING, args); + mMessageHandler.sendMessageDelayed(msg, UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS); + } + + @GuardedBy("mLock") + private void addHardwareInputLocked( + TvInputInfo inputInfo, ComponentName component, int userId) { + ServiceState serviceState = getServiceStateLocked(component, userId); + serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo); + buildTvInputListLocked(userId, null); + } + + @GuardedBy("mLock") + private void removeHardwareInputLocked(String inputId, int userId) { + if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) { + return; + } + ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent(); + ServiceState serviceState = getServiceStateLocked(component, userId); + boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; + if (removed) { + buildTvInputListLocked(userId, null); + mTvInputHardwareManager.removeHardwareInput(inputId); + } + } + private final class InputServiceConnection implements ServiceConnection { private final ComponentName mComponent; private final int mUserId; @@ -3472,6 +3563,7 @@ public final class TvInputManagerService extends SystemService { } ServiceState serviceState = userState.serviceStateMap.get(mComponent); serviceState.service = ITvInputService.Stub.asInterface(service); + serviceState.neverConnected = false; // Register a callback, if we need to. if (serviceState.isHardware && serviceState.callback == null) { @@ -3483,19 +3575,6 @@ public final class TvInputManagerService extends SystemService { } } - List<IBinder> tokensToBeRemoved = new ArrayList<>(); - - // And create sessions, if any. - for (IBinder sessionToken : serviceState.sessionTokens) { - if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) { - tokensToBeRemoved.add(sessionToken); - } - } - - for (IBinder sessionToken : tokensToBeRemoved) { - removeSessionStateLocked(sessionToken, mUserId); - } - for (TvInputState inputState : userState.inputMap.values()) { if (inputState.info.getComponent().equals(component) && inputState.state != INPUT_STATE_CONNECTED) { @@ -3505,7 +3584,24 @@ public final class TvInputManagerService extends SystemService { } if (serviceState.isHardware) { - serviceState.hardwareInputMap.clear(); + for (TvInputHardwareInfo hardwareToBeRemoved : + serviceState.hardwareDeviceRemovedBuffer) { + try { + serviceState.service.notifyHardwareRemoved(hardwareToBeRemoved); + } catch (RemoteException e) { + Slog.e(TAG, "error in hardwareDeviceRemovedBuffer", e); + } + } + serviceState.hardwareDeviceRemovedBuffer.clear(); + for (HdmiDeviceInfo hdmiDeviceToBeRemoved : + serviceState.hdmiDeviceRemovedBuffer) { + try { + serviceState.service.notifyHdmiDeviceRemoved(hdmiDeviceToBeRemoved); + } catch (RemoteException e) { + Slog.e(TAG, "error in hdmiDeviceRemovedBuffer", e); + } + } + serviceState.hdmiDeviceRemovedBuffer.clear(); for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) { try { serviceState.service.notifyHardwareAdded(hardware); @@ -3520,6 +3616,32 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in notifyHdmiDeviceAdded", e); } } + for (HdmiDeviceInfo hdmiDeviceToBeUpdated : + serviceState.hdmiDeviceUpdatedBuffer) { + try { + serviceState.service.notifyHdmiDeviceUpdated(hdmiDeviceToBeUpdated); + } catch (RemoteException e) { + Slog.e(TAG, "error in hdmiDeviceUpdatedBuffer", e); + } + } + serviceState.hdmiDeviceUpdatedBuffer.clear(); + } + + List<IBinder> tokensToBeRemoved = new ArrayList<>(); + + // And create sessions, if any. + for (IBinder sessionToken : serviceState.sessionTokens) { + if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) { + tokensToBeRemoved.add(sessionToken); + } + } + + for (IBinder sessionToken : tokensToBeRemoved) { + removeSessionStateLocked(sessionToken, mUserId); + } + + if (serviceState.isHardware) { + updateHardwareServiceConnectionDelayed(mUserId); } } } @@ -3570,13 +3692,6 @@ public final class TvInputManagerService extends SystemService { } } - @GuardedBy("mLock") - private void addHardwareInputLocked(TvInputInfo inputInfo) { - ServiceState serviceState = getServiceStateLocked(mComponent, mUserId); - serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo); - buildTvInputListLocked(mUserId, null); - } - public void addHardwareInput(int deviceId, TvInputInfo inputInfo) { ensureHardwarePermission(); ensureValidInput(inputInfo); @@ -3587,8 +3702,11 @@ public final class TvInputManagerService extends SystemService { if (serviceState.hardwareInputMap.containsKey(inputInfo.getId())) { return; } + Slog.d("ServiceCallback", + "addHardwareInput: device id " + deviceId + ", " + + inputInfo.toString()); mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo); - addHardwareInputLocked(inputInfo); + addHardwareInputLocked(inputInfo, mComponent, mUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -3606,7 +3724,7 @@ public final class TvInputManagerService extends SystemService { return; } mTvInputHardwareManager.addHdmiInput(id, inputInfo); - addHardwareInputLocked(inputInfo); + addHardwareInputLocked(inputInfo, mComponent, mUserId); if (mOnScreenInputId != null && mOnScreenSessionState != null) { if (TextUtils.equals(mOnScreenInputId, inputInfo.getParentId())) { // catch the use case when a CEC device is plugged in an HDMI port, @@ -3635,14 +3753,9 @@ public final class TvInputManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - ServiceState serviceState = getServiceStateLocked(mComponent, mUserId); - boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; - if (removed) { - buildTvInputListLocked(mUserId, null); - mTvInputHardwareManager.removeHardwareInput(inputId); - } else { - Slog.e(TAG, "failed to remove input " + inputId); - } + Slog.d("ServiceCallback", + "removeHardwareInput " + inputId + " by " + mComponent); + removeHardwareInputLocked(inputId, mUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -3860,6 +3973,23 @@ public final class TvInputManagerService extends SystemService { } @Override + public void onVideoFreezeUpdated(boolean isFrozen) { + synchronized (mLock) { + if (DEBUG) { + Slog.d(TAG, "onVideoFreezeUpdated(" + isFrozen + ")"); + } + if (mSessionState.session == null || mSessionState.client == null) { + return; + } + try { + mSessionState.client.onVideoFreezeUpdated(isFrozen, mSessionState.seq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onVideoFreezeUpdated", e); + } + } + } + + @Override public void onContentAllowed() { synchronized (mLock) { if (DEBUG) { @@ -4209,11 +4339,12 @@ public final class TvInputManagerService extends SystemService { return loggedReason; } + @GuardedBy("mLock") private UserState getUserStateLocked(int userId) { return mUserStates.get(userId); } - private static final class WatchLogHandler extends Handler { + private final class MessageHandler extends Handler { // There are only two kinds of watch events that can happen on the system: // 1. The current TV input session is tuned to a new channel. // 2. The session is released for some reason. @@ -4225,10 +4356,11 @@ public final class TvInputManagerService extends SystemService { static final int MSG_LOG_WATCH_START = 1; static final int MSG_LOG_WATCH_END = 2; static final int MSG_SWITCH_CONTENT_RESOLVER = 3; + static final int MSG_UPDATE_HARDWARE_TIS_BINDING = 4; private ContentResolver mContentResolver; - WatchLogHandler(ContentResolver contentResolver, Looper looper) { + MessageHandler(ContentResolver contentResolver, Looper looper) { super(looper); mContentResolver = contentResolver; } @@ -4287,6 +4419,14 @@ public final class TvInputManagerService extends SystemService { mContentResolver = (ContentResolver) msg.obj; break; } + case MSG_UPDATE_HARDWARE_TIS_BINDING: + SomeArgs args = (SomeArgs) msg.obj; + int userId = (int) args.arg1; + synchronized (mLock) { + updateHardwareTvInputServiceBindingLocked(userId); + } + args.recycle(); + break; default: { Slog.w(TAG, "unhandled message code: " + msg.what); break; @@ -4342,29 +4482,46 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHardwareAdded(info); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHardwareAdded(info); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHardwareAdded", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @Override public void onHardwareDeviceRemoved(TvInputHardwareInfo info) { synchronized (mLock) { + String relatedInputId = + mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId()); + removeHardwareInputLocked(relatedInputId, mCurrentUserId); UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHardwareRemoved(info); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHardwareRemoved(info); + } else { + serviceState.hardwareDeviceRemovedBuffer.add(info); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHardwareRemoved", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @@ -4374,29 +4531,46 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceAdded(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceAdded(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceAdded", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @Override public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) { synchronized (mLock) { + String relatedInputId = + mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId()); + removeHardwareInputLocked(relatedInputId, mCurrentUserId); UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceRemoved(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceRemoved(deviceInfo); + } else { + serviceState.hdmiDeviceRemovedBuffer.add(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @@ -4424,13 +4598,21 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceUpdated(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceUpdated(deviceInfo); + } else { + serviceState.hdmiDeviceUpdatedBuffer.add(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceUpdated", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } 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 7ab075e2f3a7..9c60fbb6bb9a 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -1470,6 +1470,28 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void notifyVideoFreezeUpdated(IBinder sessionToken, boolean isFrozen, int userId) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId, + "notifyVideoFreezeUpdated"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + SessionState sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).notifyVideoFreezeUpdated(isFrozen); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in notifyVideoFreezeUpdated", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void notifyContentAllowed(IBinder sessionToken, int userId) { final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); @@ -1557,7 +1579,6 @@ public class TvInteractiveAppManagerService extends SystemService { } } - @Override public void notifyRecordingStarted(IBinder sessionToken, String recordingId, String requestId, int userId) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 51acc8e01cda..8549957f46b8 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -410,9 +410,10 @@ public class WallpaperCropper { // adapt the entries in wallpaper.mCropHints for the actual display SparseArray<Rect> updatedCropHints = new SparseArray<>(); for (int i = 0; i < wallpaper.mCropHints.size(); i++) { - Rect defaultCrop = defaultDisplayCrops.valueAt(i); + int orientation = wallpaper.mCropHints.keyAt(i); + Rect defaultCrop = defaultDisplayCrops.get(orientation); if (defaultCrop != null) { - updatedCropHints.put(defaultDisplayCrops.keyAt(i), defaultCrop); + updatedCropHints.put(orientation, defaultCrop); } } wallpaper.mCropHints = updatedCropHints; diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9b1f9c8441ad..036f7b6841c2 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3965,20 +3965,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return removedFromHistory; } - boolean safelyDestroy(String reason) { - if (isDestroyable()) { - if (DEBUG_SWITCH) { - final Task task = getTask(); - Slog.v(TAG_SWITCH, "Safely destroying " + this + " in state " + getState() - + " resumed=" + task.getTopResumedActivity() - + " pausing=" + task.getTopPausingActivity() - + " for reason " + reason); - } - return destroyImmediately(reason); - } - return false; - } - /** Note: call {@link #cleanUp(boolean, boolean)} before this method. */ void removeFromHistory(String reason) { finishActivityResults(Activity.RESULT_CANCELED, @@ -4047,10 +4033,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - boolean isFinishing() { - return finishing; - } - /** * This method is to only be called from the client via binder when the activity is destroyed * AND finished. @@ -7978,6 +7960,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) { return; } + final int originalRelaunchingCount = mPendingRelaunchCount; // This is necessary in order to avoid going into size compat mode when the orientation // change request comes from the app if (getRequestedConfigurationOrientation(false, requestedOrientation) @@ -7995,8 +7978,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // the request is handled at task level with letterbox. if (!getMergedOverrideConfiguration().equals( mLastReportedConfiguration.getMergedConfiguration())) { - ensureActivityConfiguration( - false /* ignoreVisibility */, true /* isRequestedOrientationChanged */); + ensureActivityConfiguration(false /* ignoreVisibility */); + if (mPendingRelaunchCount > originalRelaunchingCount) { + mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true); + } if (mTransitionController.inPlayingTransition(this)) { mTransitionController.mValidateActivityCompat.add(this); } @@ -9502,11 +9487,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return ensureActivityConfiguration(false /* ignoreVisibility */); } - boolean ensureActivityConfiguration(boolean ignoreVisibility) { - return ensureActivityConfiguration(ignoreVisibility, - false /* isRequestedOrientationChanged */); - } - /** * Make sure the given activity matches the current configuration. Ensures the HistoryRecord * is updated with the correct configuration and all other bookkeeping is handled. @@ -9515,13 +9495,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * (stopped state). This is useful for the case where we know the * activity will be visible soon and we want to ensure its configuration * before we make it visible. - * @param isRequestedOrientationChanged whether this is triggered in response to an app calling - * {@link android.app.Activity#setRequestedOrientation}. * @return False if the activity was relaunched and true if it wasn't relaunched because we * can't or the app handles the specific configuration that is changing. */ - boolean ensureActivityConfiguration(boolean ignoreVisibility, - boolean isRequestedOrientationChanged) { + boolean ensureActivityConfiguration(boolean ignoreVisibility) { final Task rootTask = getRootTask(); if (rootTask.mConfigWillChange) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check " @@ -9658,9 +9635,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { mRelaunchReason = RELAUNCH_REASON_NONE; } - if (isRequestedOrientationChanged) { - mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true); - } if (mState == PAUSING) { // A little annoying: we are waiting for this activity to finish pausing. Let's not // do anything now, but just flag that it needs to be restarted when done pausing. diff --git a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java index f1a2159d6dbe..db27f607c867 100644 --- a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java +++ b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java @@ -43,7 +43,7 @@ class ActivitySecurityModelFeatureFlags { static final String DOC_LINK = "go/android-asm"; /** Used to determine which version of the ASM logic was used in logs while we iterate */ - static final int ASM_VERSION = 8; + static final int ASM_VERSION = 9; private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER; private static final String KEY_ASM_PREFIX = "ActivitySecurity__"; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index f6d77ea33598..d6f52b89819e 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2053,8 +2053,8 @@ class ActivityStarter { } if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart( - mSourceRecord, r, newTask, targetTask, mLaunchFlags, mBalCode, mCallingUid, - mRealCallingUid)) { + mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode, + mCallingUid, mRealCallingUid)) { return START_ABORTED; } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 2bd49bfa6219..a4d15e07a3ed 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.View.FOCUS_FORWARD; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_NONE; @@ -60,6 +61,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; import com.android.server.wm.utils.InsetUtils; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.ArrayList; @@ -167,6 +169,24 @@ class BackNavigationController { return null; } + // Move focus to the adjacent embedded window if it is higher than this window + final TaskFragment taskFragment = window.getTaskFragment(); + final TaskFragment adjacentTaskFragment = + taskFragment != null ? taskFragment.getAdjacentTaskFragment() : null; + if (adjacentTaskFragment != null && taskFragment.isEmbedded() + && Flags.embeddedActivityBackNavFlag()) { + final WindowContainer parent = taskFragment.getParent(); + if (parent.mChildren.indexOf(taskFragment) < parent.mChildren.indexOf( + adjacentTaskFragment)) { + mWindowManagerService.moveFocusToAdjacentWindow(window, FOCUS_FORWARD); + window = wmService.getFocusedWindowLocked(); + if (window == null) { + Slog.e(TAG, "Adjacent window is null, returning null."); + return null; + } + } + } + // This is needed to bridge the old and new back behavior with recents. While in // Overview with live tile enabled, the previous app is technically focused but we // add an input consumer to capture all input that would otherwise go to the apps diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 0f36d8eafbe4..bcb4559d79e4 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -57,6 +57,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Process; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArraySet; @@ -1042,8 +1043,9 @@ public class BackgroundActivityStartController { * create a new task or bring an existing one into the foreground */ boolean checkActivityAllowedToStart(@Nullable ActivityRecord sourceRecord, - @NonNull ActivityRecord targetRecord, boolean newTask, @NonNull Task targetTask, - int launchFlags, int balCode, int callingUid, int realCallingUid) { + @NonNull ActivityRecord targetRecord, boolean newTask, boolean avoidMoveTaskToFront, + @Nullable Task targetTask, int launchFlags, int balCode, int callingUid, + int realCallingUid) { // BAL Exception allowed in all cases if (balCode == BAL_ALLOW_ALLOWLISTED_UID) { return true; @@ -1067,14 +1069,36 @@ public class BackgroundActivityStartController { } if (balCode == BAL_ALLOW_GRACE_PERIOD) { + // Allow if launching into new task, and caller matches most recently finished activity if (taskToFront && mTopFinishedActivity != null && mTopFinishedActivity.mUid == callingUid) { return true; - } else if (!taskToFront) { - FinishedActivityEntry finishedEntry = - mTaskIdToFinishedActivity.get(targetTask.mTaskId); - if (finishedEntry != null && finishedEntry.mUid == callingUid) { - return true; + } + + // Launching into existing task - allow if matches most recently finished activity + // within the task. + // We can reach here multiple ways: + // 1. activity in fg fires intent (taskToFront = false, sourceRecord is available) + // 2. activity in bg fires intent (taskToFront = false, sourceRecord is available) + // 3. activity in bg fires intent with NEW_FLAG (taskToFront = true, + // avoidMoveTaskToFront = true, sourceRecord is available) + // 4. activity in bg fires PI (taskToFront = true, avoidMoveTaskToFront = true, + // sourceRecord is not available, targetTask may be available) + if (!taskToFront || avoidMoveTaskToFront) { + if (targetTask != null) { + FinishedActivityEntry finishedEntry = + mTaskIdToFinishedActivity.get(targetTask.mTaskId); + if (finishedEntry != null && finishedEntry.mUid == callingUid) { + return true; + } + } + + if (sourceRecord != null) { + FinishedActivityEntry finishedEntry = + mTaskIdToFinishedActivity.get(sourceRecord.getTask().mTaskId); + if (finishedEntry != null && finishedEntry.mUid == callingUid) { + return true; + } } } } @@ -1098,7 +1122,7 @@ public class BackgroundActivityStartController { bas = isTopActivityMatchingUidAbsentForAsm(taskToCheck, sourceRecord.getUid(), sourceRecord); } - } else if (!taskToFront) { + } else if (targetTask != null && (!taskToFront || avoidMoveTaskToFront)) { // We don't have a sourceRecord, and we're launching into an existing task. // Allow if callingUid is top of stack. bas = isTopActivityMatchingUidAbsentForAsm(targetTask, callingUid, @@ -1111,12 +1135,14 @@ public class BackgroundActivityStartController { // ASM rules have failed. Log why return logAsmFailureAndCheckFeatureEnabled(sourceRecord, callingUid, realCallingUid, - newTask, targetTask, targetRecord, balCode, launchFlags, bas, taskToFront); + newTask, avoidMoveTaskToFront, targetTask, targetRecord, balCode, launchFlags, + bas, taskToFront); } private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord sourceRecord, int callingUid, - int realCallingUid, boolean newTask, Task targetTask, ActivityRecord targetRecord, - @BalCode int balCode, int launchFlags, BlockActivityStart bas, boolean taskToFront) { + int realCallingUid, boolean newTask, boolean avoidMoveTaskToFront, Task targetTask, + ActivityRecord targetRecord, @BalCode int balCode, int launchFlags, + BlockActivityStart bas, boolean taskToFront) { ActivityRecord targetTopActivity = targetTask == null ? null : targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop()); @@ -1133,7 +1159,7 @@ public class BackgroundActivityStartController { String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", sourceRecord, targetRecord, targetTask, targetTopActivity, realCallingUid, balCode, - blockActivityStartAndFeatureEnabled, taskToFront); + blockActivityStartAndFeatureEnabled, taskToFront, avoidMoveTaskToFront); FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, /* caller_uid */ @@ -1265,7 +1291,7 @@ public class BackgroundActivityStartController { Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord, targetTask, targetTaskTop, realCallingUid, balCode, shouldBlockActivityStart, - /* taskToFront */ true)); + /* taskToFront */ true, /* avoidMoveTaskToFront */ false)); } } @@ -1379,7 +1405,7 @@ public class BackgroundActivityStartController { private BlockActivityStart isTopActivityMatchingUidAbsentForAsm(@NonNull Task task, int uid, @Nullable ActivityRecord sourceRecord) { // If the source is visible, consider it 'top'. - if (sourceRecord != null && sourceRecord.isVisible()) { + if (sourceRecord != null && sourceRecord.isVisibleRequested()) { return new BlockActivityStart(false, false); } @@ -1389,6 +1415,12 @@ public class BackgroundActivityStartController { return new BlockActivityStart(false, false); } + // If UID is visible in target task, allow launch + if (task.forAllActivities((Predicate<ActivityRecord>) + ar -> ar.isUid(uid) && ar.isVisibleRequested())) { + return new BlockActivityStart(false, false); + } + // Consider the source activity, whether or not it is finishing. Do not consider any other // finishing activity. Predicate<ActivityRecord> topOfStackPredicate = (ar) -> ar.equals(sourceRecord) @@ -1480,27 +1512,26 @@ public class BackgroundActivityStartController { @Nullable ActivityRecord sourceRecord, @NonNull ActivityRecord targetRecord, @Nullable Task targetTask, @Nullable ActivityRecord targetTopActivity, int realCallingUid, @BalCode int balCode, - boolean blockActivityStartAndFeatureEnabled, boolean taskToFront) { + boolean blockActivityStartAndFeatureEnabled, boolean taskToFront, + boolean avoidMoveTaskToFront) { final String prefix = "[ASM] "; Function<ActivityRecord, String> recordToString = (ar) -> { if (ar == null) { return null; } - return (ar == sourceRecord ? " [source]=> " + + return (ar == sourceRecord ? " [source]=> " : ar == targetTopActivity ? " [ top ]=> " - : ar == targetRecord ? " [target]=> " - : " => ") - + ar - + " :: visible=" + ar.isVisible() - + ", finishing=" + ar.isFinishing() - + ", alwaysOnTop=" + ar.isAlwaysOnTop() - + ", taskFragment=" + ar.getTaskFragment(); + : ar == targetRecord ? " [target]=> " + : " => ") + + getDebugStringForActivityRecord(ar); }; StringJoiner joiner = new StringJoiner("\n"); joiner.add(prefix + "------ Activity Security " + action + " Debug Logging Start ------"); joiner.add(prefix + "Block Enabled: " + blockActivityStartAndFeatureEnabled); joiner.add(prefix + "ASM Version: " + ActivitySecurityModelFeatureFlags.ASM_VERSION); + joiner.add(prefix + "System Time: " + SystemClock.uptimeMillis()); boolean targetTaskMatchesSourceTask = targetTask != null && sourceRecord != null && sourceRecord.getTask() == targetTask; @@ -1512,6 +1543,8 @@ public class BackgroundActivityStartController { joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage); } else { joiner.add(prefix + "Source Record: " + recordToString.apply(sourceRecord)); + joiner.add(prefix + "Source Launch Package: " + sourceRecord.launchedFromPackage); + joiner.add(prefix + "Source Launch Intent: " + sourceRecord.intent); if (targetTaskMatchesSourceTask) { joiner.add(prefix + "Source/Target Task: " + sourceRecord.getTask()); joiner.add(prefix + "Source/Target Task Stack: "); @@ -1536,7 +1569,30 @@ public class BackgroundActivityStartController { joiner.add(prefix + "Target Record: " + recordToString.apply(targetRecord)); joiner.add(prefix + "Intent: " + targetRecord.intent); joiner.add(prefix + "TaskToFront: " + taskToFront); + joiner.add(prefix + "AvoidMoveToFront: " + avoidMoveTaskToFront); joiner.add(prefix + "BalCode: " + balCodeToString(balCode)); + joiner.add(prefix + "LastResumedActivity: " + + recordToString.apply(mService.mLastResumedActivity)); + + if (mTopFinishedActivity != null) { + joiner.add(prefix + "TopFinishedActivity: " + mTopFinishedActivity.mDebugInfo); + } + + if (!mTaskIdToFinishedActivity.isEmpty()) { + joiner.add(prefix + "TaskIdToFinishedActivity: "); + mTaskIdToFinishedActivity.values().forEach( + (fae) -> joiner.add(prefix + " " + fae.mDebugInfo)); + } + + if (balCode == BAL_ALLOW_VISIBLE_WINDOW || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW + || balCode == BAL_ALLOW_FOREGROUND) { + Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask; + if (task != null && task.getDisplayArea() != null) { + joiner.add(prefix + "Tasks: "); + task.getDisplayArea().forAllTasks((Consumer<Task>) + t -> joiner.add(prefix + " T: " + t.toFullString())); + } + } joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------"); return joiner.toString(); @@ -1620,7 +1676,7 @@ public class BackgroundActivityStartController { return; } - if (!finishActivity.mVisibleRequested + if (!finishActivity.isVisibleRequested() && finishActivity != finishActivity.getTask().getTopMostActivity()) { return; } @@ -1666,10 +1722,22 @@ public class BackgroundActivityStartController { } } + private static String getDebugStringForActivityRecord(ActivityRecord ar) { + return ar + + " :: visible=" + ar.isVisible() + + ", visibleRequested=" + ar.isVisibleRequested() + + ", finishing=" + ar.finishing + + ", alwaysOnTop=" + ar.isAlwaysOnTop() + + ", lastLaunchTime=" + ar.lastLaunchTime + + ", lastVisibleTime=" + ar.lastVisibleTime + + ", taskFragment=" + ar.getTaskFragment(); + } + private class FinishedActivityEntry { int mUid; int mTaskId; int mLaunchCount; + String mDebugInfo; FinishedActivityEntry(ActivityRecord ar) { FinishedActivityEntry entry = mTaskIdToFinishedActivity.get(ar.getTask().mTaskId); @@ -1677,6 +1745,7 @@ public class BackgroundActivityStartController { this.mUid = ar.getUid(); this.mTaskId = taskId; this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1; + this.mDebugInfo = getDebugStringForActivityRecord(ar); mService.mH.postDelayed(() -> { synchronized (mService.mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 63ca5929e34d..e2bc59bb6550 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -794,6 +794,9 @@ public class DisplayPolicy { } mService.mAtmService.mKeyguardController.updateDeferTransitionForAod( mAwake /* waiting */); + if (!awake) { + mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + } } } @@ -836,7 +839,8 @@ public class DisplayPolicy { mRemoteInsetsControllerControlsSystemBars = remoteInsetsControllerControlsSystemBars; } - public void screenTurnedOn(ScreenOnListener screenOnListener) { + /** Prepares to turn on screen. The given listener is used to notify that it is ready. */ + public void screenTurningOn(ScreenOnListener screenOnListener) { WindowProcessController visibleDozeUiProcess = null; synchronized (mLock) { mScreenOnEarly = true; @@ -858,6 +862,11 @@ public class DisplayPolicy { } } + /** It is called after {@link #finishScreenTurningOn}. This runs on PowerManager's thread. */ + public void screenTurnedOn() { + mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + } + public void screenTurnedOff() { synchronized (mLock) { mScreenOnEarly = false; diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index fcc1e5b62221..f2796895d639 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -148,7 +148,7 @@ import java.util.function.Predicate; final class LetterboxUiController { private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE = - activityRecord -> activityRecord.fillsParent() && !activityRecord.isFinishing(); + ActivityRecord::occludesParent; private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 02b3f15979ce..587cc7489763 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2783,6 +2783,9 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } else { throw new RuntimeException("Create the same sleep token twice: " + token); } + if (isSwappingDisplay) { + display.mWallpaperController.onDisplaySwitchStarted(); + } return token; } diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index d68f932400a2..0fc62a758c5e 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; @@ -120,6 +121,11 @@ class WallpaperController { private boolean mShouldOffsetWallpaperCenter; + /** + * Whether the wallpaper has been notified about a physical display switch event is started. + */ + private volatile boolean mIsWallpaperNotifiedOnDisplaySwitch; + private final Consumer<WindowState> mFindWallpapers = w -> { if (w.mAttrs.type == TYPE_WALLPAPER) { WallpaperWindowToken token = w.mToken.asWallpaperToken(); @@ -1083,6 +1089,52 @@ class WallpaperController { } /** + * Notifies the wallpaper that the display turns off when switching physical device. If the + * wallpaper is currently visible, its client visibility will be preserved until the display is + * confirmed to be off or on. + */ + void onDisplaySwitchStarted() { + mIsWallpaperNotifiedOnDisplaySwitch = notifyDisplaySwitch(true /* start */); + } + + /** + * Called when the screen has finished turning on or the device goes to sleep. This is no-op if + * the operation is not part of a display switch. + */ + void onDisplaySwitchFinished() { + // The method can be called outside WM lock (turned on), so only acquire lock if needed. + // This is to optimize the common cases that regular devices don't have display switch. + if (mIsWallpaperNotifiedOnDisplaySwitch) { + synchronized (mService.mGlobalLock) { + mIsWallpaperNotifiedOnDisplaySwitch = false; + notifyDisplaySwitch(false /* start */); + } + } + } + + private boolean notifyDisplaySwitch(boolean start) { + boolean notified = false; + for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) { + final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx); + for (int i = token.getChildCount() - 1; i >= 0; i--) { + final WindowState w = token.getChildAt(i); + if (start && !w.mWinAnimator.getShown()) { + continue; + } + try { + w.mClient.dispatchWallpaperCommand(COMMAND_DISPLAY_SWITCH, 0 /* x */, 0 /* y */, + start ? 1 : 0 /* use z as start or finish */, + null /* bundle */, false /* sync */); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to dispatch COMMAND_DISPLAY_SWITCH " + e); + } + notified = true; + } + } + return notified; + } + + /** * Each window can request a zoom, example: * - User is in overview, zoomed out. * - User also pulls down the shade. diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index f8ac8da710c8..9650b8bc2281 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9156,55 +9156,63 @@ public class WindowManagerService extends IWindowManager.Stub if (fromWin == null || !fromWin.isFocused()) { return false; } - final TaskFragment fromFragment = fromWin.getTaskFragment(); - if (fromFragment == null) { - return false; - } - final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); - if (adjacentFragment == null || adjacentFragment.asTask() != null) { - // Don't move the focus to another task. - return false; - } - final Rect fromBounds = fromFragment.getBounds(); - final Rect adjacentBounds = adjacentFragment.getBounds(); - switch (direction) { - case View.FOCUS_LEFT: - if (adjacentBounds.left >= fromBounds.left) { - return false; - } - break; - case View.FOCUS_UP: - if (adjacentBounds.top >= fromBounds.top) { - return false; - } - break; - case View.FOCUS_RIGHT: - if (adjacentBounds.right <= fromBounds.right) { - return false; - } - break; - case View.FOCUS_DOWN: - if (adjacentBounds.bottom <= fromBounds.bottom) { - return false; - } - break; - case View.FOCUS_BACKWARD: - case View.FOCUS_FORWARD: - // These are not absolute directions. Skip checking the bounds. - break; - default: + return moveFocusToAdjacentWindow(fromWin, direction); + } + } + + boolean moveFocusToAdjacentWindow(WindowState fromWin, @FocusDirection int direction) { + final TaskFragment fromFragment = fromWin.getTaskFragment(); + if (fromFragment == null) { + return false; + } + final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); + if (adjacentFragment == null || adjacentFragment.asTask() != null) { + // Don't move the focus to another task. + return false; + } + if (adjacentFragment.isIsolatedNav()) { + // Don't move the focus if the adjacent TF is isolated navigation. + return false; + } + final Rect fromBounds = fromFragment.getBounds(); + final Rect adjacentBounds = adjacentFragment.getBounds(); + switch (direction) { + case View.FOCUS_LEFT: + if (adjacentBounds.left >= fromBounds.left) { return false; - } - final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity( - true /* focusableOnly */); - if (topRunningActivity == null) { - return false; - } - moveDisplayToTopInternal(topRunningActivity.getDisplayId()); - handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); - if (fromWin.isFocused()) { + } + break; + case View.FOCUS_UP: + if (adjacentBounds.top >= fromBounds.top) { + return false; + } + break; + case View.FOCUS_RIGHT: + if (adjacentBounds.right <= fromBounds.right) { + return false; + } + break; + case View.FOCUS_DOWN: + if (adjacentBounds.bottom <= fromBounds.bottom) { + return false; + } + break; + case View.FOCUS_BACKWARD: + case View.FOCUS_FORWARD: + // These are not absolute directions. Skip checking the bounds. + break; + default: return false; - } + } + final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity( + true /* focusableOnly */); + if (topRunningActivity == null) { + return false; + } + moveDisplayToTopInternal(topRunningActivity.getDisplayId()); + handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); + if (fromWin.isFocused()) { + return false; } return true; } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 1c90e30c256e..2049331e8efe 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -46,6 +46,7 @@ #include <com_android_input_flags.h> #include <input/Input.h> #include <input/PointerController.h> +#include <input/PrintTools.h> #include <input/SpriteController.h> #include <inputflinger/InputManager.h> #include <limits.h> @@ -230,10 +231,6 @@ inline static T max(const T& a, const T& b) { return a > b ? a : b; } -static inline const char* toString(bool value) { - return value ? "true" : "false"; -} - static SpriteIcon toSpriteIcon(PointerIcon pointerIcon) { // As a minor optimization, do not make a copy of the PointerIcon bitmap here. The loaded // PointerIcons are only cached by InputManagerService in java, so we can safely assume they @@ -288,7 +285,7 @@ public: void setSystemUiLightsOut(bool lightsOut); void setPointerDisplayId(int32_t displayId); void setPointerSpeed(int32_t speed); - void setMousePointerAccelerationEnabled(bool enabled); + void setMousePointerAccelerationEnabled(int32_t displayId, bool enabled); void setTouchpadPointerSpeed(int32_t speed); void setTouchpadNaturalScrollingEnabled(bool enabled); void setTouchpadTapToClickEnabled(bool enabled); @@ -304,6 +301,7 @@ public: bool setPointerIcon(std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon, int32_t displayId, DeviceId deviceId, int32_t pointerId, const sp<IBinder>& inputToken); + void setPointerIconVisibility(int32_t displayId, bool visible); void setMotionClassifierEnabled(bool enabled); std::optional<std::string> getBluetoothAddress(int32_t deviceId); void setStylusButtonMotionEventsEnabled(bool enabled); @@ -400,8 +398,8 @@ private: // Pointer speed. int32_t pointerSpeed{0}; - // True if pointer acceleration is enabled for mice. - bool mousePointerAccelerationEnabled{true}; + // Displays on which its associated mice will have pointer acceleration disabled. + std::set<int32_t> displaysWithMousePointerAccelerationDisabled{}; // True if pointer gestures are enabled. bool pointerGesturesEnabled{true}; @@ -492,8 +490,8 @@ void NativeInputManager::dump(std::string& dump) { dump += StringPrintf(INDENT "System UI Lights Out: %s\n", toString(mLocked.systemUiLightsOut)); dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed); - dump += StringPrintf(INDENT "Mouse Pointer Acceleration: %s\n", - mLocked.mousePointerAccelerationEnabled ? "Enabled" : "Disabled"); + dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n", + dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled).c_str()); dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n", toString(mLocked.pointerGesturesEnabled)); dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches)); @@ -676,11 +674,13 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon std::scoped_lock _l(mLock); outConfig->mousePointerSpeed = mLocked.pointerSpeed; - outConfig->mousePointerAccelerationEnabled = mLocked.mousePointerAccelerationEnabled; - outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed - * POINTER_SPEED_EXPONENT); + outConfig->displaysWithMousePointerAccelerationDisabled = + mLocked.displaysWithMousePointerAccelerationDisabled; + outConfig->pointerVelocityControlParameters.scale = + exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT); outConfig->pointerVelocityControlParameters.acceleration = - mLocked.mousePointerAccelerationEnabled + mLocked.displaysWithMousePointerAccelerationDisabled.count( + mLocked.pointerDisplayId) == 0 ? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION : 1; outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled; @@ -1224,16 +1224,23 @@ void NativeInputManager::setPointerSpeed(int32_t speed) { InputReaderConfiguration::Change::POINTER_SPEED); } -void NativeInputManager::setMousePointerAccelerationEnabled(bool enabled) { +void NativeInputManager::setMousePointerAccelerationEnabled(int32_t displayId, bool enabled) { { // acquire lock std::scoped_lock _l(mLock); - if (mLocked.mousePointerAccelerationEnabled == enabled) { + const bool oldEnabled = + mLocked.displaysWithMousePointerAccelerationDisabled.count(displayId) == 0; + if (oldEnabled == enabled) { return; } - ALOGI("Setting mouse pointer acceleration to %s", toString(enabled)); - mLocked.mousePointerAccelerationEnabled = enabled; + ALOGI("Setting mouse pointer acceleration to %s on display %d", toString(enabled), + displayId); + if (enabled) { + mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId); + } else { + mLocked.displaysWithMousePointerAccelerationDisabled.emplace(displayId); + } } // release lock mInputManager->getReader().requestRefreshConfiguration( @@ -1397,6 +1404,13 @@ bool NativeInputManager::setPointerIcon( return mInputManager->getChoreographer().setPointerIcon(std::move(icon), displayId, deviceId); } +void NativeInputManager::setPointerIconVisibility(int32_t displayId, bool visible) { + if (!ENABLE_POINTER_CHOREOGRAPHER) { + return; + } + mInputManager->getChoreographer().setPointerIconVisibility(displayId, visible); +} + TouchAffineTransformation NativeInputManager::getTouchAffineTransformation( JNIEnv *env, jfloatArray matrixArr) { ATRACE_CALL(); @@ -2168,10 +2182,10 @@ static void nativeSetPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed } static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj, - jboolean enabled) { + jint displayId, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - im->setMousePointerAccelerationEnabled(enabled); + im->setMousePointerAccelerationEnabled(displayId, enabled); } static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) { @@ -2550,6 +2564,13 @@ static bool nativeSetPointerIcon(JNIEnv* env, jobject nativeImplObj, jobject ico ibinderForJavaObject(env, inputTokenObj)); } +static void nativeSetPointerIconVisibility(JNIEnv* env, jobject nativeImplObj, jint displayId, + jboolean visible) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + + im->setPointerIconVisibility(displayId, visible); +} + static jboolean nativeCanDispatchToDisplay(JNIEnv* env, jobject nativeImplObj, jint deviceId, jint displayId) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2791,7 +2812,7 @@ static const JNINativeMethod gInputManagerMethods[] = { (void*)nativeTransferTouchFocus}, {"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouch}, {"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed}, - {"setMousePointerAccelerationEnabled", "(Z)V", + {"setMousePointerAccelerationEnabled", "(IZ)V", (void*)nativeSetMousePointerAccelerationEnabled}, {"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed}, {"setTouchpadNaturalScrollingEnabled", "(Z)V", @@ -2828,6 +2849,7 @@ static const JNINativeMethod gInputManagerMethods[] = { (void*)nativeSetCustomPointerIcon}, {"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z", (void*)nativeSetPointerIcon}, + {"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility}, {"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay}, {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged}, {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation}, diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp index 11c40d7bcd9b..9c033e25c04e 100644 --- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp +++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp @@ -675,7 +675,8 @@ static jboolean android_location_gnss_hal_GnssNative_start_measurement_collectio options.enableCorrVecOutputs = enableCorrVecOutputs; options.intervalMs = intervalMs; - return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>(), + return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>( + gnssMeasurementIface->getInterfaceVersion()), options); } diff --git a/services/core/jni/gnss/Gnss.cpp b/services/core/jni/gnss/Gnss.cpp index 8934c3a6abde..da8928b5f97f 100644 --- a/services/core/jni/gnss/Gnss.cpp +++ b/services/core/jni/gnss/Gnss.cpp @@ -196,7 +196,8 @@ void GnssHal::linkToDeath() { jboolean GnssHal::setCallback() { if (gnssHalAidl != nullptr) { - sp<IGnssCallbackAidl> gnssCbIfaceAidl = new GnssCallbackAidl(); + sp<IGnssCallbackAidl> gnssCbIfaceAidl = + new GnssCallbackAidl(gnssHalAidl->getInterfaceVersion()); auto status = gnssHalAidl->setCallback(gnssCbIfaceAidl); if (!checkAidlStatus(status, "IGnssAidl setCallback() failed.")) { return JNI_FALSE; diff --git a/services/core/jni/gnss/GnssCallback.cpp b/services/core/jni/gnss/GnssCallback.cpp index 60eed8e6d716..3d598f7a7203 100644 --- a/services/core/jni/gnss/GnssCallback.cpp +++ b/services/core/jni/gnss/GnssCallback.cpp @@ -120,7 +120,7 @@ void Gnss_class_init_once(JNIEnv* env, jclass& clazz) { Status GnssCallbackAidl::gnssSetCapabilitiesCb(const int capabilities) { ALOGD("%s: %du\n", __func__, capabilities); - bool isAdrCapabilityKnown = (getInterfaceVersion() >= 3) ? true : false; + bool isAdrCapabilityKnown = (interfaceVersion >= 3) ? true : false; JNIEnv* env = getJniEnv(); env->CallVoidMethod(mCallbacksObj, method_setTopHalCapabilities, capabilities, isAdrCapabilityKnown); @@ -178,7 +178,7 @@ Status GnssCallbackAidl::gnssLocationCb(const hardware::gnss::GnssLocation& loca Status GnssCallbackAidl::gnssNmeaCb(const int64_t timestamp, const std::string& nmea) { // In AIDL v1, if no listener is registered, do not report nmea to the framework. - if (getInterfaceVersion() <= 1) { + if (interfaceVersion <= 1) { if (!isNmeaRegistered) { return Status::ok(); } diff --git a/services/core/jni/gnss/GnssCallback.h b/services/core/jni/gnss/GnssCallback.h index 33acec8b5660..0622e533e5b5 100644 --- a/services/core/jni/gnss/GnssCallback.h +++ b/services/core/jni/gnss/GnssCallback.h @@ -60,6 +60,7 @@ void Gnss_class_init_once(JNIEnv* env, jclass& clazz); */ class GnssCallbackAidl : public hardware::gnss::BnGnssCallback { public: + GnssCallbackAidl(int version) : interfaceVersion(version){}; binder::Status gnssSetCapabilitiesCb(const int capabilities) override; binder::Status gnssSetSignalTypeCapabilitiesCb( const std::vector<android::hardware::gnss::GnssSignalType>& signalTypes) override; @@ -73,6 +74,9 @@ public: binder::Status gnssRequestTimeCb() override; binder::Status gnssRequestLocationCb(const bool independentFromGnss, const bool isUserEmergency) override; + +private: + const int interfaceVersion; }; /* diff --git a/services/core/jni/gnss/GnssMeasurement.h b/services/core/jni/gnss/GnssMeasurement.h index 7a95db8ed7b6..20400fd8f587 100644 --- a/services/core/jni/gnss/GnssMeasurement.h +++ b/services/core/jni/gnss/GnssMeasurement.h @@ -41,6 +41,7 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) = 0; virtual jboolean close() = 0; + virtual int getInterfaceVersion() = 0; }; class GnssMeasurement : public GnssMeasurementInterface { @@ -50,6 +51,9 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override; jboolean close() override; + int getInterfaceVersion() override { + return mIGnssMeasurement->getInterfaceVersion(); + } private: const sp<android::hardware::gnss::IGnssMeasurementInterface> mIGnssMeasurement; @@ -63,6 +67,9 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override; jboolean close() override; + int getInterfaceVersion() override { + return 0; + } private: const sp<android::hardware::gnss::V1_0::IGnssMeasurement> mIGnssMeasurement_V1_0; diff --git a/services/core/jni/gnss/GnssMeasurementCallback.cpp b/services/core/jni/gnss/GnssMeasurementCallback.cpp index 2982546bfa47..ebab4c342ca0 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.cpp +++ b/services/core/jni/gnss/GnssMeasurementCallback.cpp @@ -392,7 +392,7 @@ void GnssMeasurementCallbackAidl::translateAndSetGnssData(const GnssData& data) jobjectArray gnssAgcArray = nullptr; gnssAgcArray = translateAllGnssAgcs(env, data.gnssAgcs); - if (this->getInterfaceVersion() >= 3) { + if (interfaceVersion >= 3) { setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray, /*hasIsFullTracking=*/true, data.isFullTracking); } else { @@ -467,7 +467,7 @@ void GnssMeasurementCallbackAidl::translateSingleGnssMeasurement(JNIEnv* env, satellitePvt.tropoDelayMeters); } - if (this->getInterfaceVersion() >= 2) { + if (interfaceVersion >= 2) { callObjectMethodIgnoringResult(env, satellitePvtBuilderObject, method_satellitePvtBuilderSetTimeOfClock, satellitePvt.timeOfClockSeconds); diff --git a/services/core/jni/gnss/GnssMeasurementCallback.h b/services/core/jni/gnss/GnssMeasurementCallback.h index b3de486e6fa9..3cb47ce2fe18 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.h +++ b/services/core/jni/gnss/GnssMeasurementCallback.h @@ -53,7 +53,8 @@ void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock, class GnssMeasurementCallbackAidl : public hardware::gnss::BnGnssMeasurementCallback { public: - GnssMeasurementCallbackAidl() : mCallbacksObj(getCallbacksObj()) {} + GnssMeasurementCallbackAidl(int version) + : mCallbacksObj(getCallbacksObj()), interfaceVersion(version) {} android::binder::Status gnssMeasurementCb(const hardware::gnss::GnssData& data) override; private: @@ -71,6 +72,7 @@ private: void translateGnssClock(JNIEnv* env, const hardware::gnss::GnssData& data, JavaObject& object); jobject& mCallbacksObj; + const int interfaceVersion; }; /* @@ -110,10 +112,10 @@ private: class GnssMeasurementCallback { public: - GnssMeasurementCallback() {} + GnssMeasurementCallback(int version) : interfaceVersion(version) {} sp<GnssMeasurementCallbackAidl> getAidl() { if (callbackAidl == nullptr) { - callbackAidl = sp<GnssMeasurementCallbackAidl>::make(); + callbackAidl = sp<GnssMeasurementCallbackAidl>::make(interfaceVersion); } return callbackAidl; } @@ -128,6 +130,7 @@ public: private: sp<GnssMeasurementCallbackAidl> callbackAidl; sp<GnssMeasurementCallbackHidl> callbackHidl; + const int interfaceVersion; }; template <class T> diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java index 532823ad8367..e8c5658ca941 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java @@ -17,6 +17,7 @@ package com.android.server.devicepolicy; import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK; +import static android.app.admin.flags.Flags.defaultSmsPersonalAppSuspensionFixEnabled; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.Nullable; @@ -42,6 +43,7 @@ import android.view.accessibility.IAccessibilityManager; import android.view.inputmethod.InputMethodInfo; import com.android.internal.R; +import com.android.internal.telephony.SmsApplication; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.utils.Slogf; @@ -97,7 +99,7 @@ public final class PersonalAppsSuspensionHelper { result.removeAll(getSystemLauncherPackages()); result.removeAll(getAccessibilityServices()); result.removeAll(getInputMethodPackages()); - result.remove(Telephony.Sms.getDefaultSmsPackage(mContext)); + result.remove(getDefaultSmsPackage()); result.remove(getSettingsPackageName()); final String[] unsuspendablePackages = @@ -202,6 +204,17 @@ public final class PersonalAppsSuspensionHelper { return resolveInfos != null && !resolveInfos.isEmpty(); } + private String getDefaultSmsPackage() { + //TODO(b/319449037): Unflag the following change. + if (defaultSmsPersonalAppSuspensionFixEnabled()) { + return SmsApplication.getDefaultSmsApplicationAsUser( + mContext, /*updateIfNeeded=*/ false, mContext.getUser()) + .getPackageName(); + } else { + return Telephony.Sms.getDefaultSmsPackage(mContext); + } + } + void dump(IndentingPrintWriter pw) { pw.println("PersonalAppsSuspensionHelper"); @@ -212,7 +225,7 @@ public final class PersonalAppsSuspensionHelper { DevicePolicyManagerService.dumpApps(pw, "accessibility services", getAccessibilityServices()); DevicePolicyManagerService.dumpApps(pw, "input method packages", getInputMethodPackages()); - pw.printf("SMS package: %s\n", Telephony.Sms.getDefaultSmsPackage(mContext)); + pw.printf("SMS package: %s\n", getDefaultSmsPackage()); pw.printf("Settings package: %s\n", getSettingsPackageName()); DevicePolicyManagerService.dumpApps(pw, "Packages subject to suspension", getPersonalAppsForSuspension()); diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 62d2d7ee848a..4c74878dab70 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -22,6 +22,7 @@ import android.content.pm.PermissionGroupInfo import android.content.pm.PermissionInfo import android.content.pm.SigningDetails import android.os.Build +import android.permission.flags.Flags import android.util.Slog import com.android.internal.os.RoSystemProperties import com.android.internal.pm.permission.CompatibilityPermissionInfo @@ -1197,15 +1198,80 @@ class AppIdPermissionPolicy : SchemePolicy() { newState.externalState.packageStates[PLATFORM_PACKAGE_NAME]!! .androidPackage!! .signingDetails - return sourceSigningDetails?.hasCommonSignerWithCapability( - packageSigningDetails, - SigningDetails.CertCapabilities.PERMISSION - ) == true || - packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) || - platformSigningDetails.checkCapability( + val hasCommonSigner = + sourceSigningDetails?.hasCommonSignerWithCapability( packageSigningDetails, SigningDetails.CertCapabilities.PERMISSION - ) + ) == true || + packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) || + platformSigningDetails.checkCapability( + packageSigningDetails, + SigningDetails.CertCapabilities.PERMISSION + ) + if (!Flags.signaturePermissionAllowlistEnabled()) { + return hasCommonSigner; + } + if (!hasCommonSigner) { + return false + } + // A platform signature permission also needs to be allowlisted on non-debuggable builds. + if (permission.packageName == PLATFORM_PACKAGE_NAME) { + val isRequestedByFactoryApp = + if (packageState.isSystem) { + // For updated system applications, a signature permission still needs to be + // allowlisted if it wasn't requested by the original application. + if (packageState.isUpdatedSystemApp) { + val disabledSystemPackage = + newState.externalState.disabledSystemPackageStates[ + packageState.packageName] + ?.androidPackage + disabledSystemPackage != null && + permission.name in disabledSystemPackage.requestedPermissions + } else { + true + } + } else { + false + } + if ( + !(isRequestedByFactoryApp || + getSignaturePermissionAllowlistState(packageState, permission.name) == true) + ) { + Slog.w( + LOG_TAG, + "Signature permission ${permission.name} for package" + + " ${packageState.packageName} (${packageState.path}) not in" + + " signature permission allowlist" + ) + if (!Build.isDebuggable()) { + return false + } + } + } + return true + } + + private fun MutateStateScope.getSignaturePermissionAllowlistState( + packageState: PackageState, + permissionName: String + ): Boolean? { + val permissionAllowlist = newState.externalState.permissionAllowlist + val packageName = packageState.packageName + return when { + packageState.isVendor || packageState.isOdm -> + permissionAllowlist.getVendorSignatureAppAllowlistState(packageName, permissionName) + packageState.isProduct -> + permissionAllowlist.getProductSignatureAppAllowlistState( + packageName, + permissionName + ) + packageState.isSystemExt -> + permissionAllowlist.getSystemExtSignatureAppAllowlistState( + packageName, + permissionName + ) + else -> permissionAllowlist.getSignatureAppAllowlistState(packageName, permissionName) + } } private fun MutateStateScope.checkPrivilegedPermissionAllowlist( 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 f469ab547763..097d73a9a05b 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 @@ -960,8 +960,8 @@ class PermissionService(private val service: AccessCheckingService) : if (permissionName !in androidPackage.requestedPermissions && oldFlags == 0) { if (reportError) { - throw SecurityException( - "Permission $permissionName isn't requested by package $packageName" + Slog.e( + LOG_TAG, "Permission $permissionName isn't requested by package $packageName" ) } return diff --git a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp index 4fcdbfc21f6c..d479e52f92d8 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/Android.bp +++ b/services/tests/BackgroundInstallControlServiceTests/host/Android.bp @@ -11,7 +11,6 @@ // 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 { // See: http://go/android-license-faq // A large-scale-change added 'default_applicable_licenses' to import diff --git a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java index 74506076d82f..c99e7129853b 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java +++ b/services/tests/BackgroundInstallControlServiceTests/host/src/com/android/server/pm/test/BackgroundInstallControlServiceHostTest.java @@ -41,17 +41,26 @@ public final class BackgroundInstallControlServiceHostTest extends BaseHostJUnit private static final String MOCK_APK_FILE_1 = "BackgroundInstallControlMockApp1.apk"; private static final String MOCK_APK_FILE_2 = "BackgroundInstallControlMockApp2.apk"; + // TODO: Move the silent installs to test-app using {@link + // BackgroundInstallControlServiceTest#installPackage(String, String)} and remove deviceConfig + // branch in BICS. + // b/310983905 @Test public void testGetMockBackgroundInstalledPackages() throws Exception { - installPackage(TEST_DATA_DIR + MOCK_APK_FILE_1); + installPackage(TEST_DATA_DIR + MOCK_APK_FILE_1); installPackage(TEST_DATA_DIR + MOCK_APK_FILE_2); assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_1)).isNotNull(); assertThat(getDevice().getAppPackageInfo(MOCK_PACKAGE_NAME_2)).isNotNull(); - assertThat(getDevice().setProperty("debug.transparency.bg-install-apps", - MOCK_PACKAGE_NAME_1 + "," + MOCK_PACKAGE_NAME_2)).isTrue(); - runDeviceTest("testGetMockBackgroundInstalledPackages"); + assertThat( + getDevice() + .setProperty( + "debug.transparency.bg-install-apps", + MOCK_PACKAGE_NAME_1 + "," + MOCK_PACKAGE_NAME_2)) + .isTrue(); + runDeviceTest( + "BackgroundInstallControlServiceTest", "testGetMockBackgroundInstalledPackages"); assertThat(getDevice().uninstallPackage(MOCK_PACKAGE_NAME_1)).isNull(); assertThat(getDevice().uninstallPackage(MOCK_PACKAGE_NAME_2)).isNull(); @@ -65,10 +74,10 @@ public final class BackgroundInstallControlServiceHostTest extends BaseHostJUnit assertThat(result.getStatus() == CommandStatus.SUCCESS).isTrue(); } - private void runDeviceTest(String method) throws DeviceNotAvailableException { + private void runDeviceTest(String testName, String method) throws DeviceNotAvailableException { var options = new DeviceTestRunOptions(PACKAGE_NAME); - options.setTestClassName(PACKAGE_NAME + ".BackgroundInstallControlServiceTest"); + options.setTestClassName(PACKAGE_NAME + "." + testName); options.setTestMethodName(method); runDeviceTests(options); } -} +}
\ No newline at end of file diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml index 1fa1f84cd04e..cbe58a8fec70 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml +++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/AndroidManifest.xml @@ -24,4 +24,4 @@ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:label="APCT tests for background install control service" android:targetPackage="com.android.server.pm.test.app" /> -</manifest> +</manifest>
\ No newline at end of file diff --git a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java index b74e5619fd0c..b23f59106881 100644 --- a/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java +++ b/services/tests/BackgroundInstallControlServiceTests/host/test-app/BackgroundInstallControlServiceTestApp/src/com/android/server/pm/test/app/BackgroundInstallControlServiceTest.java @@ -16,6 +16,10 @@ package com.android.server.pm.test.app; +import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; + +import static com.android.compatibility.common.util.SystemUtil.runShellCommand; + import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -23,12 +27,15 @@ import android.content.pm.IBackgroundInstallControlService; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; +import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; -import android.os.UserHandle; import androidx.test.runner.AndroidJUnit4; +import com.android.compatibility.common.util.ShellIdentityUtils; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,31 +46,52 @@ import java.util.stream.Collectors; @RunWith(AndroidJUnit4.class) public class BackgroundInstallControlServiceTest { private static final String TAG = "BackgroundInstallControlServiceTest"; + private static final String MOCK_PACKAGE_NAME = "com.android.servicestests.apps.bicmockapp3"; private IBackgroundInstallControlService mIBics; @Before public void setUp() { - mIBics = IBackgroundInstallControlService.Stub.asInterface( - ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)); + mIBics = + IBackgroundInstallControlService.Stub.asInterface( + ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)); assertThat(mIBics).isNotNull(); } + @After + public void tearDown() { + runShellCommand("pm uninstall " + MOCK_PACKAGE_NAME); + } + @Test public void testGetMockBackgroundInstalledPackages() throws RemoteException { - ParceledListSlice<PackageInfo> slice = mIBics.getBackgroundInstalledPackages( - PackageManager.MATCH_ALL, - UserHandle.USER_ALL); + ParceledListSlice<PackageInfo> slice = + ShellIdentityUtils.invokeMethodWithShellPermissions( + mIBics, + (bics) -> { + try { + return bics.getBackgroundInstalledPackages( + PackageManager.MATCH_ALL, Process.myUserHandle() + .getIdentifier()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + }, + GET_BACKGROUND_INSTALLED_PACKAGES); assertThat(slice).isNotNull(); var packageList = slice.getList(); assertThat(packageList).isNotNull(); assertThat(packageList).hasSize(2); - var expectedPackageNames = Set.of("com.android.servicestests.apps.bicmockapp1", - "com.android.servicestests.apps.bicmockapp2"); - var actualPackageNames = packageList.stream().map((packageInfo) -> packageInfo.packageName) - .collect(Collectors.toSet()); + var expectedPackageNames = + Set.of( + "com.android.servicestests.apps.bicmockapp1", + "com.android.servicestests.apps.bicmockapp2"); + var actualPackageNames = + packageList.stream() + .map((packageInfo) -> packageInfo.packageName) + .collect(Collectors.toSet()); assertThat(actualPackageNames).containsExactlyElementsIn(expectedPackageNames); } -} +}
\ No newline at end of file diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java index d82e6abeb502..0edb3dfc0bc0 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,18 +26,13 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import androidx.test.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; -import org.junit.runner.RunWith; import java.io.File; import java.util.List; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class AdditionalSubtypeUtilsTest { +public final class AdditionalSubtypeUtilsTest { @Test public void testSaveAndLoad() throws Exception { @@ -60,7 +55,7 @@ public class AdditionalSubtypeUtilsTest { // Save & load. AtomicFile atomicFile = new AtomicFile( new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml")); - AdditionalSubtypeUtils.saveToFile(allSubtypes, methodMap, atomicFile); + AdditionalSubtypeUtils.saveToFile(allSubtypes, InputMethodMap.of(methodMap), atomicFile); ArrayMap<String, List<InputMethodSubtype>> loadedSubtypes = new ArrayMap<>(); AdditionalSubtypeUtils.loadFromFile(loadedSubtypes, atomicFile); diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java index 6eedeea8c333..b7223d6615b9 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,17 +19,11 @@ package com.android.server.inputmethod; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - import org.junit.Test; -import org.junit.runner.RunWith; import java.util.Arrays; import java.util.List; -@SmallTest -@RunWith(AndroidJUnit4.class) public final class HardwareKeyboardShortcutControllerTest { @Test diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java index 7cbfc52c5cce..71752ba3b393 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java @@ -28,6 +28,7 @@ import android.content.pm.ResolveInfo; import android.util.ArrayMap; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -123,11 +124,11 @@ public class InputMethodManagerServiceRestrictImeAmountTest extends private List<InputMethodInfo> filterInputMethodServices(List<ResolveInfo> resolveInfoList, List<String> enabledComponents) { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); - InputMethodManagerService.filterInputMethodServices(new ArrayMap<>(), methodMap, methodList, - enabledComponents, mContext, resolveInfoList); - return methodList; + final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap = + new ArrayMap<>(); + final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices( + emptyAdditionalSubtypeMap, enabledComponents, mContext, resolveInfoList); + return methodMap.values(); } private ResolveInfo createFakeSystemResolveInfo(String packageName, String componentName) { diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java index 0884b784ac73..fbe384a62658 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,22 +28,16 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl; import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; import org.junit.Test; -import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class InputMethodSubtypeSwitchingControllerTest { +public final class InputMethodSubtypeSwitchingControllerTest { private static final String DUMMY_PACKAGE_NAME = "dummy package name"; private static final String DUMMY_IME_LABEL = "dummy ime label"; private static final String DUMMY_SETTING_ACTIVITY_NAME = ""; diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index 9688ef6cc83b..2857619c70d3 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,15 +41,12 @@ import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; import androidx.annotation.NonNull; import androidx.test.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.inputmethod.StartInputFlags; import com.google.common.truth.Truth; import org.junit.Test; -import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Collections; @@ -57,9 +54,7 @@ import java.util.List; import java.util.Locale; import java.util.Objects; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class InputMethodUtilsTest { +public final class InputMethodUtilsTest { private static final boolean IS_AUX = true; private static final boolean IS_DEFAULT = true; private static final boolean IS_OVERRIDES_IMPLICITLY_ENABLED_SUBTYPE = true; @@ -274,7 +269,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_US), imi); assertEquals(1, result.size()); verifyEquality(autoSubtype, result.get(0)); @@ -298,7 +293,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_US), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -322,7 +317,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_GB), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnGB, result.get(0)); @@ -347,7 +342,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -368,7 +363,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR_CA), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -390,7 +385,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(3, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -412,7 +407,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoHi, result.get(0)); @@ -429,7 +424,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -446,7 +441,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -468,7 +463,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(Locale.forLanguageTag("sr-Latn-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrLatn, is(in(result))); @@ -488,7 +483,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(Locale.forLanguageTag("sr-Cyrl-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrCyrl, is(in(result))); @@ -514,7 +509,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList( Locale.forLanguageTag("sr-Latn-RS-x-android"), Locale.forLanguageTag("ja-JP"), @@ -541,7 +536,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FIL_PH), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFil, result.get(0)); @@ -559,7 +554,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FI), imi); assertEquals(1, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -575,7 +570,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -589,7 +584,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -603,7 +598,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -617,7 +612,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -639,7 +634,7 @@ public class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi); assertThat(nonAutoFrCA, is(in(result))); assertThat(nonAutoEnUS, is(in(result))); @@ -801,19 +796,22 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, null, "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), + null, "")); } // Returns null when the config value is empty. { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, "", "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), "", + "")); } // Returns null when the configured package doesn't have an IME. { - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.emptyMap(), systemIme.getPackageName(), "")); } @@ -821,7 +819,8 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), null)); } @@ -829,13 +828,15 @@ public class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), "")); } // Returns null when the current default isn't found. { - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.emptyMap(), systemIme.getPackageName(), systemIme.getId())); } @@ -846,7 +847,7 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), systemIme.getPackageName(), "")); } @@ -857,7 +858,8 @@ public class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), systemIme.getId())); } @@ -867,7 +869,7 @@ public class InputMethodUtilsTest { final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme", "fake.voice0", false /* isSystem */); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), nonSystemIme.getPackageName(), nonSystemIme.getId())); } @@ -878,7 +880,7 @@ public class InputMethodUtilsTest { "FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */); methodMap.put(systemIme.getId(), systemIme); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), nonSystemIme.getPackageName(), "")); } } diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java index 01f8129c2cd7..d0b46f58d626 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,18 +22,12 @@ import static org.junit.Assert.assertEquals; import android.os.LocaleList; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - import org.junit.Test; -import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Locale; -@SmallTest -@RunWith(AndroidJUnit4.class) -public class LocaleUtilsTest { +public final class LocaleUtilsTest { private static final LocaleUtils.LocaleExtractor<Locale> sIdentityMapper = source -> source; diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java index 978044045ab3..a185ad99a8f3 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java @@ -419,7 +419,7 @@ public class PackageUserStateTest { "installerTitle"); packageUserState.setArchiveState(archiveState); assertEquals(archiveState, packageUserState.getArchiveState()); - assertTrue(archiveState.getArchiveTimeMillis() > currentTimeMillis); + assertTrue(archiveState.getArchiveTimeMillis() >= currentTimeMillis); } @Test diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java index 4095be74d294..18dc114a8cd1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java @@ -20,11 +20,13 @@ import static com.google.common.truth.Truth.assertThat; import android.annotation.NonNull; import android.app.backup.BackupHelper; +import android.app.backup.BackupHelperWithLogger; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; import static org.mockito.Mockito.when; @@ -32,7 +34,10 @@ import static org.mockito.Mockito.when; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.server.backup.Flags; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -55,6 +60,9 @@ public class SystemBackupAgentTest { @Mock private PackageManager mPackageManagerMock; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -71,7 +79,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "preferred_activities", @@ -96,7 +104,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "preferred_activities", @@ -118,7 +126,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "notifications", @@ -134,7 +142,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "preferred_activities", @@ -147,12 +155,42 @@ public class SystemBackupAgentTest { "companion"); } + @Test + public void onAddHelperIfEligibleForUser_flagIsOff_helpersHaveNoLogger() { + UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); + when(mUserManagerMock.isProfile()).thenReturn(false); + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS); + + mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); + + for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){ + assertThat(helper.isLoggerSet()).isFalse(); + } + } + + @Test + public void onAddHelperIfEligibleForUser_flagIsOn_helpersHaveLogger() { + UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); + when(mUserManagerMock.isProfile()).thenReturn(false); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS); + + mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); + + for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){ + assertThat(helper.isLoggerSet()).isTrue(); + } + } + private class TestableSystemBackupAgent extends SystemBackupAgent { - final Set<String> mAddedHelpers = new ArraySet<>(); + final Set<String> mAddedHelpersKey = new ArraySet<>(); + final Set<BackupHelperWithLogger> mAddedHelpers = new ArraySet<>(); @Override public void addHelper(String keyPrefix, BackupHelper helper) { - mAddedHelpers.add(keyPrefix); + mAddedHelpersKey.add(keyPrefix); + if (helper instanceof BackupHelperWithLogger) { + mAddedHelpers.add((BackupHelperWithLogger) helper); + } } @Override diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java index 284e491f8081..93a2eefba5b1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java @@ -31,13 +31,17 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; +import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS; -import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS; +import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES; import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS; import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_APPLIED_CONSTRAINTS; import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_DEADLINE_PROXIMITY_LIMIT; import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE; -import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS; +import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINES; +import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS; +import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES; +import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING; @@ -76,6 +80,7 @@ import android.os.PowerManager; import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.EmptyArray; +import android.util.SparseArray; import com.android.server.AppSchedulingModuleThread; import com.android.server.DeviceIdleInternal; @@ -96,6 +101,7 @@ import org.mockito.quality.Strictness; import org.mockito.stubbing.Answer; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; import java.util.ArrayList; @@ -182,7 +188,12 @@ public class FlexibilityControllerTest { mSourceUid = AppGlobals.getPackageManager().getPackageUid(SOURCE_PACKAGE, 0, 0); - setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "50,60,70,80"); + setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + "500=50|60|70|80" + + ",400=50|60|70|80" + + ",300=50|60|70|80" + + ",200=50|60|70|80" + + ",100=50|60|70|80"); setDeviceConfigLong(KEY_DEADLINE_PROXIMITY_LIMIT, 0L); setDeviceConfigInt(KEY_APPLIED_CONSTRAINTS, FLEXIBLE_CONSTRAINTS); waitForQuietModuleThread(); @@ -200,6 +211,11 @@ public class FlexibilityControllerTest { } } + private void advanceElapsedClock(long incrementMs) { + JobSchedulerService.sElapsedRealtimeClock = Clock.offset( + sElapsedRealtimeClock, Duration.ofMillis(incrementMs)); + } + private void setDeviceConfigInt(String key, int val) { mDeviceConfigPropertiesBuilder.setInt(key, val); updateDeviceConfig(key); @@ -250,9 +266,12 @@ public class FlexibilityControllerTest { */ @Test public void testDefaultVariableValues() { - assertEquals(Integer.bitCount(FLEXIBLE_CONSTRAINTS), - mFlexibilityController.mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS.length - ); + SparseArray<int[]> defaultPercentsToDrop = + FlexibilityController.FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; + for (int i = 0; i < defaultPercentsToDrop.size(); ++i) { + assertEquals(Integer.bitCount(FLEXIBLE_CONSTRAINTS), + defaultPercentsToDrop.valueAt(i).length); + } } @Test @@ -379,10 +398,13 @@ public class FlexibilityControllerTest { @Test public void testOnConstantsUpdated_FallbackDeadline() { JobStatus js = createJobStatus("testFallbackDeadlineConfig", createJob(0)); - assertEquals(DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0L)); - setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 100L); - assertEquals(100L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0L)); + final long nowElapsed = sElapsedRealtimeClock.millis(); + assertEquals(DEFAULT_FALLBACK_FLEXIBILITY_DEADLINES.get(JobInfo.PRIORITY_DEFAULT), + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0L)); + setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 123L); + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, + "500=500,400=400,300=300,200=200,100=100"); + assertEquals(300L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0L)); } @Test @@ -392,10 +414,32 @@ public class FlexibilityControllerTest { JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb); assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10 * 5, mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); - setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "10,20,30,40"); + setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + "500=1|2|3|4" + + ",400=5|6|7|8" + + ",300=10|20|30|40" + + ",200=50|51|52|53" + + ",100=54|55|56|57"); + assertArrayEquals( + mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .get(JobInfo.PRIORITY_MAX), + new int[]{1, 2, 3, 4}); + assertArrayEquals( + mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .get(JobInfo.PRIORITY_HIGH), + new int[]{5, 6, 7, 8}); + assertArrayEquals( + mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .get(JobInfo.PRIORITY_DEFAULT), + new int[]{10, 20, 30, 40}); assertArrayEquals( - mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, - new int[] {10, 20, 30, 40}); + mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .get(JobInfo.PRIORITY_LOW), + new int[]{50, 51, 52, 53}); + assertArrayEquals( + mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS + .get(JobInfo.PRIORITY_MIN), + new int[]{54, 55, 56, 57}); assertEquals(FROZEN_TIME + MIN_WINDOW_FOR_FLEXIBILITY_MS / 10, mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); js.setNumDroppedFlexibleConstraints(1); @@ -408,25 +452,64 @@ public class FlexibilityControllerTest { @Test public void testOnConstantsUpdated_PercentsToDropConstraintsInvalidValues() { - JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS); - JobStatus js = createJobStatus("testPercentsToDropConstraintsConfig", jb); - js.enqueueTime = JobSchedulerService.sElapsedRealtimeClock.millis(); - assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2, - mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); - setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "10,20a,030,40"); - assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2, - mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); - setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "10,40"); - assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2, - mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); - setDeviceConfigString(KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS, "50,40,10,40"); - assertEquals(js.enqueueTime + HOUR_IN_MILLIS / 2, - mFlexibilityController.getNextConstraintDropTimeElapsedLocked(js)); + // No priority mapping + setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, "10,20,30,40"); + final SparseArray<int[]> defaultPercentsToDrop = + FlexibilityController.FcConfig.DEFAULT_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; + final SparseArray<int[]> percentsToDrop = + mFlexibilityController.mFcConfig.PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS; + for (int i = 0; i < defaultPercentsToDrop.size(); ++i) { + assertArrayEquals(defaultPercentsToDrop.valueAt(i), percentsToDrop.valueAt(i)); + } + + // Invalid priority-percentList string + setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + "500=10,20a,030,40" + + ",400=20|40|60|80" + + ",300=25|50|75|80" + + ",200=40|50|60|80" + + ",100=20|40|60|80"); + for (int i = 0; i < defaultPercentsToDrop.size(); ++i) { + assertArrayEquals(defaultPercentsToDrop.valueAt(i), percentsToDrop.valueAt(i)); + } + + // Invalid percentList strings + setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + "500=10|20a|030|40" // Letters + + ",400=10|40" // Not enough + + ",300=.|50|_|80" // Characters + + ",200=50|40|10|40" // Out of order + + ",100=30|60|90|101"); // Over 100 + for (int i = 0; i < defaultPercentsToDrop.size(); ++i) { + assertArrayEquals(defaultPercentsToDrop.valueAt(i), percentsToDrop.valueAt(i)); + } + + // Only partially invalid + setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + "500=10|20a|030|40" // Letters + + ",400=10|40" // Not enough + + ",300=.|50|_|80" // Characters + + ",200=10|20|30|40" // Valid + + ",100=20|40|60|80"); // Valid + assertArrayEquals(defaultPercentsToDrop.get(JobInfo.PRIORITY_MAX), + percentsToDrop.get(JobInfo.PRIORITY_MAX)); + assertArrayEquals(defaultPercentsToDrop.get(JobInfo.PRIORITY_HIGH), + percentsToDrop.get(JobInfo.PRIORITY_HIGH)); + assertArrayEquals(defaultPercentsToDrop.get(JobInfo.PRIORITY_DEFAULT), + percentsToDrop.get(JobInfo.PRIORITY_DEFAULT)); + assertArrayEquals(new int[]{10, 20, 30, 40}, percentsToDrop.get(JobInfo.PRIORITY_LOW)); + assertArrayEquals(new int[]{20, 40, 60, 80}, percentsToDrop.get(JobInfo.PRIORITY_MIN)); } @Test public void testGetNextConstraintDropTimeElapsedLocked() { setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 200 * HOUR_IN_MILLIS); + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, + "500=" + HOUR_IN_MILLIS + + ",400=" + 25 * HOUR_IN_MILLIS + + ",300=" + 50 * HOUR_IN_MILLIS + + ",200=" + 100 * HOUR_IN_MILLIS + + ",100=" + 200 * HOUR_IN_MILLIS); long nextTimeToDropNumConstraints; @@ -459,34 +542,34 @@ public class FlexibilityControllerTest { nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) / 2, + assertEquals(FROZEN_TIME + 800000L + (50 * HOUR_IN_MILLIS) / 2, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 6 / 10, + assertEquals(FROZEN_TIME + 800000L + (50 * HOUR_IN_MILLIS) * 6 / 10, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 7 / 10, + assertEquals(FROZEN_TIME + 800000L + (50 * HOUR_IN_MILLIS) * 7 / 10, nextTimeToDropNumConstraints); // no delay, no deadline - jb = createJob(0); + jb = createJob(0).setPriority(JobInfo.PRIORITY_LOW); js = createJobStatus("time", jb); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) / 2, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (100 * HOUR_IN_MILLIS) / 2, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 6 / 10, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (100 * HOUR_IN_MILLIS) * 6 / 10, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 7 / 10, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (100 * HOUR_IN_MILLIS) * 7 / 10, nextTimeToDropNumConstraints); // delay, deadline jb = createJob(0) @@ -514,13 +597,13 @@ public class FlexibilityControllerTest { @Test public void testCurPercent() { long deadline = 100 * MINUTE_IN_MILLIS; - long nowElapsed; + long nowElapsed = FROZEN_TIME; JobInfo.Builder jb = createJob(0).setOverrideDeadline(deadline); JobStatus js = createJobStatus("time", jb); assertEquals(FROZEN_TIME, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js)); assertEquals(deadline + FROZEN_TIME, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, FROZEN_TIME)); + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, FROZEN_TIME)); nowElapsed = FROZEN_TIME + 60 * MINUTE_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); @@ -547,7 +630,8 @@ public class FlexibilityControllerTest { assertEquals(FROZEN_TIME + delay, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js)); assertEquals(deadline + FROZEN_TIME, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, FROZEN_TIME + delay)); + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, + FROZEN_TIME + delay)); nowElapsed = FROZEN_TIME + delay + 60 * MINUTE_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = @@ -670,34 +754,63 @@ public class FlexibilityControllerTest { @Test public void testGetLifeCycleEndElapsedLocked_Prefetch() { + final long nowElapsed = sElapsedRealtimeClock.millis(); + // prefetch no estimate JobInfo.Builder jb = createJob(0).setPrefetch(true); JobStatus js = createJobStatus("time", jb); doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js); - assertEquals(Long.MAX_VALUE, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); + assertEquals(Long.MAX_VALUE, + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); + // prefetch with estimate jb = createJob(0).setPrefetch(true); js = createJobStatus("time", jb); doReturn(1000L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js); - assertEquals(1000L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); + assertEquals(1000L, + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); } @Test public void testGetLifeCycleEndElapsedLocked_NonPrefetch() { + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, + "500=" + HOUR_IN_MILLIS + + ",400=" + 2 * HOUR_IN_MILLIS + + ",300=" + 3 * HOUR_IN_MILLIS + + ",200=" + 4 * HOUR_IN_MILLIS + + ",100=" + 5 * HOUR_IN_MILLIS); + + final long nowElapsed = sElapsedRealtimeClock.millis(); + // deadline JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS); JobStatus js = createJobStatus("time", jb); assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); + // no deadline - jb = createJob(0); - js = createJobStatus("time", jb); - assertEquals(FROZEN_TIME + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, 100L)); + assertEquals(FROZEN_TIME + 2 * HOUR_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_HIGH)), + nowElapsed, 100L)); + assertEquals(FROZEN_TIME + 3 * HOUR_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)), + nowElapsed, 100L)); + assertEquals(FROZEN_TIME + 4 * HOUR_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_LOW)), + nowElapsed, 100L)); + assertEquals(FROZEN_TIME + 5 * HOUR_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("time", createJob(0).setPriority(JobInfo.PRIORITY_MIN)), + nowElapsed, 100L)); } @Test public void testGetLifeCycleEndElapsedLocked_Rescheduled() { + final long nowElapsed = sElapsedRealtimeClock.millis(); + JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS); JobStatus js = createJobStatus("time", jb); js = new JobStatus( @@ -705,20 +818,91 @@ public class FlexibilityControllerTest { 0, FROZEN_TIME, FROZEN_TIME); assertEquals(mFcConfig.RESCHEDULED_JOB_DEADLINE_MS, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); js = new JobStatus( js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 2, /* numSystemStops */ 1, 0, FROZEN_TIME, FROZEN_TIME); assertEquals(2 * mFcConfig.RESCHEDULED_JOB_DEADLINE_MS, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); js = new JobStatus( js, FROZEN_TIME, NO_LATEST_RUNTIME, /* numFailures */ 0, /* numSystemStops */ 10, 0, FROZEN_TIME, FROZEN_TIME); assertEquals(mFcConfig.MAX_RESCHEDULED_DEADLINE_MS, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); + } + + @Test + public void testGetLifeCycleEndElapsedLocked_ScoreAddition() { + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, + "500=" + HOUR_IN_MILLIS + + ",400=" + HOUR_IN_MILLIS + + ",300=" + HOUR_IN_MILLIS + + ",200=" + HOUR_IN_MILLIS + + ",100=" + HOUR_IN_MILLIS); + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINE_SCORES, + "500=5,400=4,300=3,200=2,100=1"); + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINE_ADDITIONAL_SCORE_TIME_FACTORS, + "500=" + 5 * MINUTE_IN_MILLIS + + ",400=" + 4 * MINUTE_IN_MILLIS + + ",300=" + 3 * MINUTE_IN_MILLIS + + ",200=" + 2 * MINUTE_IN_MILLIS + + ",100=" + 1 * MINUTE_IN_MILLIS); + + final long nowElapsed = sElapsedRealtimeClock.millis(); + + JobStatus jsMax = createJobStatus("testScoreCalculation", + createJob(0).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX)); + JobStatus jsHigh = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH)); + JobStatus jsDefault = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)); + JobStatus jsLow = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_LOW)); + JobStatus jsMin = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_MIN)); + // Make score = 15 + mFlexibilityController.prepareForExecutionLocked(jsMax); + mFlexibilityController.prepareForExecutionLocked(jsHigh); + mFlexibilityController.prepareForExecutionLocked(jsDefault); + mFlexibilityController.prepareForExecutionLocked(jsLow); + mFlexibilityController.prepareForExecutionLocked(jsMin); + + // deadline + JobInfo.Builder jb = createJob(0).setOverrideDeadline(HOUR_IN_MILLIS); + JobStatus js = createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", jb); + assertEquals(HOUR_IN_MILLIS + FROZEN_TIME, + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 0)); + + final long earliestMs = 123L; + // no deadline + assertEquals(earliestMs + HOUR_IN_MILLIS + 5 * 15 * MINUTE_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", + createJob(0).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX)), + nowElapsed, earliestMs)); + assertEquals(earliestMs + HOUR_IN_MILLIS + 4 * 15 * MINUTE_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH)), + nowElapsed, earliestMs)); + assertEquals(earliestMs + HOUR_IN_MILLIS + 3 * 15 * MINUTE_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)), + nowElapsed, earliestMs)); + assertEquals(earliestMs + HOUR_IN_MILLIS + 2 * 15 * MINUTE_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", + createJob(0).setPriority(JobInfo.PRIORITY_LOW)), + nowElapsed, earliestMs)); + assertEquals(earliestMs + HOUR_IN_MILLIS + 1 * 15 * MINUTE_IN_MILLIS, + mFlexibilityController.getLifeCycleEndElapsedLocked( + createJobStatus("testGetLifeCycleEndElapsedLocked_ScoreAddition", + createJob(0).setPriority(JobInfo.PRIORITY_MIN)), + nowElapsed, earliestMs)); } @Test @@ -734,6 +918,14 @@ public class FlexibilityControllerTest { @Test public void testFlexibilityTracker() { + setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 100 * HOUR_IN_MILLIS); + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, + "500=" + 100 * HOUR_IN_MILLIS + + ",400=" + 100 * HOUR_IN_MILLIS + + ",300=" + 100 * HOUR_IN_MILLIS + + ",200=" + 100 * HOUR_IN_MILLIS + + ",100=" + 100 * HOUR_IN_MILLIS); + FlexibilityController.FlexibilityTracker flexTracker = mFlexibilityController.new FlexibilityTracker(4); // Plus one for jobs with 0 required constraint. @@ -805,8 +997,7 @@ public class FlexibilityControllerTest { assertEquals(1, trackedJobs.get(3).size()); assertEquals(0, trackedJobs.get(4).size()); - final long nowElapsed = ((DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 2) - + HOUR_IN_MILLIS); + final long nowElapsed = 51 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); @@ -1220,66 +1411,161 @@ public class FlexibilityControllerTest { @Test public void testCalculateNumDroppedConstraints() { - JobInfo.Builder jb = createJob(22); - JobStatus js = createJobStatus("testCalculateNumDroppedConstraints", jb); - long nowElapsed = FROZEN_TIME; - - mFlexibilityController.mFlexibilityTracker.add(js); - - assertEquals(3, js.getNumRequiredFlexibleConstraints()); - assertEquals(0, js.getNumDroppedFlexibleConstraints()); - assertEquals(1, mFlexibilityController + setDeviceConfigString(KEY_FALLBACK_FLEXIBILITY_DEADLINES, + "500=" + HOUR_IN_MILLIS + + ",400=" + 5 * HOUR_IN_MILLIS + + ",300=" + 8 * HOUR_IN_MILLIS + + ",200=" + 10 * HOUR_IN_MILLIS + + ",100=" + 20 * HOUR_IN_MILLIS); + setDeviceConfigString(KEY_PERCENTS_TO_DROP_FLEXIBLE_CONSTRAINTS, + "500=20|40|60|80" + + ",400=20|40|60|80" + + ",300=25|50|75|80" + + ",200=40|50|60|80" + + ",100=20|40|60|80"); + + JobStatus jsHigh = createJobStatus("testCalculateNumDroppedConstraints", + createJob(24).setPriority(JobInfo.PRIORITY_HIGH)); + JobStatus jsDefault = createJobStatus("testCalculateNumDroppedConstraints", + createJob(23).setPriority(JobInfo.PRIORITY_DEFAULT)); + JobStatus jsLow = createJobStatus("testCalculateNumDroppedConstraints", + createJob(22).setPriority(JobInfo.PRIORITY_LOW)); + JobStatus jsMin = createJobStatus("testCalculateNumDroppedConstraints", + createJob(21).setPriority(JobInfo.PRIORITY_MIN)); + final long startElapsed = FROZEN_TIME; + long nowElapsed = startElapsed; + + mFlexibilityController.mFlexibilityTracker.add(jsHigh); + mFlexibilityController.mFlexibilityTracker.add(jsDefault); + mFlexibilityController.mFlexibilityTracker.add(jsLow); + mFlexibilityController.mFlexibilityTracker.add(jsMin); + + assertEquals(3, jsHigh.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsHigh.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsDefault.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsDefault.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsLow.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsLow.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsMin.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsMin.getNumDroppedFlexibleConstraints()); + assertEquals(4, mFlexibilityController .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size()); - nowElapsed += DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 5; + nowElapsed = startElapsed + HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); mFlexibilityController.mFlexibilityTracker - .setNumDroppedFlexibleConstraints(js, 1); - - assertEquals(2, js.getNumRequiredFlexibleConstraints()); - assertEquals(1, js.getNumDroppedFlexibleConstraints()); - assertEquals(1, mFlexibilityController - .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size()); - - mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed); - - assertEquals(2, js.getNumRequiredFlexibleConstraints()); - assertEquals(1, js.getNumDroppedFlexibleConstraints()); + .calculateNumDroppedConstraints(jsHigh, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsDefault, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsLow, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsMin, nowElapsed); + + assertEquals(2, jsHigh.getNumRequiredFlexibleConstraints()); + assertEquals(1, jsHigh.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsDefault.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsDefault.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsLow.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsLow.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsMin.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsMin.getNumDroppedFlexibleConstraints()); + assertEquals(3, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size()); assertEquals(1, mFlexibilityController .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size()); - nowElapsed = FROZEN_TIME; + nowElapsed = startElapsed; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); - mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed); - - assertEquals(3, js.getNumRequiredFlexibleConstraints()); - assertEquals(0, js.getNumDroppedFlexibleConstraints()); - assertEquals(1, mFlexibilityController + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsHigh, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsDefault, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsLow, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsMin, nowElapsed); + + assertEquals(3, jsHigh.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsHigh.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsDefault.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsDefault.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsLow.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsLow.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsMin.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsMin.getNumDroppedFlexibleConstraints()); + assertEquals(4, mFlexibilityController .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size()); + assertEquals(0, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size()); + assertEquals(0, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(1).size()); + assertEquals(0, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(0).size()); - nowElapsed += DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 9; + nowElapsed = startElapsed + 3 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); - mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed); - - assertEquals(0, js.getNumRequiredFlexibleConstraints()); - assertEquals(3, js.getNumDroppedFlexibleConstraints()); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsHigh, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsDefault, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsLow, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsMin, nowElapsed); + + assertEquals(0, jsHigh.getNumRequiredFlexibleConstraints()); + assertEquals(3, jsHigh.getNumDroppedFlexibleConstraints()); + assertEquals(2, jsDefault.getNumRequiredFlexibleConstraints()); + assertEquals(1, jsDefault.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsLow.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsLow.getNumDroppedFlexibleConstraints()); + assertEquals(3, jsMin.getNumRequiredFlexibleConstraints()); + assertEquals(0, jsMin.getNumDroppedFlexibleConstraints()); + assertEquals(2, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size()); + assertEquals(1, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size()); + assertEquals(0, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(1).size()); + assertEquals(1, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(0).size()); - nowElapsed = FROZEN_TIME + DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS / 10 * 6; + nowElapsed = startElapsed + 4 * HOUR_IN_MILLIS; JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(nowElapsed), ZoneOffset.UTC); - mFlexibilityController.mFlexibilityTracker.calculateNumDroppedConstraints(js, nowElapsed); - - assertEquals(1, js.getNumRequiredFlexibleConstraints()); - assertEquals(2, js.getNumDroppedFlexibleConstraints()); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsHigh, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsDefault, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsLow, nowElapsed); + mFlexibilityController.mFlexibilityTracker + .calculateNumDroppedConstraints(jsMin, nowElapsed); + + assertEquals(0, jsHigh.getNumRequiredFlexibleConstraints()); + assertEquals(3, jsHigh.getNumDroppedFlexibleConstraints()); + assertEquals(1, jsDefault.getNumRequiredFlexibleConstraints()); + assertEquals(2, jsDefault.getNumDroppedFlexibleConstraints()); + assertEquals(2, jsLow.getNumRequiredFlexibleConstraints()); + assertEquals(1, jsLow.getNumDroppedFlexibleConstraints()); + assertEquals(2, jsMin.getNumRequiredFlexibleConstraints()); + assertEquals(1, jsMin.getNumDroppedFlexibleConstraints()); + assertEquals(0, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size()); + assertEquals(2, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(2).size()); assertEquals(1, mFlexibilityController .mFlexibilityTracker.getJobsByNumRequiredConstraints(1).size()); + assertEquals(1, mFlexibilityController + .mFlexibilityTracker.getJobsByNumRequiredConstraints(0).size()); } @Test @@ -1308,7 +1594,7 @@ public class FlexibilityControllerTest { .get(js.getSourceUserId(), js.getSourcePackageName())); assertEquals(150L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js)); assertEquals(1150L, - mFlexibilityController.getLifeCycleEndElapsedLocked(js, 150L)); + mFlexibilityController.getLifeCycleEndElapsedLocked(js, nowElapsed, 150L)); assertEquals(0, mFlexibilityController.getCurPercentOfLifecycleLocked(js, FROZEN_TIME)); assertEquals(650L, mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js)); @@ -1317,6 +1603,80 @@ public class FlexibilityControllerTest { .mFlexibilityTracker.getJobsByNumRequiredConstraints(3).size()); } + @Test + public void testScoreCalculation() { + JobStatus jsMax = createJobStatus("testScoreCalculation", + createJob(0).setExpedited(true).setPriority(JobInfo.PRIORITY_MAX)); + JobStatus jsHigh = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH)); + JobStatus jsDefault = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)); + JobStatus jsLow = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_LOW)); + JobStatus jsMin = createJobStatus("testScoreCalculation", + createJob(0).setPriority(JobInfo.PRIORITY_MIN)); + + long nowElapsed = sElapsedRealtimeClock.millis(); + assertEquals(0, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + mFlexibilityController.prepareForExecutionLocked(jsMax); + assertEquals(5, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + advanceElapsedClock(30 * MINUTE_IN_MILLIS); + nowElapsed += 30 * MINUTE_IN_MILLIS; + mFlexibilityController.prepareForExecutionLocked(jsMax); + assertEquals(10, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + advanceElapsedClock(31 * MINUTE_IN_MILLIS); + nowElapsed += 31 * MINUTE_IN_MILLIS; + mFlexibilityController.prepareForExecutionLocked(jsHigh); + assertEquals(14, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + advanceElapsedClock(2 * HOUR_IN_MILLIS); + nowElapsed += 2 * HOUR_IN_MILLIS; + mFlexibilityController.prepareForExecutionLocked(jsDefault); + assertEquals(17, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + advanceElapsedClock(3 * HOUR_IN_MILLIS); + nowElapsed += 3 * HOUR_IN_MILLIS; + mFlexibilityController.prepareForExecutionLocked(jsLow); + assertEquals(19, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + advanceElapsedClock(3 * HOUR_IN_MILLIS); + nowElapsed += 3 * HOUR_IN_MILLIS; + mFlexibilityController.prepareForExecutionLocked(jsMin); + assertEquals(20, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + advanceElapsedClock(3 * HOUR_IN_MILLIS); + nowElapsed += 3 * HOUR_IN_MILLIS; + mFlexibilityController.prepareForExecutionLocked(jsMax); + assertEquals(25, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + mFlexibilityController.unprepareFromExecutionLocked(jsMax); + assertEquals(20, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + // 24 hours haven't passed yet. The jobs in the first hour bucket should still be included. + advanceElapsedClock(12 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS - 1); + nowElapsed += 12 * HOUR_IN_MILLIS - MINUTE_IN_MILLIS - 1; + assertEquals(20, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + // Passed the 24 hour mark. The jobs in the first hour bucket should no longer be included. + advanceElapsedClock(2); + nowElapsed += 2; + assertEquals(10, + mFlexibilityController.getScoreLocked(mSourceUid, SOURCE_PACKAGE, nowElapsed)); + + } + /** * The beginning of a lifecycle for prefetch jobs includes the cached maximum of the last time * the estimated launch time was updated and the last time the app was opened. diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index e6298eeccafb..5bec903e6414 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -569,6 +569,23 @@ public final class UserManagerServiceTest { } @Test + public void testAutoLockPrivateProfile() { + UserManagerService mSpiedUms = spy(mUms); + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + Mockito.doNothing().when(mSpiedUms).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), any(), + any()); + + mSpiedUms.autoLockPrivateSpace(); + + Mockito.verify(mSpiedUms).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), + any(), any()); + } + + @Test public void testAutoLockOnDeviceLockForPrivateProfile() { mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); UserManagerService mSpiedUms = spy(mUms); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 57c3a1d5f364..95cfc2a304d4 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -22,6 +22,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG; +import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; @@ -82,6 +83,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import com.android.compatibility.common.util.TestUtils; +import com.android.internal.R; import com.android.internal.compat.IPlatformCompat; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener; @@ -857,8 +859,7 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_requiredByDefault() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); - info.setComponentName(COMPONENT_NAME); + final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME); assertThat(mA11yms.isAccessibilityServiceWarningRequired(info)).isTrue(); } @@ -867,10 +868,9 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); - info_a.setComponentName(COMPONENT_NAME); - final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); - info_b.setComponentName(new ComponentName("package_b", "class_b")); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b")); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mEnabledServices.clear(); userState.mEnabledServices.add(info_b.getComponentName()); @@ -883,12 +883,12 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); - info_a.setComponentName(new ComponentName("package_a", "class_a")); - final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); - info_b.setComponentName(new ComponentName("package_b", "class_b")); - final AccessibilityServiceInfo info_c = new AccessibilityServiceInfo(); - info_c.setComponentName(new ComponentName("package_c", "class_c")); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a")); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b")); + final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo( + new ComponentName("package_c", "class_c")); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mAccessibilityButtonTargets.clear(); userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString()); @@ -900,6 +900,51 @@ public class AccessibilityManagerServiceTest { assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse(); } + @Test + @RequiresFlagsEnabled({ + FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG, + FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES}) + public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() { + mockManageAccessibilityGranted(mTestableContext); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), true); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b"), false); + final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo( + new ComponentName("package_c", "class_c"), true); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{ + info_b.getComponentName().flattenToString(), + info_c.getComponentName().flattenToString()}); + + // info_a is not in the allowlist => require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue(); + // info_b is not preinstalled => require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isTrue(); + // info_c is both in the allowlist and preinstalled => do not require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse(); + } + + private static AccessibilityServiceInfo mockAccessibilityServiceInfo( + ComponentName componentName) { + return mockAccessibilityServiceInfo(componentName, false); + } + + private static AccessibilityServiceInfo mockAccessibilityServiceInfo( + ComponentName componentName, + boolean isSystemApp) { + AccessibilityServiceInfo accessibilityServiceInfo = + Mockito.spy(new AccessibilityServiceInfo()); + accessibilityServiceInfo.setComponentName(componentName); + ResolveInfo mockResolveInfo = Mockito.mock(ResolveInfo.class); + when(accessibilityServiceInfo.getResolveInfo()).thenReturn(mockResolveInfo); + mockResolveInfo.serviceInfo = Mockito.mock(ServiceInfo.class); + mockResolveInfo.serviceInfo.applicationInfo = Mockito.mock(ApplicationInfo.class); + when(mockResolveInfo.serviceInfo.applicationInfo.isSystemApp()).thenReturn(isSystemApp); + return accessibilityServiceInfo; + } + // Single package intents can trigger multiple PackageMonitor callbacks. // Collect the state of the lock in a set, since tests only care if calls // were all locked or all unlocked. diff --git a/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java index 9c8276aac4dd..84c0ab38ca48 100644 --- a/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java @@ -15,6 +15,7 @@ */ package com.android.server.audio; +import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_NONE; import static android.media.AudioManager.GET_DEVICES_OUTPUTS; import static android.media.AudioPlaybackConfiguration.PLAYER_UPDATE_DEVICE_ID; import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4; @@ -22,6 +23,7 @@ import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_T import static android.media.MediaFormat.KEY_AAC_DRC_EFFECT_TYPE; import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION; import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL; +import static android.os.Process.myPid; import static com.android.server.audio.LoudnessCodecHelper.SPL_RANGE_LARGE; import static com.android.server.audio.LoudnessCodecHelper.SPL_RANGE_MEDIUM; @@ -37,6 +39,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.media.AudioAttributes; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.AudioPlaybackConfiguration; @@ -64,13 +67,16 @@ import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.List; -import java.util.Random; @RunWith(AndroidJUnit4.class) @Presubmit public class LoudnessCodecHelperTest { private static final String TAG = "LoudnessCodecHelperTest"; + private static final int TEST_USAGE = AudioAttributes.USAGE_MEDIA; + + private static final int TEST_CONTENT = AudioAttributes.CONTENT_TYPE_MUSIC; + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); @@ -83,94 +89,84 @@ public class LoudnessCodecHelperTest { private final int mInitialApcPiid = 1; + private int mSessionId; + @Before public void setUp() throws Exception { mLoudnessHelper = new LoudnessCodecHelper(mAudioService); + mSessionId = 1; when(mAudioService.getActivePlaybackConfigurations()).thenReturn( - getApcListForPiids(mInitialApcPiid)); + getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 0)); when(mDispatcher.asBinder()).thenReturn(Mockito.mock(IBinder.class)); } @Test - public void registerDispatcher_sendsInitialUpdateOnStart() throws Exception { + public void registerDispatcher_sendsUpdateOnAddCodec() throws Exception { mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); - mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, - List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4))); + mLoudnessHelper.startLoudnessCodecUpdates(mSessionId); + mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222, + getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D)); - verify(mDispatcher).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), any()); + verify(mDispatcher).dispatchLoudnessCodecParameterChange(eq(mSessionId), any()); } @Test - public void unregisterDispatcher_noInitialUpdateOnStart() throws Exception { + public void unregisterDispatcher_noUpdateOnAdd() throws Exception { mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); mLoudnessHelper.unregisterLoudnessCodecUpdatesDispatcher(mDispatcher); - mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, - List.of(getLoudnessInfo(/*isDownmixing=*/false, CODEC_METADATA_TYPE_MPEG_D))); + mLoudnessHelper.startLoudnessCodecUpdates(mSessionId); + mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222, + getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D)); - verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), + verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId), any()); } @Test - public void addCodecInfo_sendsInitialUpdateAfterStart() throws Exception { + public void addCodecInfoForDifferentId_noUpdateSent() throws Exception { + final int newSessionId = mSessionId + 1; mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); - mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, - List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4))); - mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid, /*mediaCodecHash=*/222, + mLoudnessHelper.startLoudnessCodecUpdates(mSessionId); + mLoudnessHelper.addLoudnessCodecInfo(newSessionId, /*mediaCodecHash=*/222, getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D)); - verify(mDispatcher, times(2)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), + verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId), any()); } @Test - public void addCodecInfoForUnstartedPiid_noUpdateSent() throws Exception { - final int newPiid = 2; + public void updateCodecParameters_noStartedPiids_noDispatch() throws Exception { mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); - - mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, - List.of(getLoudnessInfo(/*isDownmixing=*/true, - CODEC_METADATA_TYPE_MPEG_4))); - mLoudnessHelper.addLoudnessCodecInfo(newPiid, /*mediaCodecHash=*/222, + mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222, getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D)); - verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), - any()); - } + mLoudnessHelper.updateCodecParameters( + getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1)); - @Test - public void updateCodecParameters_updatesOnlyStartedPiids() throws Exception { - final int newPiid = 2; - mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); - - mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, - List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4))); - //does not trigger dispatch since active apc list does not contain newPiid - mLoudnessHelper.startLoudnessCodecUpdates(newPiid, - List.of(getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D))); - verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), + // no dispatch since mSessionId was not started + verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId), any()); - - // triggers dispatch for new active apc with newPiid - mLoudnessHelper.updateCodecParameters(getApcListForPiids(newPiid)); - verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(newPiid), any()); } @Test - public void updateCodecParameters_noStartedPiids_noDispatch() throws Exception { + public void updateCodecParameters_dispatchUpdates() throws Exception { + final LoudnessCodecInfo info = getLoudnessInfo(/*isDownmixing=*/true, + CODEC_METADATA_TYPE_MPEG_4); mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); - mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid, /*mediaCodecHash=*/222, - getLoudnessInfo(/*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_D)); - mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid)); + mLoudnessHelper.startLoudnessCodecUpdates(mSessionId); + mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222, info); - // no dispatch since mInitialApcPiid was not started - verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), + mLoudnessHelper.updateCodecParameters( + getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1)); + + // second dispatch since player configurations were updated + verify(mDispatcher, times(2)).dispatchLoudnessCodecParameterChange(eq(mSessionId), any()); } @@ -180,13 +176,15 @@ public class LoudnessCodecHelperTest { CODEC_METADATA_TYPE_MPEG_4); mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); - mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info)); - mLoudnessHelper.removeLoudnessCodecInfo(mInitialApcPiid, info); + mLoudnessHelper.startLoudnessCodecUpdates(mSessionId); + mLoudnessHelper.addLoudnessCodecInfo(mSessionId, /*mediaCodecHash=*/222, info); + mLoudnessHelper.removeLoudnessCodecInfo(mSessionId, info); - mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid)); + mLoudnessHelper.updateCodecParameters( + getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1)); // no second dispatch since codec info was removed for updates - verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), + verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mSessionId), any()); } @@ -196,13 +194,14 @@ public class LoudnessCodecHelperTest { CODEC_METADATA_TYPE_MPEG_4); mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher); - mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info)); - mLoudnessHelper.stopLoudnessCodecUpdates(mInitialApcPiid); + mLoudnessHelper.startLoudnessCodecUpdates(mSessionId); + mLoudnessHelper.stopLoudnessCodecUpdates(mSessionId); - mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid)); + mLoudnessHelper.updateCodecParameters( + getApcListForApcWithPiidSid(mInitialApcPiid, mSessionId, 1)); // no second dispatch since piid was removed for updates - verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), + verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mSessionId), any()); } @@ -308,23 +307,28 @@ public class LoudnessCodecHelperTest { assertEquals(6, loudnessParameters.getInt(KEY_AAC_DRC_EFFECT_TYPE)); } - private List<AudioPlaybackConfiguration> getApcListForPiids(int... piids) { + private List<AudioPlaybackConfiguration> getApcListForApcWithPiidSid(int piid, int sessionId, + int devIdx) { final ArrayList<AudioPlaybackConfiguration> apcList = new ArrayList<>(); AudioDeviceInfo[] devicesStatic = AudioManager.getDevicesStatic(GET_DEVICES_OUTPUTS); - assumeTrue(devicesStatic.length > 0); - int index = new Random().nextInt(devicesStatic.length); - Log.d(TAG, "Out devices number " + devicesStatic.length + ". Picking index " + index); - int deviceId = devicesStatic[index].getId(); - - for (int piid : piids) { - PlayerBase.PlayerIdCard idCard = Mockito.mock(PlayerBase.PlayerIdCard.class); - AudioPlaybackConfiguration apc = - new AudioPlaybackConfiguration(idCard, piid, /*uid=*/1, /*pid=*/1); - apc.handleStateEvent(PLAYER_UPDATE_DEVICE_ID, deviceId); - - apcList.add(apc); - } + assumeTrue(devIdx < devicesStatic.length); + Log.d(TAG, "Out devices number " + devicesStatic.length + ". Picking index " + devIdx); + int deviceId = devicesStatic[devIdx].getId(); + + PlayerBase.PlayerIdCard idCard = Mockito.mock(PlayerBase.PlayerIdCard.class); + AudioPlaybackConfiguration apc = + new AudioPlaybackConfiguration(idCard, piid, /*uid=*/1, /*pid=*/myPid()); + apc.handleStateEvent(PLAYER_UPDATE_DEVICE_ID, deviceId); + apc.handleSessionIdEvent(sessionId); + apc.handleAudioAttributesEvent(new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setAllowedCapturePolicy(ALLOW_CAPTURE_BY_NONE) + .build()); + + apcList.add(apc); + return apcList; } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java index a8eace05de97..c7300bbb50a7 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java @@ -20,6 +20,7 @@ import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.mock; @@ -33,11 +34,15 @@ import android.content.Intent; import android.hardware.biometrics.AuthenticateOptions; import android.hardware.biometrics.IBiometricContextListener; import android.hardware.biometrics.IBiometricContextListener.FoldState; +import android.hardware.biometrics.common.DisplayState; import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.common.OperationReason; import android.hardware.display.DisplayManagerGlobal; +import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.TestableContext; import android.view.Display; import android.view.DisplayInfo; @@ -72,6 +77,9 @@ public class BiometricContextProviderTest { @Rule public TestableContext mContext = new TestableContext( InstrumentationRegistry.getInstrumentation().getContext()); + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); @Mock private IStatusBarService mStatusBarService; @@ -395,6 +403,37 @@ public class BiometricContextProviderTest { } } + @Test + public void testSubscribe_thenStartHal() throws RemoteException { + Consumer<OperationContext> updateConsumer = mock(Consumer.class); + Consumer<OperationContext> startHalConsumer = mock(Consumer.class); + AuthenticateOptions options = new FingerprintAuthenticateOptions.Builder().build(); + OperationContextExt context = mProvider.updateContext(mOpContext, false /* crypto */); + + assertThat(context.getDisplayState()).isEqualTo(DisplayState.UNKNOWN); + assertThat(context.getFoldState()).isEqualTo(IBiometricContextListener.FoldState.UNKNOWN); + + mListener.onDisplayStateChanged(DisplayState.LOCKSCREEN); + mListener.onFoldChanged(FoldState.FULLY_CLOSED); + mProvider.subscribe(context, startHalConsumer, updateConsumer, options); + + assertThat(context.getDisplayState()).isEqualTo(DisplayState.LOCKSCREEN); + assertThat(context.getFoldState()).isEqualTo(FoldState.FULLY_CLOSED); + verify(updateConsumer, never()).accept(context.toAidlContext()); + verify(startHalConsumer).accept(context.toAidlContext(options)); + } + + @Test + public void testSubscribe_withInvalidOptions() { + Consumer<OperationContext> updateConsumer = mock(Consumer.class); + Consumer<OperationContext> startHalConsumer = mock(Consumer.class); + AuthenticateOptions options = mock(AuthenticateOptions.class); + OperationContextExt context = mProvider.updateContext(mOpContext, false /* crypto */); + + assertThrows(IllegalStateException.class, () -> mProvider.subscribe( + context, startHalConsumer, updateConsumer, options)); + } + private static byte reason(int type) { if (type == StatusBarManager.SESSION_BIOMETRIC_PROMPT) { return OperationReason.BIOMETRIC_PROMPT; diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java index 0973d46283ed..5e380108aeb3 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -25,6 +25,7 @@ import static com.android.server.hdmi.Constants.ADDR_RECORDER_1; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.HdmiCecLocalDevice.ActiveSource; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; +import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE; import static com.google.common.truth.Truth.assertThat; @@ -1807,4 +1808,35 @@ public class HdmiCecLocalDeviceTvTest { // TV should only send <Give Osd Name> once assertEquals(1, Collections.frequency(mNativeWrapper.getResultMessages(), giveOsdName)); } + + @Test + public void initiateCecByWakeupMessage_selectInternalSourceAfterDelay_broadcastsActiveSource() { + HdmiCecMessage activeSourceFromTv = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE); + mTestLooper.dispatchAll(); + + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + + mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback()); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(activeSourceFromTv); + } + + @Test + public void initiateCecByWakeupMessage_selectInternalSource_doesNotBroadcastActiveSource() { + HdmiCecMessage activeSourceFromTv = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE); + mTestLooper.dispatchAll(); + + mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback()); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv); + } } diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java index daf18edaf2de..8656f60afc1e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java @@ -9,13 +9,16 @@ * * 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. + * 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; +import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -27,6 +30,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -97,7 +101,6 @@ public final class BackgroundInstallControlServiceTest { private Looper mLooper; private File mFile; - @Mock private Context mContext; @Mock @@ -108,8 +111,10 @@ public final class BackgroundInstallControlServiceTest { private UsageStatsManagerInternal mUsageStatsManagerInternal; @Mock private PermissionManagerServiceInternal mPermissionManager; + @Captor private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor; + @Captor private ArgumentCaptor<UsageEventListener> mUsageEventListenerCaptor; @@ -119,11 +124,12 @@ public final class BackgroundInstallControlServiceTest { mTestLooper = new TestLooper(); mLooper = mTestLooper.getLooper(); - mFile = new File( - InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(), - "test"); - mBackgroundInstallControlService = new BackgroundInstallControlService( - new MockInjector(mContext)); + mFile = + new File( + InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(), + "test"); + mBackgroundInstallControlService = + new BackgroundInstallControlService(new MockInjector(mContext)); verify(mUsageStatsManagerInternal).registerListener(mUsageEventListenerCaptor.capture()); mUsageEventListener = mUsageEventListenerCaptor.getValue(); @@ -143,8 +149,7 @@ public final class BackgroundInstallControlServiceTest { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); mBackgroundInstallControlService.initBackgroundInstalledPackages(); assertNotNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); - assertEquals(0, - mBackgroundInstallControlService.getBackgroundInstalledPackages().size()); + assertEquals(0, mBackgroundInstallControlService.getBackgroundInstalledPackages().size()); } @Test @@ -161,12 +166,9 @@ public final class BackgroundInstallControlServiceTest { // Write test data to the file on the disk. try { ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream); - long token = protoOutputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); - protoOutputStream.write( - BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1); - protoOutputStream.write( - BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1); + long token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1); + protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1); protoOutputStream.end(token); protoOutputStream.flush(); atomicFile.finishWrite(fileOutputStream); @@ -198,20 +200,14 @@ public final class BackgroundInstallControlServiceTest { try { ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream); - long token = protoOutputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); - protoOutputStream.write( - BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1); - protoOutputStream.write( - BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1); + long token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1); + protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1); protoOutputStream.end(token); - token = protoOutputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); - protoOutputStream.write( - BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2); - protoOutputStream.write( - BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1); + token = protoOutputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + protoOutputStream.write(BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2); + protoOutputStream.write(BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1); protoOutputStream.end(token); protoOutputStream.flush(); @@ -241,7 +237,7 @@ public final class BackgroundInstallControlServiceTest { // Read the file on the disk to verify var packagesInDisk = new SparseSetArray<>(); AtomicFile atomicFile = new AtomicFile(mFile); - try (FileInputStream fileInputStream = atomicFile.openRead()) { + try (FileInputStream fileInputStream = atomicFile.openRead()) { ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream); while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { @@ -249,23 +245,25 @@ public final class BackgroundInstallControlServiceTest { != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { continue; } - long token = protoInputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + long token = + protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); String packageName = null; int userId = UserHandle.USER_NULL; while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (protoInputStream.getFieldNumber()) { case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: - packageName = protoInputStream.readString( - BackgroundInstalledPackageProto.PACKAGE_NAME); + packageName = + protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); break; case (int) BackgroundInstalledPackageProto.USER_ID: - userId = protoInputStream.readInt( - BackgroundInstalledPackageProto.USER_ID) - 1; + userId = + protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) + - 1; break; default: - fail("Undefined field in proto: " - + protoInputStream.getFieldNumber()); + fail("Undefined field in proto: " + protoInputStream.getFieldNumber()); } } protoInputStream.end(token); @@ -296,7 +294,7 @@ public final class BackgroundInstallControlServiceTest { // Read the file on the disk to verify var packagesInDisk = new SparseSetArray<>(); AtomicFile atomicFile = new AtomicFile(mFile); - try (FileInputStream fileInputStream = atomicFile.openRead()) { + try (FileInputStream fileInputStream = atomicFile.openRead()) { ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream); while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { @@ -304,23 +302,25 @@ public final class BackgroundInstallControlServiceTest { != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { continue; } - long token = protoInputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + long token = + protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); String packageName = null; int userId = UserHandle.USER_NULL; while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (protoInputStream.getFieldNumber()) { case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: - packageName = protoInputStream.readString( - BackgroundInstalledPackageProto.PACKAGE_NAME); + packageName = + protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); break; case (int) BackgroundInstalledPackageProto.USER_ID: - userId = protoInputStream.readInt( - BackgroundInstalledPackageProto.USER_ID) - 1; + userId = + protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) + - 1; break; default: - fail("Undefined field in proto: " - + protoInputStream.getFieldNumber()); + fail("Undefined field in proto: " + protoInputStream.getFieldNumber()); } } protoInputStream.end(token); @@ -353,7 +353,7 @@ public final class BackgroundInstallControlServiceTest { // Read the file on the disk to verify var packagesInDisk = new SparseSetArray<>(); AtomicFile atomicFile = new AtomicFile(mFile); - try (FileInputStream fileInputStream = atomicFile.openRead()) { + try (FileInputStream fileInputStream = atomicFile.openRead()) { ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream); while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { @@ -361,23 +361,25 @@ public final class BackgroundInstallControlServiceTest { != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { continue; } - long token = protoInputStream.start( - BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + long token = + protoInputStream.start(BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); String packageName = null; int userId = UserHandle.USER_NULL; while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (protoInputStream.getFieldNumber()) { case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: - packageName = protoInputStream.readString( - BackgroundInstalledPackageProto.PACKAGE_NAME); + packageName = + protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); break; case (int) BackgroundInstalledPackageProto.USER_ID: - userId = protoInputStream.readInt( - BackgroundInstalledPackageProto.USER_ID) - 1; + userId = + protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) + - 1; break; default: - fail("Undefined field in proto: " - + protoInputStream.getFieldNumber()); + fail("Undefined field in proto: " + protoInputStream.getFieldNumber()); } } protoInputStream.end(token); @@ -399,51 +401,55 @@ public final class BackgroundInstallControlServiceTest { @Test public void testHandleUsageEvent_permissionDenied() { - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); - doReturn(PackageManager.PERMISSION_DENIED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, 0); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_DENIED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, USER_ID_1, INSTALLER_NAME_1, 0); mTestLooper.dispatchAll(); - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); } @Test public void testHandleUsageEvent_permissionGranted() { - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, 0); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, USER_ID_1, INSTALLER_NAME_1, 0); mTestLooper.dispatchAll(); - assertEquals(1, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + assertEquals( + 1, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); } @Test public void testHandleUsageEvent_ignoredEvent() { - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.USER_INTERACTION, - USER_ID_1, INSTALLER_NAME_1, 0); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent(UsageEvents.Event.USER_INTERACTION, USER_ID_1, INSTALLER_NAME_1, 0); mTestLooper.dispatchAll(); - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); } @Test public void testHandleUsageEvent_firstActivityResumedHalfTimeFrame() { - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_1); mTestLooper.dispatchAll(); var installerForegroundTimeFrames = @@ -461,14 +467,18 @@ public final class BackgroundInstallControlServiceTest { @Test public void testHandleUsageEvent_firstActivityResumedOneTimeFrame() { - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_1); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); mTestLooper.dispatchAll(); var installerForegroundTimeFrames = @@ -486,16 +496,23 @@ public final class BackgroundInstallControlServiceTest { @Test public void testHandleUsageEvent_firstActivityResumedOneAndHalfTimeFrame() { - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_1); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_3); mTestLooper.dispatchAll(); var installerForegroundTimeFrames = @@ -517,12 +534,13 @@ public final class BackgroundInstallControlServiceTest { @Test public void testHandleUsageEvent_firstNoneActivityResumed() { - assertEquals(0, - mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); + assertEquals( + 0, mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); mTestLooper.dispatchAll(); var installerForegroundTimeFrames = @@ -535,27 +553,26 @@ public final class BackgroundInstallControlServiceTest { } @Test - public void testHandleUsageEvent_packageAddedNoUsageEvent() throws - NoSuchFieldException, PackageManager.NameNotFoundException { + public void testHandleUsageEvent_packageAddedNoUsageEvent() + throws NoSuchFieldException, PackageManager.NameNotFoundException { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); - InstallSourceInfo installSourceInfo = new InstallSourceInfo( - /* initiatingPackageName = */ INSTALLER_NAME_1, - /* initiatingPackageSigningInfo = */ null, - /* originatingPackageName = */ null, - /* installingPackageName = */ INSTALLER_NAME_1); + InstallSourceInfo installSourceInfo = + new InstallSourceInfo( + /* initiatingPackageName= */ INSTALLER_NAME_1, + /* initiatingPackageSigningInfo= */ null, + /* originatingPackageName= */ null, + /* installingPackageName= */ INSTALLER_NAME_1); assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); ApplicationInfo appInfo = mock(ApplicationInfo.class); - when(mPackageManager.getApplicationInfoAsUser( - eq(PACKAGE_NAME_1), - any(), - anyInt()) - ).thenReturn(appInfo); + when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt())) + .thenReturn(appInfo); - long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 - - (System.currentTimeMillis() - SystemClock.uptimeMillis()); - FieldSetter.setField(appInfo, + long createTimestamp = + PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField( + appInfo, ApplicationInfo.class.getDeclaredField("createTimestamp"), createTimestamp); @@ -572,27 +589,26 @@ public final class BackgroundInstallControlServiceTest { } @Test - public void testHandleUsageEvent_packageAddedInsideTimeFrame() throws - NoSuchFieldException, PackageManager.NameNotFoundException { + public void testHandleUsageEvent_packageAddedInsideTimeFrame() + throws NoSuchFieldException, PackageManager.NameNotFoundException { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); - InstallSourceInfo installSourceInfo = new InstallSourceInfo( - /* initiatingPackageName = */ INSTALLER_NAME_1, - /* initiatingPackageSigningInfo = */ null, - /* originatingPackageName = */ null, - /* installingPackageName = */ INSTALLER_NAME_1); + InstallSourceInfo installSourceInfo = + new InstallSourceInfo( + /* initiatingPackageName= */ INSTALLER_NAME_1, + /* initiatingPackageSigningInfo= */ null, + /* originatingPackageName= */ null, + /* installingPackageName= */ INSTALLER_NAME_1); assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); ApplicationInfo appInfo = mock(ApplicationInfo.class); - when(mPackageManager.getApplicationInfoAsUser( - eq(PACKAGE_NAME_1), - any(), - anyInt()) - ).thenReturn(appInfo); + when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt())) + .thenReturn(appInfo); - long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 - - (System.currentTimeMillis() - SystemClock.uptimeMillis()); - FieldSetter.setField(appInfo, + long createTimestamp = + PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField( + appInfo, ApplicationInfo.class.getDeclaredField("createTimestamp"), createTimestamp); @@ -604,12 +620,16 @@ public final class BackgroundInstallControlServiceTest { // The 2 usage events make the package adding inside a time frame. // So it's not a background install. Thus, it's null for the return of // mBackgroundInstallControlService.getBackgroundInstalledPackages() - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_1); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); mTestLooper.dispatchAll(); @@ -617,27 +637,26 @@ public final class BackgroundInstallControlServiceTest { } @Test - public void testHandleUsageEvent_packageAddedOutsideTimeFrame1() throws - NoSuchFieldException, PackageManager.NameNotFoundException { + public void testHandleUsageEvent_packageAddedOutsideTimeFrame1() + throws NoSuchFieldException, PackageManager.NameNotFoundException { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); - InstallSourceInfo installSourceInfo = new InstallSourceInfo( - /* initiatingPackageName = */ INSTALLER_NAME_1, - /* initiatingPackageSigningInfo = */ null, - /* originatingPackageName = */ null, - /* installingPackageName = */ INSTALLER_NAME_1); + InstallSourceInfo installSourceInfo = + new InstallSourceInfo( + /* initiatingPackageName= */ INSTALLER_NAME_1, + /* initiatingPackageSigningInfo= */ null, + /* originatingPackageName= */ null, + /* installingPackageName= */ INSTALLER_NAME_1); assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); ApplicationInfo appInfo = mock(ApplicationInfo.class); - when(mPackageManager.getApplicationInfoAsUser( - eq(PACKAGE_NAME_1), - any(), - anyInt()) - ).thenReturn(appInfo); + when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt())) + .thenReturn(appInfo); - long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 - - (System.currentTimeMillis() - SystemClock.uptimeMillis()); - FieldSetter.setField(appInfo, + long createTimestamp = + PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField( + appInfo, ApplicationInfo.class.getDeclaredField("createTimestamp"), createTimestamp); @@ -650,12 +669,16 @@ public final class BackgroundInstallControlServiceTest { // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame, // it's a background install. Thus, it's not null for the return of // mBackgroundInstallControlService.getBackgroundInstalledPackages() - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); mTestLooper.dispatchAll(); @@ -665,28 +688,28 @@ public final class BackgroundInstallControlServiceTest { assertEquals(1, packages.size()); assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); } + @Test - public void testHandleUsageEvent_packageAddedOutsideTimeFrame2() throws - NoSuchFieldException, PackageManager.NameNotFoundException { + public void testHandleUsageEvent_packageAddedOutsideTimeFrame2() + throws NoSuchFieldException, PackageManager.NameNotFoundException { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); - InstallSourceInfo installSourceInfo = new InstallSourceInfo( - /* initiatingPackageName = */ INSTALLER_NAME_1, - /* initiatingPackageSigningInfo = */ null, - /* originatingPackageName = */ null, - /* installingPackageName = */ INSTALLER_NAME_1); + InstallSourceInfo installSourceInfo = + new InstallSourceInfo( + /* initiatingPackageName= */ INSTALLER_NAME_1, + /* initiatingPackageSigningInfo= */ null, + /* originatingPackageName= */ null, + /* installingPackageName= */ INSTALLER_NAME_1); assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); ApplicationInfo appInfo = mock(ApplicationInfo.class); - when(mPackageManager.getApplicationInfoAsUser( - eq(PACKAGE_NAME_1), - any(), - anyInt()) - ).thenReturn(appInfo); + when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt())) + .thenReturn(appInfo); - long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 - - (System.currentTimeMillis() - SystemClock.uptimeMillis()); - FieldSetter.setField(appInfo, + long createTimestamp = + PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField( + appInfo, ApplicationInfo.class.getDeclaredField("createTimestamp"), createTimestamp); @@ -700,12 +723,16 @@ public final class BackgroundInstallControlServiceTest { // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame, // it's a background install. Thus, it's not null for the return of // mBackgroundInstallControlService.getBackgroundInstalledPackages() - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_2); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_2, + INSTALLER_NAME_2, + USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3); mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); mTestLooper.dispatchAll(); @@ -715,31 +742,31 @@ public final class BackgroundInstallControlServiceTest { assertEquals(1, packages.size()); assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); } + @Test - public void testHandleUsageEvent_packageAddedThroughAdb() throws - NoSuchFieldException, PackageManager.NameNotFoundException { + public void testHandleUsageEvent_packageAddedThroughAdb() + throws NoSuchFieldException, PackageManager.NameNotFoundException { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); // This test is a duplicate of testHandleUsageEvent_packageAddedThroughAdb except the // initiatingPackageName used to be null but is now "com.android.shell". This test ensures // that the behavior is still the same for when the initiatingPackageName is null. - InstallSourceInfo installSourceInfo = new InstallSourceInfo( - /* initiatingPackageName = */ null, - /* initiatingPackageSigningInfo = */ null, - /* originatingPackageName = */ null, - /* installingPackageName = */ INSTALLER_NAME_1); + InstallSourceInfo installSourceInfo = + new InstallSourceInfo( + /* initiatingPackageName= */ null, + /* initiatingPackageSigningInfo= */ null, + /* originatingPackageName= */ null, + /* installingPackageName= */ INSTALLER_NAME_1); // b/265203007 when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); ApplicationInfo appInfo = mock(ApplicationInfo.class); - when(mPackageManager.getApplicationInfoAsUser( - eq(PACKAGE_NAME_1), - any(), - anyInt()) - ).thenReturn(appInfo); + when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt())) + .thenReturn(appInfo); - long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 - - (System.currentTimeMillis() - SystemClock.uptimeMillis()); - FieldSetter.setField(appInfo, + long createTimestamp = + PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField( + appInfo, ApplicationInfo.class.getDeclaredField("createTimestamp"), createTimestamp); @@ -751,12 +778,16 @@ public final class BackgroundInstallControlServiceTest { // for ADB installs the initiatingPackageName used to be null, despite being detected // as a background install. Since we do not want to treat side-loaded apps as background // install getBackgroundInstalledPackages() is expected to return null - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); mTestLooper.dispatchAll(); @@ -764,31 +795,31 @@ public final class BackgroundInstallControlServiceTest { var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); assertNull(packages); } + @Test - public void testHandleUsageEvent_packageAddedThroughAdb2() throws - NoSuchFieldException, PackageManager.NameNotFoundException { + public void testHandleUsageEvent_packageAddedThroughAdb2() + throws NoSuchFieldException, PackageManager.NameNotFoundException { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); // This test is a duplicate of testHandleUsageEvent_packageAddedThroughAdb except the // initiatingPackageName used to be null but is now "com.android.shell". This test ensures // that the behavior is still the same after this change. - InstallSourceInfo installSourceInfo = new InstallSourceInfo( - /* initiatingPackageName = */ "com.android.shell", - /* initiatingPackageSigningInfo = */ null, - /* originatingPackageName = */ null, - /* installingPackageName = */ INSTALLER_NAME_1); + InstallSourceInfo installSourceInfo = + new InstallSourceInfo( + /* initiatingPackageName= */ "com.android.shell", + /* initiatingPackageSigningInfo= */ null, + /* originatingPackageName= */ null, + /* installingPackageName= */ INSTALLER_NAME_1); // b/265203007 when(mPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); ApplicationInfo appInfo = mock(ApplicationInfo.class); - when(mPackageManager.getApplicationInfoAsUser( - eq(PACKAGE_NAME_1), - any(), - anyInt()) - ).thenReturn(appInfo); + when(mPackageManager.getApplicationInfoAsUser(eq(PACKAGE_NAME_1), any(), anyInt())) + .thenReturn(appInfo); - long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 - - (System.currentTimeMillis() - SystemClock.uptimeMillis()); - FieldSetter.setField(appInfo, + long createTimestamp = + PACKAGE_ADD_TIMESTAMP_1 - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField( + appInfo, ApplicationInfo.class.getDeclaredField("createTimestamp"), createTimestamp); @@ -800,12 +831,16 @@ public final class BackgroundInstallControlServiceTest { // for ADB installs the initiatingPackageName is com.android.shell, despite being detected // as a background install. Since we do not want to treat side-loaded apps as background // install getBackgroundInstalledPackages() is expected to return null - doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( - anyString(), anyString(), anyInt(), anyInt()); - generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); - generateUsageEvent(Event.ACTIVITY_STOPPED, - USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); + doReturn(PERMISSION_GRANTED) + .when(mPermissionManager) + .checkPermission(anyString(), anyString(), anyInt(), anyInt()); + generateUsageEvent( + UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, + INSTALLER_NAME_1, + USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent( + Event.ACTIVITY_STOPPED, USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); mTestLooper.dispatchAll(); @@ -813,6 +848,7 @@ public final class BackgroundInstallControlServiceTest { var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); assertNull(packages); } + @Test public void testPackageRemoved() { assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); @@ -859,8 +895,7 @@ public final class BackgroundInstallControlServiceTest { packages.add(packageInfo2); var packageInfo3 = makePackageInfo(PACKAGE_NAME_3); packages.add(packageInfo3); - doReturn(packages).when(mPackageManager).getInstalledPackagesAsUser( - any(), anyInt()); + doReturn(packages).when(mPackageManager).getInstalledPackagesAsUser(any(), anyInt()); var resultPackages = mBackgroundInstallControlService.getBackgroundInstalledPackages(0L, USER_ID_1); @@ -870,18 +905,30 @@ public final class BackgroundInstallControlServiceTest { assertFalse(resultPackages.getList().contains(packageInfo3)); } + @Test(expected = SecurityException.class) + public void enforceCallerPermissionsThrowsSecurityException() { + doThrow(new SecurityException("test")).when(mContext) + .enforceCallingOrSelfPermission(eq(GET_BACKGROUND_INSTALLED_PACKAGES), anyString()); + + mBackgroundInstallControlService.enforceCallerPermissions(); + } + + @Test + public void enforceCallerPermissionsDoesNotThrowSecurityException() { + //enforceCallerQueryPackagesPermissions do not throw + + mBackgroundInstallControlService.enforceCallerPermissions(); + } + /** * Mock a usage event occurring. * * @param usageEventId id of a usage event - * @param userId user id of a usage event - * @param pkgName package name of a usage event - * @param timestamp timestamp of a usage event + * @param userId user id of a usage event + * @param pkgName package name of a usage event + * @param timestamp timestamp of a usage event */ - private void generateUsageEvent(int usageEventId, - int userId, - String pkgName, - long timestamp) { + private void generateUsageEvent(int usageEventId, int userId, String pkgName, long timestamp) { Event event = new Event(usageEventId, timestamp); event.mPackage = pkgName; mUsageEventListener.onUsageEvent(userId, event); diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java index 9d56a36196bb..5e11e17f9414 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java @@ -18,6 +18,7 @@ package com.android.server.rollback; import static com.google.common.truth.Truth.assertThat; +import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; import android.content.rollback.PackageRollbackInfo; import android.util.SparseIntArray; @@ -81,7 +82,8 @@ public class RollbackStoreTest { + "'installedUsers':[55,79]," + "'ceSnapshotInodes':[]}],'isStaged':false,'causePackages':[{'packageName':'hello'," + "'longVersionCode':23},{'packageName':'something','longVersionCode':999}]," - + "'committedSessionId':45654465},'timestamp':'2019-10-01T12:29:08.855Z'," + + "'committedSessionId':45654465, 'rollbackImpactLevel':1}," + + "'timestamp':'2019-10-01T12:29:08.855Z'," + "'originalSessionId':567,'state':'enabling','apkSessionId':-1," + "'restoreUserDataInProgress':true, 'userId':0," + "'installerPackageName':'some.installer'}"; @@ -138,6 +140,8 @@ public class RollbackStoreTest { assertThat(rollback.getOriginalSessionId()).isEqualTo(567); assertThat(rollback.info.getRollbackId()).isEqualTo(ID); assertThat(rollback.info.getPackages()).isEmpty(); + assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_LOW); assertThat(rollback.isEnabling()).isTrue(); assertThat(rollback.getExtensionVersions().toString()) .isEqualTo(extensionVersions.toString()); @@ -158,6 +162,8 @@ public class RollbackStoreTest { assertThat(rollback.info.getRollbackId()).isEqualTo(ID); assertThat(rollback.info.getPackages()).isEmpty(); + assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_LOW); assertThat(rollback.isEnabling()).isTrue(); assertThat(rollback.getExtensionVersions().toString()) .isEqualTo(extensionVersions.toString()); @@ -175,6 +181,7 @@ public class RollbackStoreTest { origRb.info.getCausePackages().add(new VersionedPackage("com.made.up", 2)); origRb.info.getCausePackages().add(new VersionedPackage("com.pack.age", 99)); origRb.info.setCommittedSessionId(123456); + origRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH); PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("com.made.up", 18), @@ -226,6 +233,7 @@ public class RollbackStoreTest { expectedRb.info.getCausePackages().add(new VersionedPackage("hello", 23)); expectedRb.info.getCausePackages().add(new VersionedPackage("something", 999)); expectedRb.info.setCommittedSessionId(45654465); + expectedRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH); PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("blah", 55), new VersionedPackage("blah1", 50), new ArrayList<>(), new ArrayList<>(), diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java index ea948ca0e28b..863cda4905f1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java @@ -118,12 +118,19 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { // Constructors that should be used to create instances of specific classes. Overrides scoring. private static final ImmutableMap<Class<?>, Constructor<?>> PREFERRED_CONSTRUCTORS; + // Setter methods that receive String parameters, but where those Strings represent Uris + // (and are visited/validated). + private static final ImmutableSet<Method> SETTERS_WITH_STRING_AS_URI; + static { try { PREFERRED_CONSTRUCTORS = ImmutableMap.of( Notification.Builder.class, Notification.Builder.class.getConstructor(Context.class, String.class)); + SETTERS_WITH_STRING_AS_URI = ImmutableSet.of( + Person.Builder.class.getMethod("setUri", String.class)); + EXCLUDED_SETTERS_OVERLOADS = ImmutableMultimap.<Class<?>, Method>builder() .put(RemoteViews.class, // b/245950570: Tries to connect to service and will crash. @@ -257,7 +264,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { @Nullable Class<?> styleClass, @Nullable Class<?> extenderClass, @Nullable Class<?> actionExtenderClass, boolean includeRemoteViews) { SpecialParameterGenerator specialGenerator = new SpecialParameterGenerator(context); - Set<Class<?>> excludedClasses = includeRemoteViews + ImmutableSet<Class<?>> excludedClasses = includeRemoteViews ? ImmutableSet.of() : ImmutableSet.of(RemoteViews.class); Location location = Location.root(Notification.Builder.class); @@ -294,7 +301,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static Object generateObject(Class<?> clazz, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { if (excludingClasses.contains(clazz)) { throw new IllegalArgumentException( String.format("Asked to generate a %s but it's part of the excluded set (%s)", @@ -369,7 +376,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static Object constructEmpty(Class<?> clazz, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { Constructor<?> bestConstructor; if (PREFERRED_CONSTRUCTORS.containsKey(clazz)) { // Use the preferred constructor. @@ -431,7 +438,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static void invokeAllSetters(Object instance, Location where, boolean allOverloads, - boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes, + boolean includingVoidMethods, ImmutableSet<Class<?>> excludingParameterTypes, SpecialParameterGenerator specialGenerator) { for (Method setter : ReflectionUtils.getAllSetters(instance.getClass(), where, allOverloads, includingVoidMethods, excludingParameterTypes)) { @@ -462,24 +469,34 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static Object[] generateParameters(Executable executable, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { Log.i(TAG, "About to generate parameters for " + ReflectionUtils.methodToString(executable) + " in " + where); Type[] parameterTypes = executable.getGenericParameterTypes(); Object[] parameterValues = new Object[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { - parameterValues[i] = generateParameter( - parameterTypes[i], + boolean generateUriAsString = false; + Type parameterType = parameterTypes[i]; + if (SETTERS_WITH_STRING_AS_URI.contains(executable) + && parameterType.equals(String.class)) { + generateUriAsString = true; + } + Object value = generateParameter( + generateUriAsString ? Uri.class : parameterType, where.plus(executable, String.format("[%d,%s]", i, parameterTypes[i].getTypeName())), excludingClasses, specialGenerator); + if (generateUriAsString) { + value = ((Uri) value).toString(); + } + parameterValues[i] = value; } return parameterValues; } private static Object generateParameter(Type parameterType, Location where, - Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { + ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) { if (parameterType instanceof Class<?> parameterClass) { return generateObject( parameterClass, @@ -487,7 +504,8 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { excludingClasses, specialGenerator); } else if (parameterType instanceof ParameterizedType parameterizedType) { - if (parameterizedType.getRawType().equals(List.class) + if ((parameterizedType.getRawType().equals(List.class) + || parameterizedType.getRawType().equals(ArrayList.class)) && parameterizedType.getActualTypeArguments()[0] instanceof Class<?>) { ArrayList listValue = new ArrayList(); for (int i = 0; i < NUM_ELEMENTS_IN_ARRAY; i++) { @@ -503,12 +521,14 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static class ReflectionUtils { - static Set<Class<?>> getConcreteSubclasses(Class<?> clazz, Class<?> containerClass) { - return Arrays.stream(containerClass.getDeclaredClasses()) - .filter( - innerClass -> clazz.isAssignableFrom(innerClass) - && !Modifier.isAbstract(innerClass.getModifiers())) - .collect(Collectors.toSet()); + static ImmutableSet<Class<?>> getConcreteSubclasses(Class<?> clazz, + Class<?> containerClass) { + return ImmutableSet.copyOf( + Arrays.stream(containerClass.getDeclaredClasses()) + .filter( + innerClass -> clazz.isAssignableFrom(innerClass) + && !Modifier.isAbstract(innerClass.getModifiers())) + .collect(Collectors.toSet())); } static String methodToString(Executable executable) { @@ -611,9 +631,16 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } private static class SpecialParameterGenerator { + + private static final ImmutableSet<Class<?>> INTERESTING_CLASSES_WITH_SPECIAL_GENERATION = + ImmutableSet.of(Uri.class, Icon.class, Intent.class, PendingIntent.class, + RemoteViews.class); + private static final ImmutableSet<Class<?>> INTERESTING_CLASSES = - ImmutableSet.of(Person.class, Uri.class, Icon.class, Intent.class, - PendingIntent.class, RemoteViews.class); + new ImmutableSet.Builder<Class<?>>() + .addAll(INTERESTING_CLASSES_WITH_SPECIAL_GENERATION) + .add(Person.class) // Constructed via reflection, but high-score. + .build(); private static final ImmutableSet<Class<?>> MOCKED_CLASSES = ImmutableSet.of(); private static final ImmutableMap<Class<?>, Object> PRIMITIVE_VALUES = @@ -637,7 +664,7 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { } static boolean canGenerate(Class<?> clazz) { - return INTERESTING_CLASSES.contains(clazz) + return INTERESTING_CLASSES_WITH_SPECIAL_GENERATION.contains(clazz) || MOCKED_CLASSES.contains(clazz) || clazz.equals(Context.class) || clazz.equals(Bundle.class) @@ -672,17 +699,6 @@ public class NotificationVisitUrisTest extends UiServiceTestCase { return Icon.createWithContentUri(iconUri); } - if (clazz == Person.class) { - // TODO(b/310189261): Person.setUri takes a string instead of a URI. We should - // find a way to use the SpecialParameterGenerator instead of this custom one. - Uri personUri = generateUri( - where.plus(Person.Builder.class).plus("setUri", String.class)); - Uri iconUri = generateUri(where.plus(Person.Builder.class).plus("setIcon", - Icon.class).plus(Icon.class).plus("createWithContentUri", Uri.class)); - return new Person.Builder().setUri(personUri.toString()).setIcon( - Icon.createWithContentUri(iconUri)).setName("John Doe").build(); - } - if (clazz == Intent.class) { return new Intent("action", generateUri(where.plus(Intent.class))); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 248683836336..25ad7dbac30c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -59,6 +59,7 @@ import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN; import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS; @@ -123,6 +124,7 @@ import android.os.Parcel; import android.os.Process; import android.os.SimpleClock; import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; @@ -238,15 +240,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { public TestWithLooperRule mLooperRule = new TestWithLooperRule(); ConditionProviders mConditionProviders; - @Mock NotificationManager mNotificationManager; - @Mock PackageManager mPackageManager; + @Mock + NotificationManager mNotificationManager; + @Mock + PackageManager mPackageManager; private Resources mResources; private TestableLooper mTestableLooper; private final TestClock mTestClock = new TestClock(); private ZenModeHelper mZenModeHelper; private ContentResolver mContentResolver; - @Mock DeviceEffectsApplier mDeviceEffectsApplier; - @Mock AppOpsManager mAppOps; + @Mock + DeviceEffectsApplier mDeviceEffectsApplier; + @Mock + AppOpsManager mAppOps; TestableFlagResolver mTestFlagResolver = new TestableFlagResolver(); ZenModeEventLoggerFake mZenModeEventLogger; @@ -290,7 +296,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mPackageManager.getPackageUidAsUser(eq(CUSTOM_PKG_NAME), anyInt())) .thenReturn(CUSTOM_PKG_UID); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( - new String[] {pkg}); + new String[]{pkg}); ApplicationInfo appInfoSpy = spy(new ApplicationInfo()); appInfoSpy.icon = ICON_RES_ID; @@ -305,24 +311,26 @@ public class ZenModeHelperTest extends UiServiceTestCase { } private XmlResourceParser getDefaultConfigParser() throws IOException, XmlPullParserException { - String xml = "<zen version=\"8\" user=\"0\">\n" - + "<allow calls=\"false\" repeatCallers=\"false\" messages=\"true\" " - + "reminders=\"false\" events=\"false\" callsFrom=\"1\" messagesFrom=\"2\" " - + "visualScreenOff=\"true\" alarms=\"true\" " - + "media=\"true\" system=\"false\" conversations=\"true\"" - + " conversationsFrom=\"2\"/>\n" - + "<automatic ruleId=\"" + EVENTS_DEFAULT_RULE_ID - + "\" enabled=\"false\" snoozing=\"false\"" - + " name=\"Event\" zen=\"1\"" - + " component=\"android/com.android.server.notification.EventConditionProvider\"" - + " conditionId=\"condition://android/event?userId=-10000&calendar=&" + String xml = "<zen version=\"10\">\n" + + "<allow alarms=\"true\" media=\"true\" system=\"false\" calls=\"true\" " + + "callsFrom=\"2\" messages=\"true\"\n" + + "messagesFrom=\"2\" reminders=\"false\" events=\"false\" " + + "repeatCallers=\"true\" convos=\"true\"\n" + + "convosFrom=\"2\"/>\n" + + "<automatic ruleId=" + EVENTS_DEFAULT_RULE_ID + + " enabled=\"false\" snoozing=\"false\"" + + " name=\"Event\" zen=\"1\"\n" + + " component=\"android/com.android.server.notification.EventConditionProvider\"\n" + + " conditionId=\"condition://android/event?userId=-10000&calendar=&" + "reply=1\"/>\n" - + "<automatic ruleId=\"" + SCHEDULE_DEFAULT_RULE_ID + "\" enabled=\"false\"" - + " snoozing=\"false\" name=\"Sleeping\" zen=\"1\"" - + " component=\"android/com.android.server.notification.ScheduleConditionProvider\"" - + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7 &start=22.0" - + "&end=7.0&exitAtAlarm=true\"/>" - + "<disallow visualEffects=\"511\" />" + + "<automatic ruleId=" + SCHEDULE_DEFAULT_RULE_ID + " enabled=\"false\"" + + " snoozing=\"false\" name=\"Sleeping\"\n zen=\"1\"" + + " component=\"android/com.android.server.notification" + + ".ScheduleConditionProvider\"\n" + + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7&start=22.0" + + "&end=7.0&exitAtAlarm=true\"/>\n" + + "<disallow visualEffects=\"157\" />\n" + + "<state areChannelsBypassingDnd=\"false\" />\n" + "</zen>"; TypedXmlPullParser parser = Xml.newFastPullParser(); parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), null); @@ -408,7 +416,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOff_NoMuteApplied() { mZenModeHelper.mZenMode = ZEN_MODE_OFF; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -421,7 +429,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_NotificationApplied() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); // The most permissive policy mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES @@ -442,7 +450,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_StarredCallers_CallTypesBlocked() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); // The most permissive policy mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES @@ -462,7 +470,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_AllCallers_CallTypesAllowed() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); // The most permissive policy mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES @@ -481,7 +489,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_AllowAlarmsMedia_NoAlarmMediaMuteApplied() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0); @@ -493,7 +501,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testZenOn_DisallowAlarmsMedia_AlarmMediaMuteApplied() { mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); verifyApplyRestrictions(true, true, AudioAttributes.USAGE_ALARM); @@ -506,7 +514,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testTotalSilence() { mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS | PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -525,7 +533,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testAlarmsOnly_alarmMediaMuteNotApplied() { mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -545,7 +553,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testAlarmsOnly_callsMuteApplied() { mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -559,7 +567,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testAlarmsOnly_allZenConfigToggledCannotBypass_alarmMuteNotApplied() { // Only audio attributes with SUPPRESIBLE_NEVER can bypass mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -571,7 +579,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Only audio attributes with SUPPRESIBLE_NEVER can bypass // with special case USAGE_ASSISTANCE_SONIFICATION mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -592,7 +600,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testApplyRestrictions_whitelist_priorityOnlyMode() { - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -607,7 +615,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testApplyRestrictions_whitelist_alarmsOnlyMode() { - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mZenMode = Global.ZEN_MODE_ALARMS; mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -622,7 +630,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testApplyRestrictions_whitelist_totalSilenceMode() { - mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O}); + mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O}); mZenModeHelper.mZenMode = Global.ZEN_MODE_NO_INTERRUPTIONS; mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0); mZenModeHelper.applyRestrictions(); @@ -1007,7 +1015,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); assertEquals("Config mismatch: current vs expected: " - + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected, + + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected, mZenModeHelper.mConfig); } @@ -1336,7 +1344,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM); assertEquals("Config mismatch: current vs original: " - + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original), + + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original), original, mZenModeHelper.mConfig); assertEquals(original.hashCode(), mZenModeHelper.mConfig.hashCode()); } @@ -1778,6 +1786,225 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_onModesApi_noUpgrade() throws Exception { + // When reading XML for something that is already on the modes API system, make sure no + // rules' policies get changed. + setupZenConfig(); + + // Shared for rules + ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRules = new ArrayMap<>(); + final ScheduleInfo weeknights = new ScheduleInfo(); + + // Custom rule with a custom policy + ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); + customRule.enabled = true; + customRule.name = "Custom Rule"; + customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights); + customRule.component = new ComponentName("android", "ScheduleConditionProvider"); + ZenPolicy policy = new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_CONTACTS) + .allowAlarms(true) + .allowRepeatCallers(false) + .build(); + // Fill in policy fields, since on modes api we do not expect any rules to have unset fields + customRule.zenPolicy = mZenModeHelper.getDefaultZenPolicy().overwrittenWith(policy); + enabledAutoRules.put("customRule", customRule); + mZenModeHelper.mConfig.automaticRules = enabledAutoRules; + + // set version to post-modes-API = 11 + ByteArrayOutputStream baos = writeXmlAndPurge(11); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // basic check: global config maintained + setupZenConfigMaintained(); + + // Find our automatic rules. + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules).hasSize(1); + assertThat(rules).containsKey("customRule"); + ZenRule rule = rules.get("customRule"); + assertThat(rule.zenPolicy).isEqualTo(customRule.zenPolicy); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_upgradeToModesApi_makesCustomPolicies() throws Exception { + // When reading in an XML file written from a pre-modes-API version, confirm that we create + // a custom policy matching the global config for any automatic rule with no specified + // policy. + setupZenConfig(); + + ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>(); + ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo weeknights = new ScheduleInfo(); + customRule.enabled = true; + customRule.name = "Custom Rule"; + customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights); + customRule.component = new ComponentName("android", "ScheduleConditionProvider"); + enabledAutoRule.put("customRule", customRule); // no custom policy set + mZenModeHelper.mConfig.automaticRules = enabledAutoRule; + + // set version to pre-modes-API = 10 + ByteArrayOutputStream baos = writeXmlAndPurge(10); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // basic check: global config maintained + setupZenConfigMaintained(); + + // Find our automatic rule and check that it has a policy set now + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules).hasSize(1); + assertThat(rules).containsKey("customRule"); + ZenRule rule = rules.get("customRule"); + assertThat(rule.zenPolicy).isNotNull(); + + // Check policy values as set up in setupZenConfig() to confirm they match + assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_upgradeToModesApi_fillsInCustomPolicies() throws Exception { + // When reading in an XML file written from a pre-modes-API version, confirm that for an + // underspecified ZenPolicy, we fill in all of the gaps with things from the global config + // in order to maintain consistency of behavior. + setupZenConfig(); + + ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>(); + ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo weeknights = new ScheduleInfo(); + customRule.enabled = true; + customRule.name = "Custom Rule"; + customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights); + customRule.component = new ComponentName("android", "ScheduleConditionProvider"); + customRule.zenPolicy = new ZenPolicy.Builder() + .allowAlarms(true) + .allowMedia(true) + .allowRepeatCallers(false) + .build(); + enabledAutoRule.put("customRule", customRule); + mZenModeHelper.mConfig.automaticRules = enabledAutoRule; + + // set version to pre-modes-API = 10 + ByteArrayOutputStream baos = writeXmlAndPurge(10); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // basic check: global config maintained + setupZenConfigMaintained(); + + // Find our automatic rule and check that it has a policy set now + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules).hasSize(1); + assertThat(rules).containsKey("customRule"); + ZenRule rule = rules.get("customRule"); + assertThat(rule.zenPolicy).isNotNull(); + + // Check unset policy values match values in setupZenConfig(). + // Check that set policy values match the values set in the policy. + assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_DISALLOW); + + // Check that the rest is filled in from the default + assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testReadXml_upgradeToModesApi_existingDefaultRulesGetCustomPolicy() + throws Exception { + setupZenConfig(); + + // Default rules, if they exist and have no policies, should get a snapshot of the global + // policy, even if they are disabled upon upgrade. + ArrayMap<String, ZenModeConfig.ZenRule> automaticRules = new ArrayMap<>(); + ZenModeConfig.ZenRule defaultScheduleRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo defaultScheduleRuleInfo = new ScheduleInfo(); + defaultScheduleRule.enabled = false; + defaultScheduleRule.name = "Default Schedule Rule"; + defaultScheduleRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + defaultScheduleRule.conditionId = ZenModeConfig.toScheduleConditionId( + defaultScheduleRuleInfo); + defaultScheduleRule.id = ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID; + automaticRules.put(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, defaultScheduleRule); + + ZenModeConfig.ZenRule defaultEventRule = new ZenModeConfig.ZenRule(); + final ScheduleInfo defaultEventRuleInfo = new ScheduleInfo(); + defaultEventRule.enabled = false; + defaultEventRule.name = "Default Event Rule"; + defaultEventRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + defaultEventRule.conditionId = ZenModeConfig.toScheduleConditionId( + defaultEventRuleInfo); + defaultEventRule.id = ZenModeConfig.EVENTS_DEFAULT_RULE_ID; + automaticRules.put(ZenModeConfig.EVENTS_DEFAULT_RULE_ID, defaultEventRule); + + mZenModeHelper.mConfig.automaticRules = automaticRules; + + // set previous version + ByteArrayOutputStream baos = writeXmlAndPurge(10); + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(new BufferedInputStream( + new ByteArrayInputStream(baos.toByteArray())), null); + parser.nextTag(); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + + // check default rules + ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; + assertThat(rules.size()).isGreaterThan(0); + for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) { + assertThat(rules).containsKey(defaultId); + ZenRule rule = rules.get(defaultId); + assertThat(rule.zenPolicy).isNotNull(); + + // Check policy values as set up in setupZenConfig() to confirm they match + assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW); + assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW); + } + } + + @Test public void testCountdownConditionSubscription() throws Exception { ZenModeConfig config = new ZenModeConfig(); mZenModeHelper.mConfig = config; @@ -2036,6 +2263,69 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testAddAutomaticZenRule_modesApi_fillsInDefaultValues() { + // When a new automatic zen rule is added with only some fields filled in, ensure that + // all unset fields are filled in with device defaults. + + // Zen rule with null policy: should get entirely the default state + AutomaticZenRule zenRule1 = new AutomaticZenRule("name", + new ComponentName("android", "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id1 = mZenModeHelper.addAutomaticZenRule("android", zenRule1, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // Zen rule with partially-filled policy: should get all of the filled fields set, and the + // rest filled with default state + AutomaticZenRule zenRule2 = new AutomaticZenRule("name", + null, + new ComponentName(mContext.getPackageName(), "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + new ZenPolicy.Builder() + .allowCalls(PEOPLE_TYPE_NONE) + .allowMessages(PEOPLE_TYPE_CONTACTS) + .showFullScreenIntent(true) + .build(), + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // rule 1 should exist + assertThat(id1).isNotNull(); + ZenModeConfig.ZenRule rule1InConfig = mZenModeHelper.mConfig.automaticRules.get(id1); + assertThat(rule1InConfig).isNotNull(); + assertThat(rule1InConfig.zenPolicy).isNotNull(); // we passed in null; it should now not be + + // all of rule 1 should be the device default's policy + assertThat(rule1InConfig.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy()); + + // rule 2 should exist + assertThat(id2).isNotNull(); + ZenModeConfig.ZenRule rule2InConfig = mZenModeHelper.mConfig.automaticRules.get(id2); + assertThat(rule2InConfig).isNotNull(); + + // rule 2: values set from the policy itself + assertThat(rule2InConfig.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(rule2InConfig.zenPolicy.getPriorityMessageSenders()) + .isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(rule2InConfig.zenPolicy.getVisualEffectFullScreenIntent()) + .isEqualTo(ZenPolicy.STATE_ALLOW); + + // the rest of rule 2's settings should be the device defaults + assertThat(rule2InConfig.zenPolicy.getPriorityConversationSenders()) + .isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + assertThat(rule2InConfig.zenPolicy.getPriorityCategorySystem()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule2InConfig.zenPolicy.getPriorityCategoryAlarms()) + .isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(rule2InConfig.zenPolicy.getVisualEffectPeek()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule2InConfig.zenPolicy.getVisualEffectNotificationList()) + .isEqualTo(ZenPolicy.STATE_ALLOW); + } + + @Test public void testSetAutomaticZenRuleState_nullPkg() { AutomaticZenRule zenRule = new AutomaticZenRule("name", null, @@ -2357,6 +2647,68 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) + public void updateAutomaticZenRule_nullPolicy_doesNothing() { + // Test that when updateAutomaticZenRule is called with a null policy, nothing changes + // about the existing policy. + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + .setOwner(OWNER) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars + .build()) + .build(), + UPDATE_ORIGIN_APP, "reasons", 0); + + mZenModeHelper.updateAutomaticZenRule(ruleId, + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + // no zen policy + .build(), + UPDATE_ORIGIN_APP, "reasons", 0); + + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void updateAutomaticZenRule_overwritesExistingPolicy() { + // Test that when updating an automatic zen rule with an existing policy, the newly set + // fields overwrite those from the previous policy, but unset fields in the new policy + // keep values from the previous one. + String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + .setOwner(OWNER) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars + .allowAlarms(false) + .allowReminders(true) + .build()) + .build(), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0); + + mZenModeHelper.updateAutomaticZenRule(ruleId, + new AutomaticZenRule.Builder("Rule", CONDITION_ID) + .setZenPolicy(new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) + .build()) + .build(), + UPDATE_ORIGIN_APP, "reasons", 0); + + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from update + assertThat(savedRule.getZenPolicy().getPriorityCallSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from update + assertThat(savedRule.getZenPolicy().getPriorityCategoryAlarms()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from original + assertThat(savedRule.getZenPolicy().getPriorityCategoryReminders()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from original + } + + + @Test + @EnableFlags(Flags.FLAG_MODES_API) public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() { ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS, ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID); @@ -2460,7 +2812,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI); private final boolean mEnabled; - @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi; + @ConfigChangeOrigin + private final int mOriginForUserActionInSystemUi; ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) { this.mEnabled = enabled; @@ -2506,7 +2859,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // - rules active = 1 // - user action = true (system-based turning zen mode on) // - package uid = system (as set above) - // - resulting DNDPolicyProto the same as the values in setupZenConfig() + // - resulting DNDPolicyProto the same as the values in setupZenConfig() (global policy) assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); @@ -2600,7 +2953,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // - 1 rule (newly) active // - automatic (is not a user action) // - package UID is written to be the rule *owner* even though it "comes from system" - // - zen policy is the same as the set-up zen config + // - zen policy is the default as it's unspecified assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); @@ -2609,10 +2962,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertFalse(mZenModeEventLogger.getIsUserAction(0)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); // When the automatic rule is disabled, this should turn off zen mode and also count as a - // user action. + // user action. We don't care what the consolidated policy is when DND turns off. assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(), mZenModeEventLogger.getEventId(1)); assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1)); @@ -2835,28 +3188,28 @@ public class ZenModeHelperTest extends UiServiceTestCase { // First: turn on rule 1 mZenModeHelper.setAutomaticZenRuleState(id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Second: turn on rule 2 mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Third: turn on rule 3 mZenModeHelper.setAutomaticZenRuleState(id3, new Condition(zenRule3.getConditionId(), "", STATE_TRUE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // Fourth: Turn *off* rule 2 mZenModeHelper.setAutomaticZenRuleState(id2, new Condition(zenRule2.getConditionId(), "", STATE_FALSE), - UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); // This should result in a total of four events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); // Event 1: rule 1 turns on. We expect this to turn on DND (zen mode) overall, so that's - // what the event should reflect. At this time, the policy is the same as initial setup. + // what the event should reflect. At this time, the policy is the default. assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(), mZenModeEventLogger.getEventId(0)); assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0)); @@ -2864,7 +3217,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(1, mZenModeEventLogger.getNumRulesActive(0)); assertFalse(mZenModeEventLogger.getIsUserAction(0)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0)); - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0)); + checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0)); // Event 2: rule 2 turns on. This should not change anything about the policy, so the only // change is that there are more rules active now. @@ -2873,7 +3226,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(2, mZenModeEventLogger.getNumRulesActive(1)); assertFalse(mZenModeEventLogger.getIsUserAction(1)); assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1)); - checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1)); + checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(1)); // Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such, // but meanwhile also change the number of active rules. @@ -2926,12 +3279,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true); setupZenConfig(); + // Explicitly set up all rules with the same policy as the manual rule so there will be + // no policy changes in this test case. + ZenPolicy manualRulePolicy = mZenModeHelper.mConfig.toZenPolicy(); + // Rule 1, owned by a package AutomaticZenRule zenRule = new AutomaticZenRule("name", null, new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - null, + manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID); @@ -2941,7 +3298,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, new ComponentName("android", "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), - null, + manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID); @@ -3172,7 +3529,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testUpdateConsolidatedPolicy_defaultRulesOnly() { + @DisableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() { setupZenConfig(); // When there's one automatic rule active and it doesn't specify a policy, test that the @@ -3205,12 +3563,39 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testUpdateConsolidatedPolicy_customPolicyOnly() { + @EnableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDeviceDefault() { + setupZenConfig(); + + // When there's one automatic rule active and it doesn't specify a policy, test that the + // resulting consolidated policy is one that matches the default *device* settings. + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, // null policy + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable the rule + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // inspect the consolidated policy, which should match the device default settings. + assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy)) + .isEqualTo(mZenModeHelper.getDefaultZenPolicy()); + } + + @Test + @DisableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_preModesApiCustomPolicyOnly_fillInWithGlobal() { setupZenConfig(); // when there's only one automatic rule active and it has a custom policy, make sure that's - // what the consolidated policy reflects whether or not it's stricter than what the default - // would specify. + // what the consolidated policy reflects whether or not it's stricter than what the global + // config would specify. ZenPolicy customPolicy = new ZenPolicy.Builder() .allowAlarms(true) // more lenient than default .allowMedia(true) // more lenient than default @@ -3249,7 +3634,51 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void testUpdateConsolidatedPolicy_defaultAndCustomActive() { + @EnableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDeviceDefault() { + setupZenConfig(); + + // when there's only one automatic rule active and it has a custom policy, make sure that's + // what the consolidated policy reflects whether or not it's stricter than what the default + // would specify. + ZenPolicy customPolicy = new ZenPolicy.Builder() + .allowSystem(true) // more lenient than default + .allowRepeatCallers(false) // more restrictive than default + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default + .showFullScreenIntent(true) // more lenient + .showBadges(false) // more restrictive + .build(); + + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + customPolicy, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable the rule; this will update the consolidated policy + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // since this is the only active rule, the consolidated policy should match the custom + // policy for every field specified, and take default values for unspecified things + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()).isFalse(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom + assertThat(mZenModeHelper.mConsolidatedPolicy.showFullScreenIntents()).isTrue(); // custom + } + + @Test + @DisableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_preModesApiDefaultAndCustomActive_mergesWithGlobal() { setupZenConfig(); // when there are two rules active, one inheriting the default policy and one setting its @@ -3309,6 +3738,68 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() { + setupZenConfig(); + + // when there are two rules active, one inheriting the default policy and one setting its + // own custom policy, they should be merged to form the most restrictive combination. + + // rule 1: no custom policy + AutomaticZenRule zenRule = new AutomaticZenRule("name", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable rule 1 + mZenModeHelper.setAutomaticZenRuleState(id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // custom policy for rule 2 + ZenPolicy customPolicy = new ZenPolicy.Builder() + .allowAlarms(false) // more restrictive than default + .allowSystem(true) // more lenient than default + .allowRepeatCallers(false) // more restrictive than default + .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default + .showBadges(false) // more restrictive + .showPeeking(true) // more lenient + .build(); + + AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", + null, + new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"), + ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), + customPolicy, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID); + + // enable rule 2; this will update the consolidated policy + mZenModeHelper.setAutomaticZenRuleState(id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), + UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID); + + // now both rules should be on, and the consolidated policy should reflect the most + // restrictive option of each of the two + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()) + .isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom stricter + assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isFalse(); // default stricter + } + + @Test public void testUpdateConsolidatedPolicy_allowChannels() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); setupZenConfig(); @@ -3372,7 +3863,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { null, new ComponentName(CUSTOM_PKG_NAME, "cls"), Uri.parse("priority"), - new ZenPolicy.Builder().allowMedia(true).build(), + new ZenPolicy.Builder() + .allowMedia(true) + .allowSystem(true) + .build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); String rule1Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRuleWithPriority, UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID); @@ -3394,10 +3888,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { UPDATE_ORIGIN_APP, CUSTOM_PKG_UID); // Consolidated Policy should be default + rule1. - assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // priority rule - assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default - assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isTrue(); // default + assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // priority rule + assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default @@ -3408,7 +3902,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void zenRuleToAutomaticZenRule_allFields() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( - new String[] {OWNER.getPackageName()}); + new String[]{OWNER.getPackageName()}); ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.configurationActivity = CONFIG_ACTIVITY; @@ -3452,7 +3946,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void automaticZenRuleToZenRule_allFields() { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); when(mPackageManager.getPackagesForUid(anyInt())).thenReturn( - new String[] {OWNER.getPackageName()}); + new String[]{OWNER.getPackageName()}); AutomaticZenRule azr = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setEnabled(true) @@ -3478,7 +3972,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(CONDITION_ID, storedRule.conditionId); assertEquals(INTERRUPTION_FILTER_ZR, storedRule.zenMode); assertEquals(ENABLED, storedRule.enabled); - assertEquals(POLICY, storedRule.zenPolicy); + assertEquals(mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY), + storedRule.zenPolicy); assertEquals(CONFIG_ACTIVITY, storedRule.configurationActivity); assertEquals(TYPE, storedRule.type); assertEquals(ALLOW_MANUAL, storedRule.allowManualInvocation); @@ -3561,12 +4056,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Modifies the zen policy and device effects ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) - .allowPriorityChannels(true) + .allowPriorityChannels(false) .build(); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects()) - .setShouldDisplayGrayscale(true) - .build(); + .setShouldDisplayGrayscale(true) + .build(); AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(policy) @@ -3580,7 +4075,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -3770,9 +4266,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) public void updateAutomaticZenRule_nullPolicyUpdate() { - // Adds a starting rule with empty zen policies and device effects + // Adds a starting rule with set zen policy and empty device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) - .setZenPolicy(new ZenPolicy.Builder().build()) + .setZenPolicy(POLICY) .build(); // Adds the rule using the app, to avoid having any user modified bits set. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), @@ -3790,8 +4286,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); - // When AZR's ZenPolicy is null, we expect the updated rule's policy to be null. - assertThat(rule.getZenPolicy()).isNull(); + // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged + // (equivalent to the provided policy, with additional fields filled in with defaults). + assertThat(rule.getZenPolicy()).isEqualTo( + mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY)); } @Test @@ -3847,13 +4345,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(storedRule.canBeUpdatedByApp()).isFalse(); assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_ALLOW_CHANNELS - | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS - | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS - | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM - | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT - | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS - | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK - | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT + | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS + | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS + | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM + | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT + | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS + | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK + | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT ); } @@ -4016,6 +4514,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { final int[] actualStatus = new int[2]; ZenModeHelper.Callback callback = new ZenModeHelper.Callback() { int i = 0; + @Override void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) { if (Objects.equals(createdId, id)) { @@ -4056,6 +4555,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { final int[] actualStatus = new int[2]; ZenModeHelper.Callback callback = new ZenModeHelper.Callback() { int i = 0; + @Override void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) { if (Objects.equals(createdId, id)) { @@ -4202,6 +4702,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build()), eq(UPDATE_ORIGIN_APP)); } + @Test public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); @@ -4607,7 +5108,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, - null, true)); + mZenModeHelper.mConfig.toZenPolicy(), // copy of global config + true)); } @Test @@ -4626,7 +5128,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(mZenModeHelper.mConfig.automaticRules.values()) .comparingElementsUsing(IGNORE_METADATA) .containsExactly( - expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true)); + expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, + mZenModeHelper.mConfig.toZenPolicy(), // copy of global config + true)); } @Test @@ -4820,6 +5324,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + // Store this for checking later. + ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder( + mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build(); + // From user, update that rule's policy. AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds() @@ -4839,7 +5347,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(pkg, ZEN_MODE_IMPORTANT_INTERRUPTIONS, - userUpdateZenPolicy, + // the final policy for the rule should contain the user's update + // overlaid on top of the original existing policy. + originalEffectiveZenPolicy.overwrittenWith(userUpdateZenPolicy), /* conditionActive= */ null)); } @@ -4854,6 +5364,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + // Store this for checking later. + ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder( + mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build(); + // From user, update something in that rule, but not the ZenPolicy. AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) @@ -4873,7 +5387,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowPriorityChannels(true) .build(); assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenPolicy) - .isEqualTo(appsSecondZenPolicy); + .isEqualTo(originalEffectiveZenPolicy.overwrittenWith(appsSecondZenPolicy)); } @Test @@ -4905,13 +5419,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test - public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() { + public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + mZenModeHelper.mConfig.allowCalls = true; + mZenModeHelper.mConfig.allowConversations = false; + // Implicit rule will get the global policy at the time of rule creation. mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_ALARMS); - mZenModeHelper.mConfig.allowCalls = true; - mZenModeHelper.mConfig.allowConversations = false; + + // If the policy then changes afterwards, we should keep the snapshotted version. + mZenModeHelper.mConfig.allowCalls = false; Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( CUSTOM_PKG_NAME); @@ -4952,7 +5470,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { p.recycle(); } }, - "Ignoring timestamp and userModifiedFields"); + "Ignoring timestamp and userModifiedFields"); private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { @@ -4962,7 +5480,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { if (conditionActive != null) { rule.condition = conditionActive ? new Condition(rule.conditionId, - mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE) + mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE) : new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_deactivated), STATE_FALSE); @@ -5030,8 +5548,35 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber()); } + private void checkDndProtoMatchesDefaultZenConfig(DNDPolicyProto dndProto) { + if (!Flags.modesApi()) { + checkDndProtoMatchesSetupZenConfig(dndProto); + return; + } + + // When modes_api flag is on, the default zen config is the device defaults. + assertThat(dndProto.getAlarms().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getMedia().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getSystem().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getReminders().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getCalls().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAllowCallsFrom().getNumber()).isEqualTo(PEOPLE_STARRED); + assertThat(dndProto.getMessages().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAllowMessagesFrom().getNumber()).isEqualTo(PEOPLE_STARRED); + assertThat(dndProto.getEvents().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getRepeatCallers().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getFullscreen().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getLights().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getPeek().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getStatusBar().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getBadge().getNumber()).isEqualTo(STATE_ALLOW); + assertThat(dndProto.getAmbient().getNumber()).isEqualTo(STATE_DISALLOW); + assertThat(dndProto.getNotificationList().getNumber()).isEqualTo(STATE_ALLOW); + } + private static void withoutWtfCrash(Runnable test) { - Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {}); + Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> { + }); try { test.run(); } finally { 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 4ed55df7775c..57e11328d5e1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -269,6 +269,78 @@ public class ZenPolicyTest extends UiServiceTestCase { } @Test + public void testZenPolicyOverwrite_allUnsetPolicies() { + mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + + ZenPolicy.Builder builder = new ZenPolicy.Builder(); + ZenPolicy unset = builder.build(); + + builder.allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS); + builder.allowMedia(false); + builder.allowEvents(true); + builder.showFullScreenIntent(false); + builder.showInNotificationList(false); + ZenPolicy set = builder.build(); + + ZenPolicy overwritten = set.overwrittenWith(unset); + assertThat(overwritten).isEqualTo(set); + + // should actually work the other way too. + ZenPolicy overwrittenWithSet = unset.overwrittenWith(set); + assertThat(overwrittenWithSet).isEqualTo(set); + } + + @Test + public void testZenPolicyOverwrite_someOverlappingFields_takeNewPolicy() { + mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); + + ZenPolicy p1 = new ZenPolicy.Builder() + .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) + .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED) + .allowMedia(false) + .showBadges(true) + .build(); + + ZenPolicy p2 = new ZenPolicy.Builder() + .allowRepeatCallers(false) + .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT) + .allowMessages(ZenPolicy.PEOPLE_TYPE_NONE) + .showBadges(false) + .showPeeking(true) + .build(); + + // when p1 is overwritten with p2, all values from p2 win regardless of strictness, and + // remaining fields take values from p1. + ZenPolicy p1OverwrittenWithP2 = p1.overwrittenWith(p2); + assertThat(p1OverwrittenWithP2.getPriorityCallSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from p1 + assertThat(p1OverwrittenWithP2.getPriorityMessageSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE); // from p2 + assertThat(p1OverwrittenWithP2.getPriorityCategoryRepeatCallers()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2 + assertThat(p1OverwrittenWithP2.getPriorityCategoryMedia()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p1 + assertThat(p1OverwrittenWithP2.getVisualEffectBadge()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2 + assertThat(p1OverwrittenWithP2.getVisualEffectPeek()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2 + + ZenPolicy p2OverwrittenWithP1 = p2.overwrittenWith(p1); + assertThat(p2OverwrittenWithP1.getPriorityCallSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from p1 + assertThat(p2OverwrittenWithP1.getPriorityMessageSenders()) + .isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED); // from p1 + assertThat(p2OverwrittenWithP1.getPriorityCategoryRepeatCallers()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2 + assertThat(p2OverwrittenWithP1.getPriorityCategoryMedia()) + .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p1 + assertThat(p2OverwrittenWithP1.getVisualEffectBadge()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from p1 + assertThat(p2OverwrittenWithP1.getVisualEffectPeek()) + .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2 + } + + @Test public void testZenPolicyMessagesInvalid() { ZenPolicy.Builder builder = new ZenPolicy.Builder(); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index ba7b52e368f3..2a89b02482b3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -1762,32 +1762,6 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(1, task.getChildCount()); } - /** - * Test that an activity will not be destroyed if it is marked as non-destroyable. - */ - @Test - public void testSafelyDestroy_nonDestroyable() { - final ActivityRecord activity = createActivityWithTask(); - doReturn(false).when(activity).isDestroyable(); - - activity.safelyDestroy("test"); - - verify(activity, never()).destroyImmediately(anyString()); - } - - /** - * Test that an activity will not be destroyed if it is marked as non-destroyable. - */ - @Test - public void testSafelyDestroy_destroyable() { - final ActivityRecord activity = createActivityWithTask(); - doReturn(true).when(activity).isDestroyable(); - - activity.safelyDestroy("test"); - - verify(activity).destroyImmediately(anyString()); - } - @Test public void testRemoveImmediately() { final Consumer<Consumer<ActivityRecord>> test = setup -> { 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 1c57623442c5..29faed195b4e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java @@ -132,8 +132,10 @@ import org.junit.runner.RunWith; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; /** @@ -563,6 +565,86 @@ public class ActivityStarterTests extends WindowTestsBase { return Pair.create(splitPrimaryActivity, splitSecondActivity); } + /** + * This test ensures that if the intent is being delivered to a desktop mode unfocused task + * while it is already on top, reports it as delivering to top. + */ + @Test + public void testDesktopModeDeliverToTop() { + final ActivityStarter starter = prepareStarter( + FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP, + false /* mockGetRootTask */); + final List<ActivityRecord> activities = createActivitiesInDesktopMode(); + + // Set focus back to the first task. + activities.get(0).moveFocusableActivityToTop("testDesktopModeDeliverToTop"); + + // Start activity and delivered new intent. + starter.getIntent().setComponent(activities.get(3).mActivityComponent); + doReturn(activities.get(3)).when(mRootWindowContainer).findTask(any(), any()); + final int result = starter.setReason("testDesktopModeDeliverToTop").execute(); + + // Ensure result is delivering intent to top. + assertEquals(START_DELIVERED_TO_TOP, result); + } + + /** + * This test ensures that if the intent is being delivered to a desktop mode unfocused task + * reports it is brought to front instead of delivering to top. + */ + @Test + public void testDesktopModeTaskToFront() { + final ActivityStarter starter = prepareStarter( + FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP, false); + final List<ActivityRecord> activities = createActivitiesInDesktopMode(); + final ActivityRecord desktopModeFocusActivity = activities.get(0); + final ActivityRecord desktopModeReusableActivity = activities.get(1); + final ActivityRecord desktopModeTopActivity = new ActivityBuilder(mAtm).setCreateTask(true) + .setParentTask(desktopModeReusableActivity.getRootTask()).build(); + assertTrue(desktopModeTopActivity.inMultiWindowMode()); + + // Let first stack has focus. + desktopModeFocusActivity.moveFocusableActivityToTop("testDesktopModeTaskToFront"); + + // Start activity and delivered new intent. + starter.getIntent().setComponent(desktopModeReusableActivity.mActivityComponent); + doReturn(desktopModeReusableActivity).when(mRootWindowContainer).findTask(any(), any()); + final int result = starter.setReason("testDesktopModeMoveToFront").execute(); + + // Ensure result is moving task to front. + assertEquals(START_TASK_TO_FRONT, result); + } + + /** Returns 4 activities. */ + private List<ActivityRecord> createActivitiesInDesktopMode() { + final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm); + List<ActivityRecord> activityRecords = new ArrayList<>(); + + for (int i = 0; i < 4; i++) { + Rect bounds = new Rect(desktopOrganizer.getDefaultDesktopTaskBounds()); + bounds.offset(20 * i, 20 * i); + desktopOrganizer.createTask(bounds); + } + + for (int i = 0; i < 4; i++) { + activityRecords.add(new TaskBuilder(mSupervisor) + .setParentTask(desktopOrganizer.mTasks.get(i)) + .setCreateActivity(true) + .build() + .getTopMostActivity()); + } + + for (int i = 0; i < 4; i++) { + activityRecords.get(i).setVisibleRequested(true); + } + + for (int i = 0; i < 4; i++) { + assertEquals(desktopOrganizer.mTasks.get(i), activityRecords.get(i).getRootTask()); + } + + return activityRecords; + } + @Test public void testMoveVisibleTaskToFront() { final ActivityRecord activity = new TaskBuilder(mSupervisor) diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 402cbccbca01..c44be7b9db51 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -56,6 +56,7 @@ import android.os.Bundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArraySet; import android.view.WindowManager; import android.window.BackAnimationAdapter; @@ -69,6 +70,7 @@ import android.window.TaskSnapshot; import android.window.WindowOnBackInvokedDispatcher; import com.android.server.LocalServices; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -81,6 +83,12 @@ import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +/** + * Tests for the {@link BackNavigationController} class. + * + * Build/Install/Run: + * atest WmTests:BackNavigationControllerTests + */ @Presubmit @RunWith(WindowTestRunner.class) public class BackNavigationControllerTests extends WindowTestsBase { @@ -623,6 +631,22 @@ public class BackNavigationControllerTests extends WindowTestsBase { 0, navigationObserver.getCount()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG) + public void testAdjacentFocusInActivityEmbedding() { + Task task = createTask(mDefaultDisplay); + TaskFragment primary = createTaskFragmentWithActivity(task); + TaskFragment secondary = createTaskFragmentWithActivity(task); + primary.setAdjacentTaskFragment(secondary); + secondary.setAdjacentTaskFragment(primary); + + WindowState windowState = mock(WindowState.class); + doReturn(windowState).when(mWm).getFocusedWindowLocked(); + doReturn(primary).when(windowState).getTaskFragment(); + + startBackNavigation(); + verify(mWm).moveFocusToAdjacentWindow(any(), anyInt()); + } /** * Test with diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 782d89cdcd29..95850ac2f3b2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -2131,8 +2131,8 @@ public class DisplayContentTests extends WindowTestsBase { // Once transition starts, rotation is applied and transition shows DC rotating. testPlayer.startTransition(); waitUntilHandlersIdle(); - verify(activity1).ensureActivityConfiguration(anyBoolean(), anyBoolean()); - verify(activity2).ensureActivityConfiguration(anyBoolean(), anyBoolean()); + verify(activity1).ensureActivityConfiguration(anyBoolean()); + verify(activity2).ensureActivityConfiguration(anyBoolean()); assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation()); assertNotNull(testPlayer.mLastReady); assertTrue(testPlayer.mController.isPlaying()); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index be96e60917a3..9e00f927a568 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -283,12 +283,12 @@ public class DisplayPolicyTests extends WindowTestsBase { policy.screenTurnedOff(); policy.setAwake(false); - policy.screenTurnedOn(null /* screenOnListener */); + policy.screenTurningOn(null /* screenOnListener */); assertTrue(wpc.isShowingUiWhileDozing()); policy.screenTurnedOff(); assertFalse(wpc.isShowingUiWhileDozing()); - policy.screenTurnedOn(null /* screenOnListener */); + policy.screenTurningOn(null /* screenOnListener */); assertTrue(wpc.isShowingUiWhileDozing()); policy.setAwake(true); assertFalse(wpc.isShowingUiWhileDozing()); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 5518c604446d..6759eef2066d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -197,10 +197,10 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); translucentActivity.setState(DESTROYED, "testing"); @@ -225,10 +225,10 @@ public class SizeCompatTests extends WindowTestsBase { // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); spyOn(translucentActivity.mLetterboxUiController); @@ -300,10 +300,10 @@ public class SizeCompatTests extends WindowTestsBase { // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); spyOn(translucentActivity.mLetterboxUiController); @@ -376,10 +376,10 @@ public class SizeCompatTests extends WindowTestsBase { // Launch translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Transparent strategy applied assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -404,10 +404,10 @@ public class SizeCompatTests extends WindowTestsBase { // Launch translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Transparent strategy applied assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -441,10 +441,10 @@ public class SizeCompatTests extends WindowTestsBase { // Launch translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Transparent strategy applied assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -465,12 +465,12 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .setMinAspectRatio(1.1f) .setMaxAspectRatio(3f) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // We check bounds final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); @@ -493,9 +493,9 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .build(); - doReturn(false).when(translucentActivity).fillsParent(); final Configuration requestedConfig = translucentActivity.getRequestedOverrideConfiguration(); final WindowConfiguration translucentWinConf = requestedConfig.windowConfiguration; @@ -525,12 +525,12 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .setMinAspectRatio(1.1f) .setMaxAspectRatio(3f) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // We check bounds final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); @@ -538,10 +538,10 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(opaqueBounds, translucentRequestedBounds); // Launch another translucent activity final ActivityRecord translucentActivity2 = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .build(); - doReturn(false).when(translucentActivity2).fillsParent(); mTask.addChild(translucentActivity2); // We check bounds final Rect translucent2RequestedBounds = translucentActivity2.getRequestedOverrideBounds(); @@ -558,9 +558,9 @@ public class SizeCompatTests extends WindowTestsBase { // simplicity. doReturn(true).when(mActivity).isEmbedded(); // Translucent Activity - final ActivityRecord translucentActivity = new ActivityBuilder(mAtm).build(); + final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent).build(); doReturn(false).when(translucentActivity).matchParentBounds(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Check the strategy has not being applied assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -580,10 +580,10 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mActivity.inSizeCompatMode()); // We launch a transparent activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // It should not be in SCM assertFalse(translucentActivity.inSizeCompatMode()); @@ -600,12 +600,16 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .build(); - doReturn(false).when(translucentActivity).fillsParent(); - spyOn(mActivity); + assertFalse(translucentActivity.fillsParent()); + assertTrue(mActivity.fillsParent()); + mActivity.finishing = true; + assertFalse(mActivity.occludesParent()); mTask.addChild(translucentActivity); - verify(mActivity).isFinishing(); + // The translucent activity won't inherit letterbox behavior from a finishing activity. + assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); } @Test @@ -619,10 +623,10 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); // We launch a transparent activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); assertEquals(translucentActivity.getBounds(), mActivity.getBounds()); @@ -655,10 +659,10 @@ public class SizeCompatTests extends WindowTestsBase { // We launch a transparent activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // The transparent activity inherits the compat display insets of the opaque activity @@ -1020,8 +1024,17 @@ public class SizeCompatTests extends WindowTestsBase { // Activity is sandboxed due to fixed aspect ratio. assertActivityMaxBoundsSandboxed(); + // Prepare the states for verifying relaunching after changing orientation. + mActivity.finishRelaunching(); + mActivity.setState(RESUMED, "testFixedAspectRatioOrientationChangeOrientation"); + mActivity.setLastReportedConfiguration(mAtm.getGlobalConfiguration(), + mActivity.getConfiguration()); + // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); + assertTrue(mActivity.isRelaunching()); + assertTrue(mActivity.mLetterboxUiController + .getIsRelaunchingAfterRequestedOrientationChanged()); assertFitted(); assertEquals(originalBounds.width(), mActivity.getBounds().height()); diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index 45ecc3f762ec..00ecd008cde7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -215,7 +215,7 @@ class TestDisplayContent extends DisplayContent { doReturn(false).when(newDisplay).supportsSystemDecorations(); } // Update the display policy to make the screen fully turned on so animation is allowed - displayPolicy.screenTurnedOn(null /* screenOnListener */); + displayPolicy.screenTurningOn(null /* screenOnListener */); displayPolicy.finishKeyguardDrawn(); displayPolicy.finishWindowsDrawn(); displayPolicy.finishScreenTurningOn(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index 114b9c3a68f2..a0bafb64090f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -20,6 +20,7 @@ import static android.app.AppOpsManager.OP_NONE; import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; @@ -133,7 +134,9 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** Common base class for window manager unit test classes. */ class WindowTestsBase extends SystemServiceTestsBase { @@ -226,7 +229,7 @@ class WindowTestsBase extends SystemServiceTestsBase { mDefaultDisplay = mWm.mRoot.getDefaultDisplay(); // Update the display policy to make the screen fully turned on so animation is allowed final DisplayPolicy displayPolicy = mDefaultDisplay.getDisplayPolicy(); - displayPolicy.screenTurnedOn(null /* screenOnListener */); + displayPolicy.screenTurningOn(null /* screenOnListener */); displayPolicy.finishKeyguardDrawn(); displayPolicy.finishWindowsDrawn(); displayPolicy.finishScreenTurningOn(); @@ -1892,6 +1895,55 @@ class WindowTestsBase extends SystemServiceTestsBase { } } + static class TestDesktopOrganizer extends WindowOrganizerTests.StubOrganizer { + final int mDesktopModeDefaultWidthDp = 840; + final int mDesktopModeDefaultHeightDp = 630; + final int mDesktopDensity = 284; + + final ActivityTaskManagerService mService; + final TaskDisplayArea mDefaultTDA; + List<Task> mTasks; + final DisplayContent mDisplay; + Rect mStableBounds; + + TestDesktopOrganizer(ActivityTaskManagerService service, DisplayContent display) { + mService = service; + mDefaultTDA = display.getDefaultTaskDisplayArea(); + mDisplay = display; + mService.mTaskOrganizerController.registerTaskOrganizer(this); + mTasks = new ArrayList<>(); + mStableBounds = display.getBounds(); + } + + TestDesktopOrganizer(ActivityTaskManagerService service) { + this(service, service.mTaskSupervisor.mRootWindowContainer.getDefaultDisplay()); + } + + public Task createTask(Rect bounds) { + Task task = mService.mTaskOrganizerController.createRootTask( + mDisplay, WINDOWING_MODE_FREEFORM, null); + task.setBounds(bounds); + mTasks.add(task); + spyOn(task); + return task; + } + + public Rect getDefaultDesktopTaskBounds() { + int width = (int) (mDesktopModeDefaultWidthDp * mDesktopDensity + 0.5f); + int height = (int) (mDesktopModeDefaultHeightDp * mDesktopDensity + 0.5f); + Rect outBounds = new Rect(); + + outBounds.set(0, 0, width, height); + // Center the task in stable bounds + outBounds.offset( + mStableBounds.centerX() - outBounds.centerX(), + mStableBounds.centerY() - outBounds.centerY() + ); + return outBounds; + } + + } + static TestWindowToken createTestWindowToken(int type, DisplayContent dc) { return createTestWindowToken(type, dc, false /* persistOnEmpty */); } diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java index 94c737d61b0a..a089f5c9d641 100644 --- a/telecomm/java/android/telecom/PhoneAccount.java +++ b/telecomm/java/android/telecom/PhoneAccount.java @@ -18,6 +18,7 @@ package android.telecom; import static android.Manifest.permission.MODIFY_PHONE_STATE; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; @@ -30,11 +31,15 @@ import android.os.Parcelable; import android.telephony.CarrierConfigManager; import android.telephony.TelephonyManager; import android.text.TextUtils; +import android.util.ArraySet; + +import com.android.internal.telephony.flags.Flags; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Set; /** * Represents a distinct method to place or receive a phone call. Apps which can place calls and @@ -491,6 +496,7 @@ public final class PhoneAccount implements Parcelable { private final Bundle mExtras; private boolean mIsEnabled; private String mGroupId; + private final Set<PhoneAccountHandle> mSimultaneousCallingRestriction; @Override public boolean equals(Object o) { @@ -508,7 +514,9 @@ public final class PhoneAccount implements Parcelable { Objects.equals(mShortDescription, that.mShortDescription) && Objects.equals(mSupportedUriSchemes, that.mSupportedUriSchemes) && areBundlesEqual(mExtras, that.mExtras) && - Objects.equals(mGroupId, that.mGroupId); + Objects.equals(mGroupId, that.mGroupId) + && Objects.equals(mSimultaneousCallingRestriction, + that.mSimultaneousCallingRestriction); } @Override @@ -516,7 +524,7 @@ public final class PhoneAccount implements Parcelable { return Objects.hash(mAccountHandle, mAddress, mSubscriptionAddress, mCapabilities, mHighlightColor, mLabel, mShortDescription, mSupportedUriSchemes, mSupportedAudioRoutes, - mExtras, mIsEnabled, mGroupId); + mExtras, mIsEnabled, mGroupId, mSimultaneousCallingRestriction); } /** @@ -537,6 +545,7 @@ public final class PhoneAccount implements Parcelable { private Bundle mExtras; private boolean mIsEnabled = false; private String mGroupId = ""; + private Set<PhoneAccountHandle> mSimultaneousCallingRestriction = null; /** * Creates a builder with the specified {@link PhoneAccountHandle} and label. @@ -787,6 +796,57 @@ public final class PhoneAccount implements Parcelable { } /** + * Restricts the ability of this {@link PhoneAccount} to ONLY support simultaneous calling + * with the other {@link PhoneAccountHandle}s in this Set. + * <p> + * If two or more {@link PhoneAccount}s support calling simultaneously, it means that + * Telecom allows the user to place additional outgoing calls and receive additional + * incoming calls using other {@link PhoneAccount}s while this PhoneAccount also has one or + * more active calls. + * <p> + * If this setter method is never called or cleared using + * {@link #clearSimultaneousCallingRestriction()}, there is no restriction and all + * {@link PhoneAccount}s registered to Telecom by this package support simultaneous calling. + * <p> + * Note: Simultaneous calling restrictions can only be placed on {@link PhoneAccount}s that + * were registered by the same application. Simultaneous calling across applications is + * always possible as long as the {@link Connection} supports hold. If a + * {@link PhoneAccountHandle} is included here and the package name doesn't match this + * application's package name, {@link TelecomManager#registerPhoneAccount(PhoneAccount)} + * will throw a {@link SecurityException}. + * + * @param handles The other {@link PhoneAccountHandle}s that support calling simultaneously + * with this one. Use {@link #clearSimultaneousCallingRestriction()} to remove the + * restriction and allow simultaneous calling to be supported across all + * {@link PhoneAccount}s registered by this package. + * @return The Builder used to set up the new PhoneAccount. + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + public @NonNull Builder setSimultaneousCallingRestriction( + @NonNull Set<PhoneAccountHandle> handles) { + if (handles == null) { + throw new IllegalArgumentException("the Set of PhoneAccountHandles must not be " + + "null"); + } + mSimultaneousCallingRestriction = handles; + return this; + } + + /** + * Clears a previously set simultaneous calling restriction set when + * {@link PhoneAccount.Builder#Builder(PhoneAccount)} is used to create a new PhoneAccount + * from an existing one. + * + * @return The Builder used to set up the new PhoneAccount. + * @see #setSimultaneousCallingRestriction(Set) + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + public @NonNull Builder clearSimultaneousCallingRestriction() { + mSimultaneousCallingRestriction = null; + return this; + } + + /** * Creates an instance of a {@link PhoneAccount} based on the current builder settings. * * @return The {@link PhoneAccount}. @@ -810,7 +870,8 @@ public final class PhoneAccount implements Parcelable { mExtras, mSupportedAudioRoutes, mIsEnabled, - mGroupId); + mGroupId, + mSimultaneousCallingRestriction); } } @@ -827,7 +888,8 @@ public final class PhoneAccount implements Parcelable { Bundle extras, int supportedAudioRoutes, boolean isEnabled, - String groupId) { + String groupId, + Set<PhoneAccountHandle> simultaneousCallingRestriction) { mAccountHandle = account; mAddress = address; mSubscriptionAddress = subscriptionAddress; @@ -841,6 +903,7 @@ public final class PhoneAccount implements Parcelable { mSupportedAudioRoutes = supportedAudioRoutes; mIsEnabled = isEnabled; mGroupId = groupId; + mSimultaneousCallingRestriction = simultaneousCallingRestriction; } public static Builder builder( @@ -1050,6 +1113,49 @@ public final class PhoneAccount implements Parcelable { return (mCapabilities & CAPABILITY_SELF_MANAGED) == CAPABILITY_SELF_MANAGED; } + /** + * If a restriction is set (see {@link #hasSimultaneousCallingRestriction()}), this method + * returns the Set of {@link PhoneAccountHandle}s that are allowed to support calls + * simultaneously with this {@link PhoneAccount}. + * <p> + * If this {@link PhoneAccount} is busy with one or more ongoing calls, a restriction is set on + * this PhoneAccount (see {@link #hasSimultaneousCallingRestriction()} to check), and a new + * incoming or outgoing call is received or placed on a PhoneAccount that is not in this Set, + * Telecom will reject or cancel the pending call in favor of keeping the ongoing call alive. + * <p> + * Note: Simultaneous calling restrictions can only be placed on {@link PhoneAccount}s that + * were registered by the same application. Simultaneous calling across applications is + * always possible as long as the {@link Connection} supports hold. + * + * @return the Set of {@link PhoneAccountHandle}s that this {@link PhoneAccount} supports + * simultaneous calls with. + * @throws IllegalStateException If there is no restriction set on this {@link PhoneAccount} + * and this method is called. Whether or not there is a restriction can be checked using + * {@link #hasSimultaneousCallingRestriction()}. + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + public @NonNull Set<PhoneAccountHandle> getSimultaneousCallingRestriction() { + if (mSimultaneousCallingRestriction == null) { + throw new IllegalStateException("This method can not be called if there is no " + + "simultaneous calling restriction. See #hasSimultaneousCallingRestriction"); + } + return mSimultaneousCallingRestriction; + } + + /** + * Whether or not this {@link PhoneAccount} contains a simultaneous calling restriction on it. + * + * @return {@code true} if this PhoneAccount contains a simultaneous calling restriction, + * {@code false} if it does not. Use {@link #getSimultaneousCallingRestriction()} to query which + * other {@link PhoneAccount}s support simultaneous calling with this one. + * @see #getSimultaneousCallingRestriction() for more information on how the sinultaneous + * calling restriction works. + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + public boolean hasSimultaneousCallingRestriction() { + return mSimultaneousCallingRestriction != null; + } + // // Parcelable implementation // @@ -1095,6 +1201,12 @@ public final class PhoneAccount implements Parcelable { out.writeBundle(mExtras); out.writeString(mGroupId); out.writeInt(mSupportedAudioRoutes); + if (mSimultaneousCallingRestriction == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeTypedList(mSimultaneousCallingRestriction.stream().toList()); + } } public static final @android.annotation.NonNull Creator<PhoneAccount> CREATOR @@ -1140,6 +1252,13 @@ public final class PhoneAccount implements Parcelable { mExtras = in.readBundle(); mGroupId = in.readString(); mSupportedAudioRoutes = in.readInt(); + if (in.readBoolean()) { + List<PhoneAccountHandle> list = new ArrayList<>(); + in.readTypedList(list, PhoneAccountHandle.CREATOR); + mSimultaneousCallingRestriction = new ArraySet<>(list); + } else { + mSimultaneousCallingRestriction = null; + } } @Override diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index a5c6d57aed82..1bf11df7059a 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1618,14 +1618,15 @@ public class SubscriptionManager { } /** - * Register for changes to the list of active {@link SubscriptionInfo} records or to the - * individual records themselves. When a change occurs the onSubscriptionsChanged method of - * the listener will be invoked immediately if there has been a notification. The - * onSubscriptionChanged method will also be triggered once initially when calling this - * function. + * Register for changes to the list of {@link SubscriptionInfo} records or to the + * individual records (active or inactive) themselves. When a change occurs, the + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of + * the listener will be invoked immediately. The + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked + * once initially when calling this method. * * @param listener an instance of {@link OnSubscriptionsChangedListener} with - * onSubscriptionsChanged overridden. + * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden. * @param executor the executor that will execute callbacks. */ public void addOnSubscriptionsChangedListener( @@ -1953,7 +1954,6 @@ public class SubscriptionManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - // @RequiresPermission(TODO(b/308809058)) public List<SubscriptionInfo> getActiveSubscriptionInfoList() { List<SubscriptionInfo> activeList = null; @@ -2010,6 +2010,9 @@ public class SubscriptionManager { * Create a new subscription manager instance that can see all subscriptions across * user profiles. * + * The permission check for accessing all subscriptions will be enforced upon calling the + * individual APIs linked below. + * * @return a SubscriptionManager that can see all subscriptions regardless its user profile * association. * @@ -2018,9 +2021,7 @@ public class SubscriptionManager { * @see UserHandle */ @FlaggedApi(Flags.FLAG_ENFORCE_SUBSCRIPTION_USER_FILTER) - // @RequiresPermission(TODO(b/308809058)) - // The permission check for accessing all subscriptions will be enforced upon calling the - // individual APIs linked above. + @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) @NonNull public SubscriptionManager createForAllUserProfiles() { return new SubscriptionManager(mContext, true/*isForAllUserProfiles*/); } @@ -2215,7 +2216,6 @@ public class SubscriptionManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - // @RequiresPermission(TODO(b/308809058)) public int getActiveSubscriptionInfoCount() { int result = 0; diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 9ec5f7a77b2c..cbd552454642 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -6888,6 +6888,7 @@ public class TelephonyManager { } } + // TODO(b/316183370): replace all @code with @link in javadoc after feature is released /** * @return true if the current device is "voice capable". * <p> @@ -6901,7 +6902,10 @@ public class TelephonyManager { * PackageManager.FEATURE_TELEPHONY system feature, which is available * on any device with a telephony radio, even if the device is * data-only. - * @deprecated Replaced by {@link #isDeviceVoiceCapable()} + * @deprecated Replaced by {@code #isDeviceVoiceCapable()}. Starting from Android 15, voice + * capability may also be overridden by carriers for a given subscription. For voice capable + * device (when {@code #isDeviceVoiceCapable} return {@code true}), caller should check for + * subscription-level voice capability as well. See {@code #isDeviceVoiceCapable} for details. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING) @Deprecated @@ -6923,9 +6927,10 @@ public class TelephonyManager { * .FEATURE_TELEPHONY system feature, which is available on any device with a telephony * radio, even if the device is data-only. * <p> - * To check if a subscription is "voice capable", call method - * {@link SubscriptionInfo#getServiceCapabilities()} and compare the result with - * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_VOICE}. + * Starting from Android 15, voice capability may also be overridden by carrier for a given + * subscription on a voice capable device. To check if a subscription is "voice capable", + * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if + * {@code SubscriptionManager#SERVICE_CAPABILITY_VOICE} is included. * * @see SubscriptionInfo#getServiceCapabilities() */ @@ -6943,7 +6948,10 @@ public class TelephonyManager { * <p> * Note: Voicemail waiting sms, cell broadcasting sms, and MMS are * disabled when device doesn't support sms. - * @deprecated Replaced by {@link #isDeviceSmsCapable()} + * @deprecated Replaced by {@code #isDeviceSmsCapable()}. Starting from Android 15, SMS + * capability may also be overridden by carriers for a given subscription. For SMS capable + * device (when {@code #isDeviceSmsCapable} return {@code true}), caller should check for + * subscription-level SMS capability as well. See {@code #isDeviceSmsCapable} for details. */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING) public boolean isSmsCapable() { @@ -6961,9 +6969,10 @@ public class TelephonyManager { * Note: Voicemail waiting SMS, cell broadcasting SMS, and MMS are * disabled when device doesn't support SMS. * <p> - * To check if a subscription is "SMS capable", call method - * {@link SubscriptionInfo#getServiceCapabilities()} and compare result with - * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_SMS}. + * Starting from Android 15, SMS capability may also be overridden by carriers for a given + * subscription on an SMS capable device. To check if a subscription is "SMS capable", + * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if + * {@code SubscriptionManager#SERVICE_CAPABILITY_SMS} is included. * * @see SubscriptionInfo#getServiceCapabilities() */ @@ -13139,7 +13148,7 @@ public class TelephonyManager { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) public ServiceState getServiceStateForSubscriber(int subId) { - return getServiceStateForSubscriber(getSubId(), false, false); + return getServiceStateForSubscriber(subId, false, false); } /** @@ -18483,7 +18492,6 @@ public class TelephonyManager { /** * Get last known cell identity. - * Require appropriate permissions, otherwise throws SecurityException. * * If there is current registered network this value will be same as the registered cell * identity. If the device goes out of service the previous cell identity is cached and @@ -18495,7 +18503,7 @@ public class TelephonyManager { @SystemApi @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) @RequiresPermission(allOf = {Manifest.permission.ACCESS_FINE_LOCATION, - "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) + Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public @Nullable CellIdentity getLastKnownCellIdentity() { try { ITelephony telephony = getITelephony(); diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index acbf354bb4de..24296f4d9031 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3078,6 +3078,16 @@ interface ITelephony { in List<String> satelliteCountryCodes); /** + * This API can be used in only testing to override oem-enabled satellite provision status. + * + * @param reset {@code true} mean the overriding status should not be used, {@code false} + * otherwise. + * @param isProvisioned The overriding provision status. + * @return {@code true} if the provision status is set successfully, {@code false} otherwise. + */ + boolean setOemEnabledSatelliteProvisionStatus(in boolean reset, in boolean isProvisioned); + + /** * Test method to confirm the file contents are not altered. */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" diff --git a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java index 2bc056ee743f..cee27b6847ec 100644 --- a/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java +++ b/tests/BinaryTransparencyHostTest/test-app/src/android/transparency/test/app/BinaryTransparencyTest.java @@ -16,6 +16,8 @@ package android.transparency.test.app; +import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; + import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -27,6 +29,7 @@ import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; +import com.android.compatibility.common.util.ShellIdentityUtils; import com.android.internal.os.IBinaryTransparencyService.AppInfo; import org.junit.Before; @@ -116,7 +119,12 @@ public class BinaryTransparencyTest { @Test public void testCollectAllSilentInstalledMbaInfo() { // Action - var appInfoList = mBt.collectAllSilentInstalledMbaInfo(new Bundle()); + var appInfoList = + ShellIdentityUtils.invokeMethodWithShellPermissions( + mBt, + (Bt) -> + mBt.collectAllSilentInstalledMbaInfo(new Bundle()), + GET_BACKGROUND_INSTALLED_PACKAGES); // Verify assertThat(appInfoList).isNotEmpty(); // because we just installed from the host side diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index b05863188beb..256a4696b763 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -292,14 +292,16 @@ class InputManagerServiceTests { setVirtualMousePointerDisplayIdAndVerify(10) localService.setPointerIconVisible(false, 10) + verify(native).setPointerIconVisibility(10, false) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) localService.setMousePointerAccelerationEnabled(false, 10) - verify(native).setMousePointerAccelerationEnabled(eq(false)) + verify(native).setMousePointerAccelerationEnabled(10, false) service.onDisplayRemoved(10) + verify(native).setPointerIconVisibility(10, true) verify(native).displayRemoved(eq(10)) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) - verify(native).setMousePointerAccelerationEnabled(true) + verify(native).setMousePointerAccelerationEnabled(10, true) verifyNoMoreInteractions(native) // This call should not block because the virtual mouse pointer override was never removed. @@ -315,25 +317,26 @@ class InputManagerServiceTests { localService.setPointerIconVisible(false, 10) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) + verify(native).setPointerIconVisibility(10, false) localService.setMousePointerAccelerationEnabled(false, 10) - verify(native).setMousePointerAccelerationEnabled(eq(false)) + verify(native).setMousePointerAccelerationEnabled(10, false) localService.setPointerIconVisible(true, 10) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED)) + verify(native).setPointerIconVisibility(10, true) localService.setMousePointerAccelerationEnabled(true, 10) - verify(native).setMousePointerAccelerationEnabled(eq(true)) + verify(native).setMousePointerAccelerationEnabled(10, true) - // Verify that setting properties on a different display is not propagated until the - // pointer is moved to that display. localService.setPointerIconVisible(false, 20) + verify(native).setPointerIconVisibility(20, false) localService.setMousePointerAccelerationEnabled(false, 20) + verify(native).setMousePointerAccelerationEnabled(20, false) verifyNoMoreInteractions(native) clearInvocations(native) setVirtualMousePointerDisplayIdAndVerify(20) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - verify(native).setMousePointerAccelerationEnabled(eq(false)) } @Test @@ -341,12 +344,13 @@ class InputManagerServiceTests { localService.setPointerIconVisible(false, 10) localService.setMousePointerAccelerationEnabled(false, 10) + verify(native).setPointerIconVisibility(10, false) + verify(native).setMousePointerAccelerationEnabled(10, false) verifyNoMoreInteractions(native) setVirtualMousePointerDisplayIdAndVerify(10) verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL)) - verify(native).setMousePointerAccelerationEnabled(eq(false)) } @Test diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java index cbdcb8869628..518183f9cd64 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -30,6 +30,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.PackageManager; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; import android.os.UserManager; @@ -146,7 +147,8 @@ public class RollbackTest { assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); // Upgrade from v1 to v2, with rollbacks enabled. - Install.single(TestApp.A2).setEnableRollback().commit(); + Install.single(TestApp.A2).setEnableRollback().setRollbackImpactLevel( + PackageManager.ROLLBACK_USER_IMPACT_HIGH).commit(); assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); // The app should now be available for rollback. @@ -154,6 +156,8 @@ public class RollbackTest { assertThat(available).isNotStaged(); assertThat(available).packagesContainsExactly( Rollback.from(TestApp.A2).to(TestApp.A1)); + assertThat(available.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_HIGH); // We should not have received any rollback requests yet. // TODO: Possibly flaky if, by chance, some other app on device @@ -264,6 +268,8 @@ public class RollbackTest { RollbackInfo rollbackB = waitForAvailableRollback(TestApp.B); assertThat(rollbackB).packagesContainsExactly( Rollback.from(TestApp.B2).to(TestApp.B1)); + assertThat(rollbackB.getRollbackImpactLevel()).isEqualTo( + PackageManager.ROLLBACK_USER_IMPACT_LOW); // Register rollback committed receiver RollbackBroadcastReceiver rollbackReceiver = new RollbackBroadcastReceiver(); |