diff options
208 files changed, 9740 insertions, 2290 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java index 37fb746fa16f..232d3c9aaa53 100644 --- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java +++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java @@ -39,6 +39,7 @@ import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROU import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED; import static android.os.UserHandle.USER_SYSTEM; +import static com.android.server.SystemClockTime.TIME_CONFIDENCE_HIGH; import static com.android.server.SystemTimeZone.TIME_ZONE_CONFIDENCE_HIGH; import static com.android.server.SystemTimeZone.getTimeZoneId; import static com.android.server.alarm.Alarm.APP_STANDBY_POLICY_INDEX; @@ -58,6 +59,8 @@ import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_R import static com.android.server.alarm.AlarmManagerService.RemovedAlarm.REMOVE_REASON_UNDEFINED; import android.Manifest; +import android.annotation.CurrentTimeMillisLong; +import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.UserIdInt; import android.app.Activity; @@ -87,7 +90,6 @@ import android.os.BatteryManager; import android.os.Binder; import android.os.Build; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -101,7 +103,6 @@ import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; import android.os.SystemClock; -import android.os.SystemProperties; import android.os.ThreadLocalWorkSource; import android.os.Trace; import android.os.UserHandle; @@ -145,6 +146,8 @@ import com.android.server.DeviceIdleInternal; import com.android.server.EventLogTags; import com.android.server.JobSchedulerBackgroundThread; import com.android.server.LocalServices; +import com.android.server.SystemClockTime; +import com.android.server.SystemClockTime.TimeConfidence; import com.android.server.SystemService; import com.android.server.SystemServiceManager; import com.android.server.SystemTimeZone; @@ -1417,7 +1420,7 @@ public class AlarmManagerService extends SystemService { private long convertToElapsed(long when, int type) { if (isRtc(type)) { - when -= mInjector.getCurrentTimeMillis() - mInjector.getElapsedRealtime(); + when -= mInjector.getCurrentTimeMillis() - mInjector.getElapsedRealtimeMillis(); } return when; } @@ -1577,7 +1580,7 @@ public class AlarmManagerService extends SystemService { alarmsToDeliver = alarmsForUid; mPendingBackgroundAlarms.remove(uid); } - deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime()); + deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtimeMillis()); } /** @@ -1594,7 +1597,8 @@ public class AlarmManagerService extends SystemService { mPendingBackgroundAlarms, alarmsToDeliver, this::isBackgroundRestricted); if (alarmsToDeliver.size() > 0) { - deliverPendingBackgroundAlarmsLocked(alarmsToDeliver, mInjector.getElapsedRealtime()); + deliverPendingBackgroundAlarmsLocked( + alarmsToDeliver, mInjector.getElapsedRealtimeMillis()); } } @@ -1908,17 +1912,8 @@ public class AlarmManagerService extends SystemService { mInjector.syncKernelTimeZoneOffset(); } - // Ensure that we're booting with a halfway sensible current time. Use the - // most recent of Build.TIME, the root file system's timestamp, and the - // value of the ro.build.date.utc system property (which is in seconds). - final long systemBuildTime = Long.max( - 1000L * SystemProperties.getLong("ro.build.date.utc", -1L), - Long.max(Environment.getRootDirectory().lastModified(), Build.TIME)); - if (mInjector.getCurrentTimeMillis() < systemBuildTime) { - Slog.i(TAG, "Current time only " + mInjector.getCurrentTimeMillis() - + ", advancing to build time " + systemBuildTime); - mInjector.setKernelTime(systemBuildTime); - } + // Ensure that we're booting with a halfway sensible current time. + mInjector.initializeTimeIfRequired(); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); // Determine SysUI's uid @@ -2133,21 +2128,18 @@ public class AlarmManagerService extends SystemService { } } - boolean setTimeImpl(long millis) { - if (!mInjector.isAlarmDriverPresent()) { - Slog.w(TAG, "Not setting time since no alarm driver is available."); - return false; - } - + boolean setTimeImpl( + @CurrentTimeMillisLong long newSystemClockTimeMillis, @TimeConfidence int confidence, + @NonNull String logMsg) { synchronized (mLock) { - final long currentTimeMillis = mInjector.getCurrentTimeMillis(); - mInjector.setKernelTime(millis); + final long oldSystemClockTimeMillis = mInjector.getCurrentTimeMillis(); + mInjector.setCurrentTimeMillis(newSystemClockTimeMillis, confidence, logMsg); if (KERNEL_TIME_ZONE_SYNC_ENABLED) { // Changing the time may cross a DST transition; sync the kernel offset if needed. final TimeZone timeZone = TimeZone.getTimeZone(SystemTimeZone.getTimeZoneId()); - final int currentTzOffset = timeZone.getOffset(currentTimeMillis); - final int newTzOffset = timeZone.getOffset(millis); + final int currentTzOffset = timeZone.getOffset(oldSystemClockTimeMillis); + final int newTzOffset = timeZone.getOffset(newSystemClockTimeMillis); if (currentTzOffset != newTzOffset) { Slog.i(TAG, "Timezone offset has changed, updating kernel timezone"); mInjector.setKernelTimeZoneOffset(newTzOffset); @@ -2260,7 +2252,7 @@ public class AlarmManagerService extends SystemService { triggerAtTime = 0; } - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); final long nominalTrigger = convertToElapsed(triggerAtTime, type); // Try to prevent spamming by making sure apps aren't firing alarms in the immediate future final long minTrigger = nowElapsed @@ -2383,7 +2375,7 @@ public class AlarmManagerService extends SystemService { // No need to fuzz as this is already earlier than the coming wake-from-idle. return changedBeforeFuzz; } - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); final long futurity = upcomingWakeFromIdle - nowElapsed; if (futurity <= mConstants.MIN_DEVICE_IDLE_FUZZ) { @@ -2405,7 +2397,7 @@ public class AlarmManagerService extends SystemService { * @return {@code true} if the alarm delivery time was updated. */ private boolean adjustDeliveryTimeBasedOnBatterySaver(Alarm alarm) { - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); if (isExemptFromBatterySaver(alarm)) { return false; } @@ -2472,7 +2464,7 @@ public class AlarmManagerService extends SystemService { * @return {@code true} if the alarm delivery time was updated. */ private boolean adjustDeliveryTimeBasedOnDeviceIdle(Alarm alarm) { - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); if (mPendingIdleUntil == null || mPendingIdleUntil == alarm) { return alarm.setPolicyElapsed(DEVICE_IDLE_POLICY_INDEX, nowElapsed); } @@ -2527,7 +2519,7 @@ public class AlarmManagerService extends SystemService { * adjustments made in this call. */ private boolean adjustDeliveryTimeBasedOnBucketLocked(Alarm alarm) { - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); if (mConstants.USE_TARE_POLICY || isExemptFromAppStandby(alarm) || mAppStandbyParole) { return alarm.setPolicyElapsed(APP_STANDBY_POLICY_INDEX, nowElapsed); } @@ -2587,7 +2579,7 @@ public class AlarmManagerService extends SystemService { * adjustments made in this call. */ private boolean adjustDeliveryTimeBasedOnTareLocked(Alarm alarm) { - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); if (!mConstants.USE_TARE_POLICY || isExemptFromTare(alarm) || hasEnoughWealthLocked(alarm)) { return alarm.setPolicyElapsed(TARE_POLICY_INDEX, nowElapsed); @@ -2643,7 +2635,7 @@ public class AlarmManagerService extends SystemService { ent.pkg = a.sourcePackage; ent.tag = a.statsTag; ent.op = "START IDLE"; - ent.elapsedRealtime = mInjector.getElapsedRealtime(); + ent.elapsedRealtime = mInjector.getElapsedRealtimeMillis(); ent.argRealtime = a.getWhenElapsed(); mAllowWhileIdleDispatches.add(ent); } @@ -2719,6 +2711,13 @@ public class AlarmManagerService extends SystemService { } @Override + public void setTime( + @CurrentTimeMillisLong long unixEpochTimeMillis, int confidence, + String logMsg) { + setTimeImpl(unixEpochTimeMillis, confidence, logMsg); + } + + @Override public void registerInFlightListener(InFlightListener callback) { synchronized (mLock) { mInFlightListeners.add(callback); @@ -2987,12 +2986,16 @@ public class AlarmManagerService extends SystemService { } @Override - public boolean setTime(long millis) { + public boolean setTime(@CurrentTimeMillisLong long millis) { getContext().enforceCallingOrSelfPermission( "android.permission.SET_TIME", "setTime"); - return setTimeImpl(millis); + // The public API (and the shell command that also uses this method) have no concept + // of confidence, but since the time should come either from apps working on behalf of + // the user or a developer, confidence is assumed "high". + final int timeConfidence = TIME_CONFIDENCE_HIGH; + return setTimeImpl(millis, timeConfidence, "AlarmManager.setTime() called"); } @Override @@ -3142,7 +3145,7 @@ public class AlarmManagerService extends SystemService { pw.println(); } - final long nowELAPSED = mInjector.getElapsedRealtime(); + final long nowELAPSED = mInjector.getElapsedRealtimeMillis(); final long nowUPTIME = SystemClock.uptimeMillis(); final long nowRTC = mInjector.getCurrentTimeMillis(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); @@ -3618,7 +3621,7 @@ public class AlarmManagerService extends SystemService { synchronized (mLock) { final long nowRTC = mInjector.getCurrentTimeMillis(); - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); proto.write(AlarmManagerServiceDumpProto.CURRENT_TIME, nowRTC); proto.write(AlarmManagerServiceDumpProto.ELAPSED_REALTIME, nowElapsed); proto.write(AlarmManagerServiceDumpProto.LAST_TIME_CHANGE_CLOCK_TIME, @@ -3956,7 +3959,7 @@ public class AlarmManagerService extends SystemService { void rescheduleKernelAlarmsLocked() { // Schedule the next upcoming wakeup alarm. If there is a deliverable batch // prior to that which contains no wakeups, we schedule that as well. - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); long nextNonWakeup = 0; if (mAlarmStore.size() > 0) { final long firstWakeup = mAlarmStore.getNextWakeupDeliveryTime(); @@ -4069,7 +4072,7 @@ public class AlarmManagerService extends SystemService { @GuardedBy("mLock") private void removeAlarmsInternalLocked(Predicate<Alarm> whichAlarms, int reason) { final long nowRtc = mInjector.getCurrentTimeMillis(); - final long nowElapsed = mInjector.getElapsedRealtime(); + final long nowElapsed = mInjector.getElapsedRealtimeMillis(); final ArrayList<Alarm> removedAlarms = mAlarmStore.remove(whichAlarms); final boolean removedFromStore = !removedAlarms.isEmpty(); @@ -4208,7 +4211,7 @@ public class AlarmManagerService extends SystemService { void interactiveStateChangedLocked(boolean interactive) { if (mInteractive != interactive) { mInteractive = interactive; - final long nowELAPSED = mInjector.getElapsedRealtime(); + final long nowELAPSED = mInjector.getElapsedRealtimeMillis(); if (interactive) { if (mPendingNonWakeupAlarms.size() > 0) { final long thisDelayTime = nowELAPSED - mStartCurrentDelayTime; @@ -4310,7 +4313,6 @@ public class AlarmManagerService extends SystemService { private static native void close(long nativeData); private static native int set(long nativeData, int type, long seconds, long nanoseconds); private static native int waitForAlarm(long nativeData); - private static native int setKernelTime(long nativeData, long millis); /* * b/246256335: The @Keep ensures that the native definition is kept even when the optimizer can @@ -4357,7 +4359,7 @@ public class AlarmManagerService extends SystemService { ent.pkg = alarm.sourcePackage; ent.tag = alarm.statsTag; ent.op = "END IDLE"; - ent.elapsedRealtime = mInjector.getElapsedRealtime(); + ent.elapsedRealtime = mInjector.getElapsedRealtimeMillis(); ent.argRealtime = alarm.getWhenElapsed(); mAllowWhileIdleDispatches.add(ent); } @@ -4602,20 +4604,27 @@ public class AlarmManagerService extends SystemService { setKernelTimeZoneOffset(utcOffsetMillis); } - void setKernelTime(long millis) { - if (mNativeData != 0) { - AlarmManagerService.setKernelTime(mNativeData, millis); - } + void initializeTimeIfRequired() { + SystemClockTime.initializeIfRequired(); + } + + void setCurrentTimeMillis( + @CurrentTimeMillisLong long unixEpochMillis, + @TimeConfidence int confidence, + @NonNull String logMsg) { + SystemClockTime.setTimeAndConfidence(unixEpochMillis, confidence, logMsg); } void close() { AlarmManagerService.close(mNativeData); } - long getElapsedRealtime() { + @ElapsedRealtimeLong + long getElapsedRealtimeMillis() { return SystemClock.elapsedRealtime(); } + @CurrentTimeMillisLong long getCurrentTimeMillis() { return System.currentTimeMillis(); } @@ -4665,7 +4674,7 @@ public class AlarmManagerService extends SystemService { while (true) { int result = mInjector.waitForAlarm(); final long nowRTC = mInjector.getCurrentTimeMillis(); - final long nowELAPSED = mInjector.getElapsedRealtime(); + final long nowELAPSED = mInjector.getElapsedRealtimeMillis(); synchronized (mLock) { mLastWakeup = nowELAPSED; } @@ -4913,7 +4922,7 @@ public class AlarmManagerService extends SystemService { // this way, so WAKE_UP alarms will be delivered only when the device is awake. ArrayList<Alarm> triggerList = new ArrayList<Alarm>(); synchronized (mLock) { - final long nowELAPSED = mInjector.getElapsedRealtime(); + final long nowELAPSED = mInjector.getElapsedRealtimeMillis(); triggerAlarmsLocked(triggerList, nowELAPSED); updateNextAlarmClockLocked(); } @@ -5090,7 +5099,7 @@ public class AlarmManagerService extends SystemService { flags |= mConstants.TIME_TICK_ALLOWED_WHILE_IDLE ? FLAG_ALLOW_WHILE_IDLE_UNRESTRICTED : 0; - setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtime() + tickEventDelay, 0, + setImpl(ELAPSED_REALTIME, mInjector.getElapsedRealtimeMillis() + tickEventDelay, 0, 0, null, mTimeTickTrigger, TIME_TICK_TAG, flags, workSource, null, Process.myUid(), "android", null, EXACT_ALLOW_REASON_ALLOW_LIST); @@ -5277,7 +5286,7 @@ public class AlarmManagerService extends SystemService { } synchronized (mLock) { mTemporaryQuotaReserve.replenishQuota(packageName, userId, quotaBump, - mInjector.getElapsedRealtime()); + mInjector.getElapsedRealtimeMillis()); } mHandler.obtainMessage(AlarmHandler.TEMPORARY_QUOTA_CHANGED, userId, -1, packageName).sendToTarget(); @@ -5439,7 +5448,7 @@ public class AlarmManagerService extends SystemService { } private void updateStatsLocked(InFlight inflight) { - final long nowELAPSED = mInjector.getElapsedRealtime(); + final long nowELAPSED = mInjector.getElapsedRealtimeMillis(); BroadcastStats bs = inflight.mBroadcastStats; bs.nesting--; if (bs.nesting <= 0) { diff --git a/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp b/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp index 8f9e187a7a93..b2ed4d47adf4 100644 --- a/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp +++ b/apex/jobscheduler/service/jni/com_android_server_alarm_AlarmManagerService.cpp @@ -76,19 +76,17 @@ typedef std::array<int, N_ANDROID_TIMERFDS> TimerFds; class AlarmImpl { public: - AlarmImpl(const TimerFds &fds, int epollfd, const std::string &rtc_dev) - : fds{fds}, epollfd{epollfd}, rtc_dev{rtc_dev} {} + AlarmImpl(const TimerFds &fds, int epollfd) + : fds{fds}, epollfd{epollfd} {} ~AlarmImpl(); int set(int type, struct timespec *ts); - int setTime(struct timeval *tv); int waitForAlarm(); int getTime(int type, struct itimerspec *spec); private: const TimerFds fds; const int epollfd; - std::string rtc_dev; }; AlarmImpl::~AlarmImpl() @@ -131,43 +129,6 @@ int AlarmImpl::getTime(int type, struct itimerspec *spec) return timerfd_gettime(fds[type], spec); } -int AlarmImpl::setTime(struct timeval *tv) -{ - if (settimeofday(tv, NULL) == -1) { - ALOGV("settimeofday() failed: %s", strerror(errno)); - return -1; - } - - android::base::unique_fd fd{open(rtc_dev.c_str(), O_RDWR)}; - if (!fd.ok()) { - ALOGE("Unable to open %s: %s", rtc_dev.c_str(), strerror(errno)); - return -1; - } - - struct tm tm; - if (!gmtime_r(&tv->tv_sec, &tm)) { - ALOGV("gmtime_r() failed: %s", strerror(errno)); - return -1; - } - - struct rtc_time rtc = {}; - rtc.tm_sec = tm.tm_sec; - rtc.tm_min = tm.tm_min; - rtc.tm_hour = tm.tm_hour; - rtc.tm_mday = tm.tm_mday; - rtc.tm_mon = tm.tm_mon; - rtc.tm_year = tm.tm_year; - rtc.tm_wday = tm.tm_wday; - rtc.tm_yday = tm.tm_yday; - rtc.tm_isdst = tm.tm_isdst; - if (ioctl(fd, RTC_SET_TIME, &rtc) == -1) { - ALOGV("RTC_SET_TIME ioctl failed: %s", strerror(errno)); - return -1; - } - - return 0; -} - int AlarmImpl::waitForAlarm() { epoll_event events[N_ANDROID_TIMERFDS]; @@ -198,28 +159,6 @@ int AlarmImpl::waitForAlarm() return result; } -static jint android_server_alarm_AlarmManagerService_setKernelTime(JNIEnv*, jobject, jlong nativeData, jlong millis) -{ - AlarmImpl *impl = reinterpret_cast<AlarmImpl *>(nativeData); - - if (millis <= 0 || millis / 1000LL >= std::numeric_limits<time_t>::max()) { - return -1; - } - - struct timeval tv; - tv.tv_sec = (millis / 1000LL); - tv.tv_usec = ((millis % 1000LL) * 1000LL); - - ALOGD("Setting time of day to sec=%ld", tv.tv_sec); - - int ret = impl->setTime(&tv); - if (ret < 0) { - ALOGW("Unable to set rtc to %ld: %s", tv.tv_sec, strerror(errno)); - ret = -1; - } - return ret; -} - static jint android_server_alarm_AlarmManagerService_setKernelTimezone(JNIEnv*, jobject, jlong, jint minswest) { struct timezone tz; @@ -287,19 +226,7 @@ static jlong android_server_alarm_AlarmManagerService_init(JNIEnv*, jobject) } } - // Find the wall clock RTC. We expect this always to be /dev/rtc0, but - // check the /dev/rtc symlink first so that legacy devices that don't use - // rtc0 can add a symlink rather than need to carry a local patch to this - // code. - // - // TODO: if you're reading this in a world where all devices are using the - // GKI, you can remove the readlink and just assume /dev/rtc0. - std::string dev_rtc; - if (!android::base::Readlink("/dev/rtc", &dev_rtc)) { - dev_rtc = "/dev/rtc0"; - } - - std::unique_ptr<AlarmImpl> alarm{new AlarmImpl(fds, epollfd, dev_rtc)}; + std::unique_ptr<AlarmImpl> alarm{new AlarmImpl(fds, epollfd)}; for (size_t i = 0; i < fds.size(); i++) { epoll_event event; @@ -392,7 +319,6 @@ static const JNINativeMethod sMethods[] = { {"close", "(J)V", (void*)android_server_alarm_AlarmManagerService_close}, {"set", "(JIJJ)I", (void*)android_server_alarm_AlarmManagerService_set}, {"waitForAlarm", "(J)I", (void*)android_server_alarm_AlarmManagerService_waitForAlarm}, - {"setKernelTime", "(JJ)I", (void*)android_server_alarm_AlarmManagerService_setKernelTime}, {"setKernelTimezone", "(JI)I", (void*)android_server_alarm_AlarmManagerService_setKernelTimezone}, {"getNextAlarm", "(JI)J", (void*)android_server_alarm_AlarmManagerService_getNextAlarm}, }; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index f221eca45337..134b71a49272 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -167,6 +167,7 @@ package android { field public static final String MANAGE_CONTENT_SUGGESTIONS = "android.permission.MANAGE_CONTENT_SUGGESTIONS"; field public static final String MANAGE_DEBUGGING = "android.permission.MANAGE_DEBUGGING"; field public static final String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; + field public static final String MANAGE_DEVICE_POLICY_APP_EXEMPTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS"; field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS"; field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION"; field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY"; @@ -2789,6 +2790,8 @@ package android.companion.virtual { public final class VirtualDeviceManager { method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.VirtualDeviceManager.VirtualDevice createVirtualDevice(int, @NonNull android.companion.virtual.VirtualDeviceParams); + field public static final int DEFAULT_DEVICE_ID = 0; // 0x0 + field public static final int INVALID_DEVICE_ID = -1; // 0xffffffff field public static final int LAUNCH_FAILURE_NO_ACTIVITY = 2; // 0x2 field public static final int LAUNCH_FAILURE_PENDING_INTENT_CANCELED = 1; // 0x1 field public static final int LAUNCH_SUCCESS = 0; // 0x0 @@ -2808,6 +2811,7 @@ package android.companion.virtual { method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); + method public int getDeviceId(); method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean); diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 626b7d3cd5b1..8da9631a23b0 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -31,6 +31,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ActivityPresentationInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PermissionMethod; +import android.content.pm.PermissionName; import android.content.pm.UserInfo; import android.net.Uri; import android.os.Bundle; @@ -294,7 +295,7 @@ public abstract class ActivityManagerInternal { /** Checks if the calling binder pid as the permission. */ @PermissionMethod - public abstract void enforceCallingPermission(String permission, String func); + public abstract void enforceCallingPermission(@PermissionName String permission, String func); /** Returns the current user id. */ public abstract int getCurrentUserId(); diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index b4abd3c69482..6404a1f59135 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -524,9 +524,30 @@ interface IActivityManager { @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) void suppressResizeConfigChanges(boolean suppress); + + /** + * @deprecated Use {@link #unlockUser2(int, IProgressListener)} instead, since the token and + * secret arguments no longer do anything. This method still exists only because it is marked + * with {@code @UnsupportedAppUsage}, so it might not be safe to remove it or change its + * signature. + */ @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553) boolean unlockUser(int userid, in byte[] token, in byte[] secret, in IProgressListener listener); + + /** + * Tries to unlock the given user. + * <p> + * This will succeed only if the user's CE storage key is already unlocked or if the user + * doesn't have a lockscreen credential set. + * + * @param userId The ID of the user to unlock. + * @param listener An optional progress listener. + * + * @return true if the user was successfully unlocked, otherwise false. + */ + boolean unlockUser2(int userId, in IProgressListener listener); + void killPackageDependents(in String packageName, int userId); void makePackageIdle(String packageName, int userId); int getMemoryTrimLevel(); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index d5b85cde4926..4ddfdb603e73 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -81,6 +81,8 @@ import android.content.pm.verify.domain.DomainVerificationManager; import android.content.pm.verify.domain.IDomainVerificationManager; import android.content.res.Resources; import android.content.rollback.RollbackManagerFrameworkInitializer; +import android.credentials.CredentialManager; +import android.credentials.ICredentialManager; import android.debug.AdbManager; import android.debug.IAdbManager; import android.graphics.fonts.FontManager; @@ -1138,6 +1140,19 @@ public final class SystemServiceRegistry { return new AutofillManager(ctx.getOuterContext(), service); }}); + registerService(Context.CREDENTIAL_SERVICE, CredentialManager.class, + new CachedServiceFetcher<CredentialManager>() { + @Override + public CredentialManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getService(Context.CREDENTIAL_SERVICE); + ICredentialManager service = ICredentialManager.Stub.asInterface(b); + if (service != null) { + return new CredentialManager(ctx.getOuterContext(), service); + } + return null; + }}); + registerService(Context.MUSIC_RECOGNITION_SERVICE, MusicRecognitionManager.class, new CachedServiceFetcher<MusicRecognitionManager>() { @Override diff --git a/core/java/android/companion/OWNERS b/core/java/android/companion/OWNERS index 004f66caed7b..0348fe2776fe 100644 --- a/core/java/android/companion/OWNERS +++ b/core/java/android/companion/OWNERS @@ -1,5 +1,3 @@ -ewol@google.com evanxinchen@google.com guojing@google.com -svetoslavganov@google.com -sergeynv@google.com
\ No newline at end of file +raphk@google.com
\ No newline at end of file diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 9c99da52d962..e7f191665302 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -43,6 +43,11 @@ interface IVirtualDevice { int getAssociationId(); /** + * Returns the unique device ID for this virtual device. + */ + int getDeviceId(); + + /** * Closes the virtual device and frees all associated resources. */ void close(); diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index fadfa5cd68a5..08bee2552a1f 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -78,6 +78,16 @@ public final class VirtualDeviceManager { | DisplayManager.VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP; + /** + * The default device ID, which is the ID of the primary (non-virtual) device. + */ + public static final int DEFAULT_DEVICE_ID = 0; + + /** + * Invalid device ID. + */ + public static final int INVALID_DEVICE_ID = -1; + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( @@ -204,6 +214,17 @@ public final class VirtualDeviceManager { } /** + * Returns the unique ID of this virtual device. + */ + public int getDeviceId() { + try { + return mVirtualDevice.getDeviceId(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Launches a given pending intent on the give display ID. * * @param displayId The display to launch the pending intent on. This display must be diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 0d73c1375b03..cb5a99fcc811 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -52,6 +52,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionMethod; +import android.content.pm.PermissionName; import android.content.res.AssetManager; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -6088,7 +6089,8 @@ public abstract class Context { @CheckResult(suggest="#enforcePermission(String,int,int,String)") @PackageManager.PermissionResult @PermissionMethod - public abstract int checkPermission(@NonNull String permission, int pid, int uid); + public abstract int checkPermission( + @NonNull @PermissionName String permission, int pid, int uid); /** @hide */ @SuppressWarnings("HiddenAbstractMethod") @@ -6121,7 +6123,7 @@ public abstract class Context { @CheckResult(suggest="#enforceCallingPermission(String,String)") @PackageManager.PermissionResult @PermissionMethod - public abstract int checkCallingPermission(@NonNull String permission); + public abstract int checkCallingPermission(@NonNull @PermissionName String permission); /** * Determine whether the calling process of an IPC <em>or you</em> have been @@ -6142,7 +6144,7 @@ public abstract class Context { @CheckResult(suggest="#enforceCallingOrSelfPermission(String,String)") @PackageManager.PermissionResult @PermissionMethod - public abstract int checkCallingOrSelfPermission(@NonNull String permission); + public abstract int checkCallingOrSelfPermission(@NonNull @PermissionName String permission); /** * Determine whether <em>you</em> have been granted a particular permission. @@ -6172,7 +6174,7 @@ public abstract class Context { */ @PermissionMethod public abstract void enforcePermission( - @NonNull String permission, int pid, int uid, @Nullable String message); + @NonNull @PermissionName String permission, int pid, int uid, @Nullable String message); /** * If the calling process of an IPC you are handling has not been @@ -6194,7 +6196,7 @@ public abstract class Context { */ @PermissionMethod public abstract void enforceCallingPermission( - @NonNull String permission, @Nullable String message); + @NonNull @PermissionName String permission, @Nullable String message); /** * If neither you nor the calling process of an IPC you are @@ -6211,7 +6213,7 @@ public abstract class Context { */ @PermissionMethod public abstract void enforceCallingOrSelfPermission( - @NonNull String permission, @Nullable String message); + @NonNull @PermissionName String permission, @Nullable String message); /** * Grant permission to access a specific Uri to another package, regardless diff --git a/core/java/android/content/pm/PermissionMethod.java b/core/java/android/content/pm/PermissionMethod.java index 021b2e1cd8f1..ba97342c5e3e 100644 --- a/core/java/android/content/pm/PermissionMethod.java +++ b/core/java/android/content/pm/PermissionMethod.java @@ -26,7 +26,7 @@ import java.lang.annotation.Target; * Documents that the subject method's job is to look * up whether the provided or calling uid/pid has the requested permission. * - * Methods should either return `void`, but potentially throw {@link SecurityException}, + * <p>Methods should either return `void`, but potentially throw {@link SecurityException}, * or return {@link android.content.pm.PackageManager.PermissionResult} `int`. * * @hide diff --git a/core/java/android/content/pm/PermissionName.java b/core/java/android/content/pm/PermissionName.java new file mode 100644 index 000000000000..719e13be05cc --- /dev/null +++ b/core/java/android/content/pm/PermissionName.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Denotes that the annotated {@link String} represents a permission name. + * + * @hide + */ +@Retention(CLASS) +@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD}) +public @interface PermissionName {} diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index c0e2864d1993..9bc7ffdef26e 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -2913,6 +2913,7 @@ public final class PowerManager { private int mFlags; @UnsupportedAppUsage private String mTag; + private int mTagHash; private final String mPackageName; private final IBinder mToken; private int mInternalCount; @@ -2921,7 +2922,6 @@ public final class PowerManager { private boolean mHeld; private WorkSource mWorkSource; private String mHistoryTag; - private final String mTraceName; private final int mDisplayId; private WakeLockStateListener mListener; private IWakeLockCallback mCallback; @@ -2931,9 +2931,9 @@ public final class PowerManager { WakeLock(int flags, String tag, String packageName, int displayId) { mFlags = flags; mTag = tag; + mTagHash = mTag.hashCode(); mPackageName = packageName; mToken = new Binder(); - mTraceName = "WakeLock (" + mTag + ")"; mDisplayId = displayId; } @@ -2942,7 +2942,8 @@ public final class PowerManager { synchronized (mToken) { if (mHeld) { Log.wtf(TAG, "WakeLock finalized while still held: " + mTag); - Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0); + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER, + "WakeLocks", mTagHash); try { mService.releaseWakeLock(mToken, 0); } catch (RemoteException e) { @@ -3012,7 +3013,8 @@ public final class PowerManager { // should immediately acquire the wake lock once again despite never having // been explicitly released by the keyguard. mHandler.removeCallbacks(mReleaser); - Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0); + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER, + "WakeLocks", mTag, mTagHash); try { mService.acquireWakeLock(mToken, mFlags, mTag, mPackageName, mWorkSource, mHistoryTag, mDisplayId, mCallback); @@ -3060,7 +3062,8 @@ public final class PowerManager { if (!mRefCounted || mInternalCount == 0) { mHandler.removeCallbacks(mReleaser); if (mHeld) { - Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0); + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER, + "WakeLocks", mTagHash); try { mService.releaseWakeLock(mToken, flags); } catch (RemoteException e) { @@ -3137,6 +3140,7 @@ public final class PowerManager { /** @hide */ public void setTag(String tag) { mTag = tag; + mTagHash = mTag.hashCode(); } /** @hide */ diff --git a/core/java/android/os/storage/IStorageManager.aidl b/core/java/android/os/storage/IStorageManager.aidl index df0bee7e1f2d..bc52744078ea 100644 --- a/core/java/android/os/storage/IStorageManager.aidl +++ b/core/java/android/os/storage/IStorageManager.aidl @@ -137,6 +137,7 @@ interface IStorageManager { void createUserKey(int userId, int serialNumber, boolean ephemeral) = 61; @EnforcePermission("STORAGE_INTERNAL") void destroyUserKey(int userId) = 62; + @EnforcePermission("STORAGE_INTERNAL") void unlockUserKey(int userId, int serialNumber, in byte[] secret) = 63; @EnforcePermission("STORAGE_INTERNAL") void lockUserKey(int userId) = 64; @@ -146,9 +147,7 @@ interface IStorageManager { @EnforcePermission("STORAGE_INTERNAL") void destroyUserStorage(in String volumeUuid, int userId, int flags) = 67; @EnforcePermission("STORAGE_INTERNAL") - void addUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 70; - @EnforcePermission("STORAGE_INTERNAL") - void fixateNewestUserKeyAuth(int userId) = 71; + void setUserKeyProtection(int userId, in byte[] secret) = 70; @EnforcePermission("MOUNT_FORMAT_FILESYSTEMS") void fstrim(int flags, IVoldTaskListener listener) = 72; AppFuseMount mountProxyFileDescriptorBridge() = 73; @@ -165,8 +164,6 @@ interface IStorageManager { @EnforcePermission("MOUNT_FORMAT_FILESYSTEMS") boolean needsCheckpoint() = 86; void abortChanges(in String message, boolean retry) = 87; - @EnforcePermission("STORAGE_INTERNAL") - void clearUserKeyAuth(int userId, int serialNumber, in byte[] secret) = 88; void fixupAppDir(in String path) = 89; void disableAppDataIsolation(in String pkgName, int pid, int userId) = 90; PendingIntent getManageSpaceActivityIntent(in String packageName, int requestCode) = 91; diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index c1606e89645e..38ac98425ae6 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1606,15 +1606,6 @@ public class StorageManager { } /** {@hide} */ - public void unlockUserKey(int userId, int serialNumber, byte[] secret) { - try { - mStorageManager.unlockUserKey(userId, serialNumber, secret); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - - /** {@hide} */ public void lockUserKey(int userId) { try { mStorageManager.lockUserKey(userId); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index cab6acbfe2b7..00633a2a9a26 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -393,6 +393,21 @@ public final class Settings { "android.settings.ACCESSIBILITY_DETAILS_SETTINGS"; /** + * Activity Action: Show settings to allow configuration of accessibility color and motion. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_ACCESSIBILITY_COLOR_MOTION_SETTINGS = + "android.settings.ACCESSIBILITY_COLOR_MOTION_SETTINGS"; + + /** * Activity Action: Show settings to allow configuration of Reduce Bright Colors. * <p> * In some cases, a matching Activity may not exist, so ensure you @@ -438,6 +453,21 @@ public final class Settings { "android.settings.COLOR_INVERSION_SETTINGS"; /** + * Activity Action: Show settings to allow configuration of text reading. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_TEXT_READING_SETTINGS = + "android.settings.TEXT_READING_SETTINGS"; + + /** * Activity Action: Show settings to control access to usage information. * <p> * In some cases, a matching Activity may not exist, so ensure you diff --git a/core/java/android/security/keymaster/KeymasterDefs.java b/core/java/android/security/keymaster/KeymasterDefs.java index 8efc5eb6b6ff..e720f1ab1523 100644 --- a/core/java/android/security/keymaster/KeymasterDefs.java +++ b/core/java/android/security/keymaster/KeymasterDefs.java @@ -65,6 +65,7 @@ public final class KeymasterDefs { public static final int KM_TAG_PADDING = Tag.PADDING; // KM_ENUM_REP | 6; public static final int KM_TAG_CALLER_NONCE = Tag.CALLER_NONCE; // KM_BOOL | 7; public static final int KM_TAG_MIN_MAC_LENGTH = Tag.MIN_MAC_LENGTH; // KM_UINT | 8; + public static final int KM_TAG_EC_CURVE = Tag.EC_CURVE; // KM_ENUM | 10; public static final int KM_TAG_RSA_PUBLIC_EXPONENT = Tag.RSA_PUBLIC_EXPONENT; // KM_ULONG | 200; public static final int KM_TAG_INCLUDE_UNIQUE_ID = Tag.INCLUDE_UNIQUE_ID; // KM_BOOL | 202; diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java index 1ec5325623ec..4f74ca72b4aa 100644 --- a/core/java/com/android/internal/app/ChooserListAdapter.java +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -86,7 +86,6 @@ public class ChooserListAdapter extends ResolverListAdapter { private final ChooserActivityLogger mChooserActivityLogger; private int mNumShortcutResults = 0; - private Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); private boolean mApplySharingAppLimits; // Reserve spots for incoming direct share targets by adding placeholders @@ -265,31 +264,20 @@ public class ChooserListAdapter extends ResolverListAdapter { return; } - if (!(info instanceof DisplayResolveInfo)) { - holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); - holder.bindIcon(info); - - if (info instanceof SelectableTargetInfo) { - // direct share targets should append the application name for a better readout - DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo(); - CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; - CharSequence extendedInfo = info.getExtendedInfo(); - String contentDescription = String.join(" ", info.getDisplayLabel(), - extendedInfo != null ? extendedInfo : "", appName); - holder.updateContentDescription(contentDescription); - } - } else { + holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); + holder.bindIcon(info); + if (info instanceof SelectableTargetInfo) { + // direct share targets should append the application name for a better readout + DisplayResolveInfo rInfo = ((SelectableTargetInfo) info).getDisplayResolveInfo(); + CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : ""; + CharSequence extendedInfo = info.getExtendedInfo(); + String contentDescription = String.join(" ", info.getDisplayLabel(), + extendedInfo != null ? extendedInfo : "", appName); + holder.updateContentDescription(contentDescription); + } else if (info instanceof DisplayResolveInfo) { DisplayResolveInfo dri = (DisplayResolveInfo) info; - holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel()); - LoadIconTask task = mIconLoaders.get(dri); - if (task == null) { - task = new LoadIconTask(dri, holder); - mIconLoaders.put(dri, task); - task.execute(); - } else { - // The holder was potentially changed as the underlying items were - // reshuffled, so reset the target holder - task.setViewHolder(holder); + if (!dri.hasDisplayIcon()) { + loadIcon(dri); } } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index c8bc204bd9aa..822393ff221e 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -55,6 +55,7 @@ import android.content.pm.UserInfo; import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Insets; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -1475,14 +1476,21 @@ public class ResolverActivity extends Activity implements mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0); boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK; - ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter(); - DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0); + final ResolverListAdapter inactiveAdapter = + mMultiProfilePagerAdapter.getInactiveListAdapter(); + final DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0); // Load the icon asynchronously ImageView icon = findViewById(R.id.icon); - ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask( - otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon)); - iconTask.execute(); + inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) { + @Override + protected void onPostExecute(Drawable drawable) { + if (!isDestroyed()) { + otherProfileResolveInfo.setDisplayIcon(drawable); + new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo); + } + } + }.execute(); ((TextView) findViewById(R.id.open_cross_profile)).setText( getResources().getString( diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java index 66fff5c13ab7..f6075b008f72 100644 --- a/core/java/com/android/internal/app/ResolverListAdapter.java +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -58,7 +58,10 @@ import com.android.internal.app.chooser.DisplayResolveInfo; import com.android.internal.app.chooser.TargetInfo; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class ResolverListAdapter extends BaseAdapter { private static final String TAG = "ResolverListAdapter"; @@ -87,6 +90,8 @@ public class ResolverListAdapter extends BaseAdapter { private Runnable mPostListReadyRunnable; private final boolean mIsAudioCaptureDevice; private boolean mIsTabLoaded; + private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>(); + private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>(); public ResolverListAdapter(Context context, List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, @@ -636,26 +641,47 @@ public class ResolverListAdapter extends BaseAdapter { if (info == null) { holder.icon.setImageDrawable( mContext.getDrawable(R.drawable.resolver_icon_placeholder)); + holder.bindLabel("", "", false); return; } - if (info instanceof DisplayResolveInfo - && !((DisplayResolveInfo) info).hasDisplayLabel()) { - getLoadLabelTask((DisplayResolveInfo) info, holder).execute(); - } else { - holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); + if (info instanceof DisplayResolveInfo) { + DisplayResolveInfo dri = (DisplayResolveInfo) info; + boolean hasLabel = dri.hasDisplayLabel(); + holder.bindLabel( + dri.getDisplayLabel(), + dri.getExtendedInfo(), + hasLabel && alwaysShowSubLabel()); + holder.bindIcon(info); + if (!hasLabel) { + loadLabel(dri); + } + if (!dri.hasDisplayIcon()) { + loadIcon(dri); + } } + } - if (info instanceof DisplayResolveInfo - && !((DisplayResolveInfo) info).hasDisplayIcon()) { - new LoadIconTask((DisplayResolveInfo) info, holder).execute(); - } else { - holder.bindIcon(info); + protected final void loadIcon(DisplayResolveInfo info) { + LoadIconTask task = mIconLoaders.get(info); + if (task == null) { + task = new LoadIconTask((DisplayResolveInfo) info); + mIconLoaders.put(info, task); + task.execute(); + } + } + + private void loadLabel(DisplayResolveInfo info) { + LoadLabelTask task = mLabelLoaders.get(info); + if (task == null) { + task = createLoadLabelTask(info); + mLabelLoaders.put(info, task); + task.execute(); } } - protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { - return new LoadLabelTask(info, holder); + protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { + return new LoadLabelTask(info); } public void onDestroy() { @@ -666,6 +692,16 @@ public class ResolverListAdapter extends BaseAdapter { if (mResolverListController != null) { mResolverListController.destroy(); } + cancelTasks(mIconLoaders.values()); + cancelTasks(mLabelLoaders.values()); + mIconLoaders.clear(); + mLabelLoaders.clear(); + } + + private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) { + for (T task: tasks) { + task.cancel(false); + } } private static ColorMatrixColorFilter getSuspendedColorMatrix() { @@ -883,11 +919,9 @@ public class ResolverListAdapter extends BaseAdapter { protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { private final DisplayResolveInfo mDisplayResolveInfo; - private final ViewHolder mHolder; - protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) { + protected LoadLabelTask(DisplayResolveInfo dri) { mDisplayResolveInfo = dri; - mHolder = holder; } @Override @@ -925,21 +959,22 @@ public class ResolverListAdapter extends BaseAdapter { @Override protected void onPostExecute(CharSequence[] result) { + if (mDisplayResolveInfo.hasDisplayLabel()) { + return; + } mDisplayResolveInfo.setDisplayLabel(result[0]); mDisplayResolveInfo.setExtendedInfo(result[1]); - mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel()); + notifyDataSetChanged(); } } class LoadIconTask extends AsyncTask<Void, Void, Drawable> { protected final DisplayResolveInfo mDisplayResolveInfo; private final ResolveInfo mResolveInfo; - private ViewHolder mHolder; - LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) { + LoadIconTask(DisplayResolveInfo dri) { mDisplayResolveInfo = dri; mResolveInfo = dri.getResolveInfo(); - mHolder = holder; } @Override @@ -953,17 +988,9 @@ public class ResolverListAdapter extends BaseAdapter { mResolverListCommunicator.updateProfileViewButton(); } else if (!mDisplayResolveInfo.hasDisplayIcon()) { mDisplayResolveInfo.setDisplayIcon(d); - mHolder.bindIcon(mDisplayResolveInfo); - // Notify in case view is already bound to resolve the race conditions on - // low end devices notifyDataSetChanged(); } } - - public void setViewHolder(ViewHolder holder) { - mHolder = holder; - mHolder.bindIcon(mDisplayResolveInfo); - } } /** diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 65c2d00fda06..55a26fee407f 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -26,6 +26,7 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.app.PropertyInvalidatedCache; import android.app.admin.DevicePolicyManager; import android.app.admin.PasswordMetrics; @@ -1784,4 +1785,16 @@ public class LockPatternUtils { re.rethrowFromSystemServer(); } } + + public void unlockUserKeyIfUnsecured(@UserIdInt int userId) { + getLockSettingsInternal().unlockUserKeyIfUnsecured(userId); + } + + public void createNewUser(@UserIdInt int userId, int userSerialNumber) { + getLockSettingsInternal().createNewUser(userId, userSerialNumber); + } + + public void removeUser(@UserIdInt int userId) { + getLockSettingsInternal().removeUser(userId); + } } diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java index 0a2c18f83fef..5b08bb1f42d3 100644 --- a/core/java/com/android/internal/widget/LockSettingsInternal.java +++ b/core/java/com/android/internal/widget/LockSettingsInternal.java @@ -18,6 +18,7 @@ package com.android.internal.widget; import android.annotation.IntDef; import android.annotation.Nullable; +import android.annotation.UserIdInt; import android.app.admin.PasswordMetrics; import java.lang.annotation.Retention; @@ -53,6 +54,37 @@ public abstract class LockSettingsInternal { // TODO(b/183140900) split store escrow key errors into detailed ones. /** + * Unlocks the credential-encrypted storage for the given user if the user is not secured, i.e. + * doesn't have an LSKF. + * <p> + * This doesn't throw an exception on failure; whether the storage has been unlocked can be + * determined by {@link StorageManager#isUserKeyUnlocked()}. + * + * @param userId the ID of the user whose storage to unlock + */ + public abstract void unlockUserKeyIfUnsecured(@UserIdInt int userId); + + /** + * Creates the locksettings state for a new user. + * <p> + * This includes creating a synthetic password and protecting it with an empty LSKF. + * + * @param userId the ID of the new user + * @param userSerialNumber the serial number of the new user + */ + public abstract void createNewUser(@UserIdInt int userId, int userSerialNumber); + + /** + * Removes the locksettings state for the given user. + * <p> + * This includes removing the user's synthetic password and any protectors that are protecting + * it. + * + * @param userId the ID of the user being removed + */ + public abstract void removeUser(@UserIdInt int userId); + + /** * Create an escrow token for the current user, which can later be used to unlock FBE * or change user password. * diff --git a/core/jni/android_media_AudioVolumeGroups.cpp b/core/jni/android_media_AudioVolumeGroups.cpp index 7098451901c4..1252e89935e5 100644 --- a/core/jni/android_media_AudioVolumeGroups.cpp +++ b/core/jni/android_media_AudioVolumeGroups.cpp @@ -94,6 +94,11 @@ static jint convertAudioVolumeGroupsFromNative( for (size_t j = 0; j < static_cast<size_t>(numAttributes); j++) { auto attributes = group.getAudioAttributes()[j]; + // Native & Java audio attributes default initializers are not aligned for the source. + // Given the volume group class concerns only playback, this field must be equal to the + // default java initializer. + attributes.source = AUDIO_SOURCE_INVALID; + jStatus = JNIAudioAttributeHelper::nativeToJava(env, &jAudioAttribute, attributes); if (jStatus != AUDIO_JAVA_SUCCESS) { goto exit; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index ded1b898d857..f0b1b2aa7e3d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3051,6 +3051,10 @@ <permission android:name="android.permission.QUERY_ADMIN_POLICY" android:protectionLevel="signature|role" /> + <!-- @SystemApi @hide Allows an application to exempt apps from platform restrictions.--> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS" + android:protectionLevel="signature|role" /> + <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.--> <permission android:name="android.permission.PROVISION_DEMO_DEVICE" android:protectionLevel="signature|setup" /> diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java index 56a70708b6df..2861428ece4d 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java @@ -46,14 +46,14 @@ public class ResolverWrapperAdapter extends ResolverListAdapter { } @Override - protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { - return new LoadLabelWrapperTask(info, holder); + protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { + return new LoadLabelWrapperTask(info); } class LoadLabelWrapperTask extends LoadLabelTask { - protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) { - super(dri, holder); + protected LoadLabelWrapperTask(DisplayResolveInfo dri) { + super(dri); } @Override diff --git a/keystore/java/android/security/KeyStore2.java b/keystore/java/android/security/KeyStore2.java index c2cd6ffc622c..f507d76d9e5c 100644 --- a/keystore/java/android/security/KeyStore2.java +++ b/keystore/java/android/security/KeyStore2.java @@ -32,6 +32,7 @@ import android.system.keystore2.ResponseCode; import android.util.Log; import java.util.Calendar; +import java.util.Objects; /** * @hide This should not be made public in its present form because it @@ -137,13 +138,13 @@ public class KeyStore2 { return new KeyStore2(); } - private synchronized IKeystoreService getService(boolean retryLookup) { + @NonNull private synchronized IKeystoreService getService(boolean retryLookup) { if (mBinder == null || retryLookup) { mBinder = IKeystoreService.Stub.asInterface(ServiceManager .getService(KEYSTORE2_SERVICE_NAME)); Binder.allowBlocking(mBinder.asBinder()); } - return mBinder; + return Objects.requireNonNull(mBinder); } void delete(KeyDescriptor descriptor) throws KeyStoreException { diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java index b631999c2c54..4e73bd9d3c82 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreECPublicKey.java @@ -18,13 +18,19 @@ package android.security.keystore2; import android.annotation.NonNull; import android.security.KeyStoreSecurityLevel; +import android.security.keymaster.KeymasterDefs; import android.security.keystore.KeyProperties; +import android.system.keystore2.Authorization; import android.system.keystore2.KeyDescriptor; import android.system.keystore2.KeyMetadata; +import java.security.AlgorithmParameters; +import java.security.NoSuchAlgorithmException; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; import java.security.spec.ECParameterSpec; import java.security.spec.ECPoint; +import java.security.spec.InvalidParameterSpecException; /** * {@link ECPublicKey} backed by keystore. @@ -56,11 +62,45 @@ public class AndroidKeyStoreECPublicKey extends AndroidKeyStorePublicKey impleme } } + private static String getEcCurveFromKeymaster(int ecCurve) { + switch (ecCurve) { + case android.hardware.security.keymint.EcCurve.P_224: + return "secp224r1"; + case android.hardware.security.keymint.EcCurve.P_256: + return "secp256r1"; + case android.hardware.security.keymint.EcCurve.P_384: + return "secp384r1"; + case android.hardware.security.keymint.EcCurve.P_521: + return "secp521r1"; + } + return ""; + } + + private ECParameterSpec getCurveSpec(String name) + throws NoSuchAlgorithmException, InvalidParameterSpecException { + AlgorithmParameters parameters = AlgorithmParameters.getInstance("EC"); + parameters.init(new ECGenParameterSpec(name)); + return parameters.getParameterSpec(ECParameterSpec.class); + } + @Override public AndroidKeyStorePrivateKey getPrivateKey() { + ECParameterSpec params = mParams; + for (Authorization a : getAuthorizations()) { + try { + if (a.keyParameter.tag == KeymasterDefs.KM_TAG_EC_CURVE) { + params = getCurveSpec(getEcCurveFromKeymaster( + a.keyParameter.value.getEcCurve())); + break; + } + } catch (Exception e) { + throw new RuntimeException("Unable to parse EC curve " + + a.keyParameter.value.getEcCurve()); + } + } return new AndroidKeyStoreECPrivateKey( getUserKeyDescriptor(), getKeyIdDescriptor().nspace, getAuthorizations(), - getSecurityLevel(), mParams); + getSecurityLevel(), params); } @Override diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java index b1338d164055..4caa47f2078b 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreKeyAgreementSpi.java @@ -31,6 +31,8 @@ import java.security.NoSuchAlgorithmException; import java.security.ProviderException; import java.security.PublicKey; import java.security.SecureRandom; +import java.security.interfaces.ECKey; +import java.security.interfaces.XECKey; import java.security.spec.AlgorithmParameterSpec; import java.util.ArrayList; import java.util.List; @@ -132,6 +134,15 @@ public class AndroidKeyStoreKeyAgreementSpi extends KeyAgreementSpi throw new InvalidKeyException("key == null"); } else if (!(key instanceof PublicKey)) { throw new InvalidKeyException("Only public keys supported. Key: " + key); + } else if (!(mKey instanceof ECKey && key instanceof ECKey) + && !(mKey instanceof XECKey && key instanceof XECKey)) { + throw new InvalidKeyException( + "Public and Private key should be of the same type:"); + } else if (mKey instanceof ECKey + && !((ECKey) key).getParams().getCurve() + .equals(((ECKey) mKey).getParams().getCurve())) { + throw new InvalidKeyException( + "Public and Private key parameters should be same."); } else if (!lastPhase) { throw new IllegalStateException( "Only one other party supported. lastPhase must be set to true."); diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java index 42589640d2b7..e392c8dcca93 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreXDHPrivateKey.java @@ -22,16 +22,18 @@ import android.system.keystore2.Authorization; import android.system.keystore2.KeyDescriptor; import java.security.PrivateKey; -import java.security.interfaces.EdECKey; +import java.security.interfaces.XECPrivateKey; import java.security.spec.NamedParameterSpec; +import java.util.Optional; /** * X25519 Private Key backed by Keystore. - * instance of {@link PrivateKey} and {@link EdECKey} + * instance of {@link PrivateKey} and {@link XECPrivateKey} * * @hide */ -public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey implements EdECKey { +public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey + implements XECPrivateKey { public AndroidKeyStoreXDHPrivateKey( @NonNull KeyDescriptor descriptor, long keyId, @NonNull Authorization[] authorizations, @@ -44,4 +46,12 @@ public class AndroidKeyStoreXDHPrivateKey extends AndroidKeyStorePrivateKey impl public NamedParameterSpec getParams() { return NamedParameterSpec.X25519; } + + @Override + public Optional<byte[]> getScalar() { + /* An empty Optional if the scalar cannot be extracted (e.g. if the provider is a hardware + * token and the private key is not allowed to leave the crypto boundary). + */ + return Optional.empty(); + } } diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index afd3aac9b461..70755e6cc3cf 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -119,7 +119,7 @@ <!-- Temporarily extending the background to show an edu text hint for opening the menu --> <FrameLayout - android:id="@+id/tv_pip_menu_edu_text_container" + android:id="@+id/tv_pip_menu_edu_text_drawer_placeholder" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/tv_pip" @@ -127,23 +127,8 @@ android:layout_alignStart="@+id/tv_pip" android:layout_alignEnd="@+id/tv_pip" android:background="@color/tv_pip_menu_background" - android:clipChildren="true"> - - <TextView - android:id="@+id/tv_pip_menu_edu_text" - android:layout_width="wrap_content" - android:layout_height="@dimen/pip_menu_edu_text_view_height" - android:layout_gravity="bottom|center" - android:gravity="center" - android:clickable="false" - android:paddingBottom="@dimen/pip_menu_border_width" - android:text="@string/pip_edu_text" - android:singleLine="true" - android:ellipsize="marquee" - android:marqueeRepeatLimit="1" - android:scrollHorizontally="true" - android:textAppearance="@style/TvPipEduText"/> - </FrameLayout> + android:paddingBottom="@dimen/pip_menu_border_width" + android:paddingTop="@dimen/pip_menu_border_width"/> <!-- Frame around the PiP content + edu text hint - used to highlight open menu --> <View diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index b45b9ec0c457..9833a88a1c0a 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -41,8 +41,10 @@ <dimen name="pip_menu_edu_text_view_height">24dp</dimen> <dimen name="pip_menu_edu_text_home_icon">9sp</dimen> <dimen name="pip_menu_edu_text_home_icon_outline">14sp</dimen> - <integer name="pip_edu_text_show_duration_ms">10500</integer> - <integer name="pip_edu_text_window_exit_animation_duration_ms">1000</integer> - <integer name="pip_edu_text_view_exit_animation_duration_ms">300</integer> + <integer name="pip_edu_text_scroll_times">2</integer> + <integer name="pip_edu_text_non_scroll_show_duration">10500</integer> + <integer name="pip_edu_text_start_scroll_delay">2000</integer> + <integer name="pip_edu_text_window_exit_animation_duration">1000</integer> + <integer name="pip_edu_text_view_exit_animation_duration">300</integer> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml index 2b7a13eac6ca..8f806cf56c9b 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -42,8 +42,8 @@ <!-- Educative text instructing the user to double press the HOME button to access the pip controls menu [CHAR LIMIT=50] --> - <string name="pip_edu_text"> Double press <annotation icon="home_icon"> HOME </annotation> for - controls </string> + <string name="pip_edu_text">Double press <annotation icon="home_icon">HOME</annotation> for + controls</string> <!-- Accessibility announcement when opening the PiP menu. [CHAR LIMIT=NONE] --> <string name="a11y_pip_menu_entered">Picture-in-Picture menu.</string> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index c4accde23d93..3b735fce3fa9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -69,7 +69,6 @@ import com.android.wm.shell.floating.FloatingTasksController; import com.android.wm.shell.freeform.FreeformComponents; import com.android.wm.shell.fullscreen.FullscreenTaskListener; import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController; -import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.onehanded.OneHanded; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; @@ -192,25 +191,6 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides - static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( - Context context, - ShellInit shellInit, - ShellCommandHandler shellCommandHandler, - SyncTransactionQueue syncTransactionQueue, - DisplayController displayController, - DisplayInsetsController displayInsetsController, - Optional<UnfoldAnimationController> unfoldAnimationController, - Optional<RecentTasksController> recentTasksOptional, - @ShellMainThread ShellExecutor mainExecutor, - @ShellMainThread Handler mainHandler - ) { - return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler, - syncTransactionQueue, displayController, displayInsetsController, - unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler); - } - - @WMSingleton - @Provides static CompatUIController provideCompatUIController(Context context, ShellInit shellInit, ShellController shellController, @@ -782,7 +762,6 @@ public abstract class WMShellBaseModule { DisplayInsetsController displayInsetsController, DragAndDropController dragAndDropController, ShellTaskOrganizer shellTaskOrganizer, - KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<BubbleController> bubblesOptional, Optional<SplitScreenController> splitScreenOptional, Optional<Pip> pipOptional, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 37a50b611039..47b665970d28 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -56,6 +56,7 @@ import com.android.wm.shell.freeform.FreeformTaskListener; import com.android.wm.shell.freeform.FreeformTaskTransitionHandler; import com.android.wm.shell.freeform.FreeformTaskTransitionObserver; import com.android.wm.shell.fullscreen.FullscreenTaskListener; +import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer; import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; @@ -620,6 +621,28 @@ public abstract class WMShellModule { } // + // Kids mode + // + @WMSingleton + @Provides + static KidsModeTaskOrganizer provideKidsModeTaskOrganizer( + Context context, + ShellInit shellInit, + ShellCommandHandler shellCommandHandler, + SyncTransactionQueue syncTransactionQueue, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + Optional<UnfoldAnimationController> unfoldAnimationController, + Optional<RecentTasksController> recentTasksOptional, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler + ) { + return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler, + syncTransactionQueue, displayController, displayInsetsController, + unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler); + } + + // // Misc // @@ -630,6 +653,7 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DefaultMixedHandler defaultMixedHandler, + KidsModeTaskOrganizer kidsModeTaskOrganizer, Optional<DesktopModeController> desktopModeController) { return new Object(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl index 4def15db2f52..2624ee536b58 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/IPip.aidl @@ -59,10 +59,15 @@ interface IPip { /** * Sets listener to get pinned stack animation callbacks. */ - oneway void setPinnedStackAnimationListener(IPipAnimationListener listener) = 3; + oneway void setPipAnimationListener(IPipAnimationListener listener) = 3; /** * Sets the shelf height and visibility. */ oneway void setShelfHeight(boolean visible, int shelfHeight) = 4; + + /** + * Sets the next pip animation type to be the alpha animation. + */ + oneway void setPipAnimationTypeToAlpha() = 5; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java index c06881ae6ad7..72b9dd37ac7d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java @@ -51,15 +51,6 @@ public interface Pip { } /** - * Sets both shelf visibility and its height. - * - * @param visible visibility of shelf. - * @param height to specify the height for shelf. - */ - default void setShelfHeight(boolean visible, int height) { - } - - /** * Set the callback when {@link PipTaskOrganizer#isInPip()} state is changed. * * @param callback The callback accepts the result of {@link PipTaskOrganizer#isInPip()} @@ -68,14 +59,6 @@ public interface Pip { default void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) {} /** - * Set the pinned stack with {@link PipAnimationController.AnimationType} - * - * @param animationType The pre-defined {@link PipAnimationController.AnimationType} - */ - default void setPinnedStackAnimationType(int animationType) { - } - - /** * Called when showing Pip menu. */ default void showPictureInPictureMenu() {} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index af47666efa5a..3345b1b2d0e8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -23,6 +23,7 @@ import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; @@ -1065,13 +1066,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setShelfHeight(boolean visible, int height) { - mMainExecutor.execute(() -> { - PipController.this.setShelfHeight(visible, height); - }); - } - - @Override public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { mMainExecutor.execute(() -> { PipController.this.setOnIsInPipStateChangedListener(callback); @@ -1079,13 +1073,6 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setPinnedStackAnimationType(int animationType) { - mMainExecutor.execute(() -> { - PipController.this.setPinnedStackAnimationType(animationType); - }); - } - - @Override public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) { mMainExecutor.execute(() -> { mPipBoundsState.addPipExclusionBoundsChangeCallback(listener); @@ -1178,8 +1165,8 @@ public class PipController implements PipTransitionController.PipTransitionCallb } @Override - public void setPinnedStackAnimationListener(IPipAnimationListener listener) { - executeRemoteCallWithTaskPermission(mController, "setPinnedStackAnimationListener", + public void setPipAnimationListener(IPipAnimationListener listener) { + executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", (controller) -> { if (listener != null) { mListener.register(listener); @@ -1188,5 +1175,13 @@ public class PipController implements PipTransitionController.PipTransitionCallb } }); } + + @Override + public void setPipAnimationTypeToAlpha() { + executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha", + (controller) -> { + controller.setPinnedStackAnimationType(ANIM_TYPE_ALPHA); + }); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java index acc0caf95e35..d7d335b856be 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java @@ -43,16 +43,16 @@ public class PipDoubleTapHelper { * <p>MAX - maximum allowed screen size</p> */ @IntDef(value = { - SIZE_SPEC_CUSTOM, SIZE_SPEC_DEFAULT, - SIZE_SPEC_MAX + SIZE_SPEC_MAX, + SIZE_SPEC_CUSTOM }) @Retention(RetentionPolicy.SOURCE) @interface PipSizeSpec {} - static final int SIZE_SPEC_CUSTOM = 2; static final int SIZE_SPEC_DEFAULT = 0; static final int SIZE_SPEC_MAX = 1; + static final int SIZE_SPEC_CUSTOM = 2; /** * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 85a544b2e8de..3e8de454bcff 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -127,7 +127,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private int mPipForceCloseDelay; private int mResizeAnimationDuration; - private int mEduTextWindowExitAnimationDurationMs; + private int mEduTextWindowExitAnimationDuration; public static Pip create( Context context, @@ -240,12 +240,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); - if (isPipShown()) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: > closing Pip.", TAG); - closePip(); - } - loadConfigurations(); mPipNotificationController.onConfigurationChanged(mContext); mTvPipBoundsAlgorithm.onConfigurationChanged(mContext); @@ -377,10 +371,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } @Override - public void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration) { - mPipTaskOrganizer.scheduleAnimateResizePip(newTargetBounds, + public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) { + mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds, animationDuration, rect -> mTvPipMenuController.updateExpansionState()); - mTvPipMenuController.onPipTransitionStarted(newTargetBounds); + mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds); } /** @@ -417,7 +411,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void closeEduText() { - updatePinnedStackBounds(mEduTextWindowExitAnimationDurationMs, false); + updatePinnedStackBounds(mEduTextWindowExitAnimationDuration, false); } private void registerSessionListenerForCurrentUser() { @@ -459,27 +453,30 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } @Override - public void onPipTransitionStarted(int direction, Rect pipBounds) { + public void onPipTransitionStarted(int direction, Rect currentPipBounds) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransition_Started(), state=%s", TAG, stateToName(mState)); - mTvPipMenuController.notifyPipAnimating(true); + "%s: onPipTransition_Started(), state=%s, direction=%d", + TAG, stateToName(mState), direction); } @Override public void onPipTransitionCanceled(int direction) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); - mTvPipMenuController.notifyPipAnimating(false); + mTvPipMenuController.onPipTransitionFinished( + PipAnimationController.isInPipDirection(direction)); } @Override public void onPipTransitionFinished(int direction) { - if (PipAnimationController.isInPipDirection(direction) && mState == STATE_NO_PIP) { + final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction); + if (enterPipTransition && mState == STATE_NO_PIP) { setState(STATE_PIP); } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransition_Finished(), state=%s", TAG, stateToName(mState)); - mTvPipMenuController.notifyPipAnimating(false); + "%s: onPipTransition_Finished(), state=%s, direction=%d", + TAG, stateToName(mState), direction); + mTvPipMenuController.onPipTransitionFinished(enterPipTransition); } private void setState(@State int state) { @@ -493,8 +490,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal final Resources res = mContext.getResources(); mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration); mPipForceCloseDelay = res.getInteger(R.integer.config_pipForceCloseDelay); - mEduTextWindowExitAnimationDurationMs = - res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms); + mEduTextWindowExitAnimationDuration = + res.getInteger(R.integer.pip_edu_text_window_exit_animation_duration); } private void registerTaskStackListenerCallback(TaskStackListenerImpl taskStackListener) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 176fdfee2512..ab7edbfaa4ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -60,9 +60,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private final SystemWindows mSystemWindows; private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; - private final int mPipMenuBorderWidth; - private final int mPipEduTextShowDurationMs; - private final int mPipEduTextHeight; private Delegate mDelegate; private SurfaceControl mLeash; @@ -85,8 +82,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis RectF mTmpDestinationRectF = new RectF(); Matrix mMoveTransform = new Matrix(); - private final Runnable mCloseEduTextRunnable = this::closeEduText; - public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, PipMediaController pipMediaController, Handler mainHandler) { @@ -109,12 +104,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis pipMediaController.addActionListener(this::onMediaActionsChanged); - mPipEduTextShowDurationMs = context.getResources() - .getInteger(R.integer.pip_edu_text_show_duration_ms); - mPipEduTextHeight = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); - mPipMenuBorderWidth = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_border_width); } void setDelegate(Delegate delegate) { @@ -152,15 +141,17 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis attachPipBackgroundView(); attachPipMenuView(); - mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-mPipMenuBorderWidth, - -mPipMenuBorderWidth, -mPipMenuBorderWidth, -mPipMenuBorderWidth)); - mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -mPipEduTextHeight)); - mMainHandler.postDelayed(mCloseEduTextRunnable, mPipEduTextShowDurationMs); + int pipEduTextHeight = mContext.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); + int pipMenuBorderWidth = mContext.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_border_width); + mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth, + -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight)); } private void attachPipMenuView() { - mPipMenuView = new TvPipMenuView(mContext); - mPipMenuView.setListener(this); + mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); maybeUpdateMenuViewActions(); @@ -192,11 +183,15 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis 0 /* height */), 0 /* displayId */, SHELL_ROOT_LAYER_PIP); } - void notifyPipAnimating(boolean animating) { - mPipMenuView.setEduTextActive(!animating); - if (!animating) { - mPipMenuView.onPipTransitionFinished(mTvPipBoundsState.isTvPipExpanded()); - } + void onPipTransitionFinished(boolean enterTransition) { + // There is a race between when this is called and when the last frame of the pip transition + // is drawn. To ensure that view updates are applied only when the animation has fully drawn + // and the menu view has been fully remeasured and relaid out, we add a small delay here by + // posting on the handler. + mMainHandler.post(() -> { + mPipMenuView.onPipTransitionFinished( + enterTransition, mTvPipBoundsState.isTvPipExpanded()); + }); } void showMovementMenuOnly() { @@ -219,7 +214,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } - maybeCloseEduText(); maybeUpdateMenuViewActions(); updateExpansionState(); @@ -232,25 +226,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mPipMenuView.updateBounds(mTvPipBoundsState.getBounds()); } - void onPipTransitionStarted(Rect finishBounds) { + void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { if (mPipMenuView != null) { - mPipMenuView.onPipTransitionStarted(finishBounds); - } - } - - private void maybeCloseEduText() { - if (mMainHandler.hasCallbacks(mCloseEduTextRunnable)) { - mMainHandler.removeCallbacks(mCloseEduTextRunnable); - mCloseEduTextRunnable.run(); + mPipMenuView.onPipTransitionToTargetBoundsStarted(targetBounds); } } - private void closeEduText() { - mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); - mPipMenuView.hideEduText(); - mDelegate.closeEduText(); - } - void updateGravity(int gravity) { mPipMenuView.showMovementHints(gravity); } @@ -332,7 +313,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void detach() { closeMenu(); - mMainHandler.removeCallbacks(mCloseEduTextRunnable); detachPipMenu(); mLeash = null; } @@ -578,6 +558,12 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mDelegate.togglePipExpansion(); } + @Override + public void onCloseEduText() { + mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); + mDelegate.closeEduText(); + } + interface Delegate { void movePipToFullscreen(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java new file mode 100644 index 000000000000..6eef22562caa --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuEduTextDrawer.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static android.view.Gravity.BOTTOM; +import static android.view.Gravity.CENTER; +import static android.view.View.GONE; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.text.Annotation; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannedString; +import android.text.TextUtils; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; + +import java.util.Arrays; + +/** + * The edu text drawer shows the user a hint for how to access the Picture-in-Picture menu. + * It displays a text in a drawer below the Picture-in-Picture window. The drawer has the same + * width as the Picture-in-Picture window. Depending on the Picture-in-Picture mode, there might + * not be enough space to fit the whole educational text in the available space. In such cases we + * apply a marquee animation to the TextView inside the drawer. + * + * The drawer is shown temporarily giving the user enough time to read it, after which it slides + * shut. We show the text for a duration calculated based on whether the text is marqueed or not. + */ +class TvPipMenuEduTextDrawer extends FrameLayout { + private static final String TAG = "TvPipMenuEduTextDrawer"; + + private static final float MARQUEE_DP_PER_SECOND = 30; // Copy of TextView.MARQUEE_DP_PER_SECOND + private static final int MARQUEE_RESTART_DELAY = 1200; // Copy of TextView.MARQUEE_DELAY + private final float mMarqueeAnimSpeed; // pixels per ms + + private final Runnable mCloseDrawerRunnable = this::closeDrawer; + private final Runnable mStartScrollEduTextRunnable = this::startScrollEduText; + + private final Handler mMainHandler; + private final Listener mListener; + private final TextView mEduTextView; + + TvPipMenuEduTextDrawer(@NonNull Context context, Handler mainHandler, Listener listener) { + super(context, null, 0, 0); + + mListener = listener; + mMainHandler = mainHandler; + + // Taken from TextView.Marquee calculation + mMarqueeAnimSpeed = + (MARQUEE_DP_PER_SECOND * context.getResources().getDisplayMetrics().density) / 1000f; + + mEduTextView = new TextView(mContext); + setupDrawer(); + } + + private void setupDrawer() { + final int eduTextHeight = mContext.getResources().getDimensionPixelSize( + R.dimen.pip_menu_edu_text_view_height); + final int marqueeRepeatLimit = mContext.getResources() + .getInteger(R.integer.pip_edu_text_scroll_times); + + mEduTextView.setLayoutParams( + new LayoutParams(MATCH_PARENT, eduTextHeight, BOTTOM | CENTER)); + mEduTextView.setGravity(CENTER); + mEduTextView.setClickable(false); + mEduTextView.setText(createEduTextString()); + mEduTextView.setSingleLine(); + mEduTextView.setTextAppearance(R.style.TvPipEduText); + mEduTextView.setEllipsize(TextUtils.TruncateAt.MARQUEE); + mEduTextView.setMarqueeRepeatLimit(marqueeRepeatLimit); + mEduTextView.setHorizontallyScrolling(true); + mEduTextView.setHorizontalFadingEdgeEnabled(true); + mEduTextView.setSelected(false); + addView(mEduTextView); + + setLayoutParams(new LayoutParams(MATCH_PARENT, eduTextHeight, CENTER)); + setClipChildren(true); + } + + /** + * Initializes the edu text. Should only be called once when the PiP is entered + */ + void init() { + ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: init()", TAG); + scheduleLifecycleEvents(); + } + + private void scheduleLifecycleEvents() { + final int startScrollDelay = mContext.getResources().getInteger( + R.integer.pip_edu_text_start_scroll_delay); + if (isEduTextMarqueed()) { + mMainHandler.postDelayed(mStartScrollEduTextRunnable, startScrollDelay); + } + mMainHandler.postDelayed(mCloseDrawerRunnable, startScrollDelay + getEduTextShowDuration()); + mEduTextView.getViewTreeObserver().addOnWindowAttachListener( + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + } + + @Override + public void onWindowDetached() { + mEduTextView.getViewTreeObserver().removeOnWindowAttachListener(this); + mMainHandler.removeCallbacks(mStartScrollEduTextRunnable); + mMainHandler.removeCallbacks(mCloseDrawerRunnable); + } + }); + } + + private int getEduTextShowDuration() { + int eduTextShowDuration; + if (isEduTextMarqueed()) { + // Calculate the time it takes to fully scroll the text once: time = distance / speed + final float singleMarqueeDuration = + getMarqueeAnimEduTextLineWidth() / mMarqueeAnimSpeed; + // The TextView adds a delay between each marquee repetition. Take that into account + final float durationFromStartToStart = singleMarqueeDuration + MARQUEE_RESTART_DELAY; + // Finally, multiply by the number of times we repeat the marquee animation + eduTextShowDuration = + (int) durationFromStartToStart * mEduTextView.getMarqueeRepeatLimit(); + } else { + eduTextShowDuration = mContext.getResources() + .getInteger(R.integer.pip_edu_text_non_scroll_show_duration); + } + + ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: getEduTextShowDuration(), showDuration=%d", + TAG, eduTextShowDuration); + return eduTextShowDuration; + } + + /** + * Returns true if the edu text width is bigger than the width of the text view, which indicates + * that the edu text will be marqueed + */ + private boolean isEduTextMarqueed() { + final int availableWidth = (int) mEduTextView.getWidth() + - mEduTextView.getCompoundPaddingLeft() + - mEduTextView.getCompoundPaddingRight(); + return availableWidth < getEduTextWidth(); + } + + /** + * Returns the width of a single marquee repetition of the edu text in pixels. + * This is the width from the start of the edu text to the start of the next edu + * text when it is marqueed. + * + * This is calculated based on the TextView.Marquee#start calculations + */ + private float getMarqueeAnimEduTextLineWidth() { + // When the TextView has a marquee animation, it puts a gap between the text end and the + // start of the next edu text repetition. The space is equal to a third of the TextView + // width + final float gap = mEduTextView.getWidth() / 3.0f; + return getEduTextWidth() + gap; + } + + private void startScrollEduText() { + ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, "%s: startScrollEduText(), repeat=%d", + TAG, mEduTextView.getMarqueeRepeatLimit()); + mEduTextView.setSelected(true); + } + + /** + * Returns the width of the edu text irrespective of the TextView width + */ + private int getEduTextWidth() { + return (int) mEduTextView.getLayout().getLineWidth(0); + } + + /** + * Closes the edu text drawer if it hasn't been closed yet + */ + void closeIfNeeded() { + if (mMainHandler.hasCallbacks(mCloseDrawerRunnable)) { + ProtoLog.d(WM_SHELL_PICTURE_IN_PICTURE, + "%s: close(), closing the edu text drawer because of user action", TAG); + mMainHandler.removeCallbacks(mCloseDrawerRunnable); + mCloseDrawerRunnable.run(); + } else { + // Do nothing, the drawer has already been closed + } + } + + private void closeDrawer() { + ProtoLog.i(WM_SHELL_PICTURE_IN_PICTURE, "%s: closeDrawer()", TAG); + final int eduTextFadeExitAnimationDuration = mContext.getResources().getInteger( + R.integer.pip_edu_text_view_exit_animation_duration); + final int eduTextSlideExitAnimationDuration = mContext.getResources().getInteger( + R.integer.pip_edu_text_window_exit_animation_duration); + + // Start fading out the edu text + mEduTextView.animate() + .alpha(0f) + .setInterpolator(TvPipInterpolators.EXIT) + .setDuration(eduTextFadeExitAnimationDuration) + .start(); + + // Start animation to close the drawer by animating its height to 0 + final ValueAnimator heightAnimation = ValueAnimator.ofInt(getHeight(), 0); + heightAnimation.setDuration(eduTextSlideExitAnimationDuration); + heightAnimation.setInterpolator(TvPipInterpolators.BROWSE); + heightAnimation.addUpdateListener(animator -> { + final ViewGroup.LayoutParams params = getLayoutParams(); + params.height = (int) animator.getAnimatedValue(); + setLayoutParams(params); + if (params.height == 0) { + setVisibility(GONE); + } + }); + heightAnimation.start(); + + mListener.onCloseEduText(); + } + + /** + * Creates the educational text that will be displayed to the user. Here we replace the + * HOME annotation in the String with an icon + */ + private CharSequence createEduTextString() { + final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text); + final SpannableString spannableString = new SpannableString(eduText); + Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst() + .ifPresent(annotation -> { + final Drawable icon = + getResources().getDrawable(R.drawable.home_icon, mContext.getTheme()); + if (icon != null) { + icon.mutate(); + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + spannableString.setSpan(new CenteredImageSpan(icon), + eduText.getSpanStart(annotation), + eduText.getSpanEnd(annotation), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + }); + + return spannableString; + } + + /** + * A listener for edu text drawer event states. + */ + interface Listener { + /** + * The edu text closing impacts the size of the Picture-in-Picture window and influences + * how it is positioned on the screen. + */ + void onCloseEduText(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 9cd05b0eea75..57e95c416b3c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -25,18 +25,11 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; -import android.animation.ValueAnimator; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.Context; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.os.Handler; -import android.text.Annotation; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.SpannedString; -import android.util.AttributeSet; import android.view.Gravity; import android.view.KeyEvent; import android.view.SurfaceControl; @@ -49,7 +42,6 @@ import android.widget.HorizontalScrollView; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ScrollView; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -61,7 +53,6 @@ import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -74,21 +65,16 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private static final int FIRST_CUSTOM_ACTION_POSITION = 3; - @Nullable - private Listener mListener; + private final Listener mListener; private final LinearLayout mActionButtonsContainer; private final View mMenuFrameView; private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; private final View mPipView; - private final TextView mEduTextView; - private final View mEduTextContainerView; + private final TvPipMenuEduTextDrawer mEduTextDrawer; private final int mPipMenuOuterSpace; private final int mPipMenuBorderWidth; - private final int mEduTextFadeExitAnimationDurationMs; - private final int mEduTextSlideExitAnimationDurationMs; - private int mEduTextHeight; private final ImageView mArrowUp; private final ImageView mArrowRight; @@ -116,25 +102,17 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final int mResizeAnimationDuration; private final AccessibilityManager mA11yManager; + private final Handler mMainHandler; - public TvPipMenuView(@NonNull Context context) { - this(context, null); - } - - public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public TvPipMenuView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); + public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler, + @NonNull Listener listener) { + super(context, null, 0, 0); inflate(context, R.layout.tv_pip_menu, this); + mMainHandler = mainHandler; + mListener = listener; + mA11yManager = context.getSystemService(AccessibilityManager.class); mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); @@ -166,9 +144,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left); mA11yDoneButton = findViewById(R.id.tv_pip_menu_done_button); - mEduTextView = findViewById(R.id.tv_pip_menu_edu_text); - mEduTextContainerView = findViewById(R.id.tv_pip_menu_edu_text_container); - mResizeAnimationDuration = context.getResources().getInteger( R.integer.config_pipResizeAnimationDuration); mPipMenuFadeAnimationDuration = context.getResources() @@ -178,63 +153,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { .getDimensionPixelSize(R.dimen.pip_menu_outer_space); mPipMenuBorderWidth = context.getResources() .getDimensionPixelSize(R.dimen.pip_menu_border_width); - mEduTextHeight = context.getResources() - .getDimensionPixelSize(R.dimen.pip_menu_edu_text_view_height); - mEduTextFadeExitAnimationDurationMs = context.getResources() - .getInteger(R.integer.pip_edu_text_view_exit_animation_duration_ms); - mEduTextSlideExitAnimationDurationMs = context.getResources() - .getInteger(R.integer.pip_edu_text_window_exit_animation_duration_ms); - - initEduText(); - } - void initEduText() { - final SpannedString eduText = (SpannedString) getResources().getText(R.string.pip_edu_text); - final SpannableString spannableString = new SpannableString(eduText); - Arrays.stream(eduText.getSpans(0, eduText.length(), Annotation.class)).findFirst() - .ifPresent(annotation -> { - final Drawable icon = - getResources().getDrawable(R.drawable.home_icon, mContext.getTheme()); - if (icon != null) { - icon.mutate(); - icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); - spannableString.setSpan(new CenteredImageSpan(icon), - eduText.getSpanStart(annotation), - eduText.getSpanEnd(annotation), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - }); - - mEduTextView.setText(spannableString); + mEduTextDrawer = new TvPipMenuEduTextDrawer(mContext, mainHandler, mListener); + ((FrameLayout) findViewById(R.id.tv_pip_menu_edu_text_drawer_placeholder)) + .addView(mEduTextDrawer); } - void setEduTextActive(boolean active) { - mEduTextView.setSelected(active); - } - - void hideEduText() { - final ValueAnimator heightAnimation = ValueAnimator.ofInt(mEduTextHeight, 0); - heightAnimation.setDuration(mEduTextSlideExitAnimationDurationMs); - heightAnimation.setInterpolator(TvPipInterpolators.BROWSE); - heightAnimation.addUpdateListener(animator -> { - mEduTextHeight = (int) animator.getAnimatedValue(); - }); - mEduTextView.animate() - .alpha(0f) - .setInterpolator(TvPipInterpolators.EXIT) - .setDuration(mEduTextFadeExitAnimationDurationMs) - .withEndAction(() -> { - mEduTextContainerView.setVisibility(GONE); - }).start(); - heightAnimation.start(); - } - - void onPipTransitionStarted(Rect finishBounds) { + void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { // Fade out content by fading in view on top. - if (mCurrentPipBounds != null && finishBounds != null) { + if (mCurrentPipBounds != null && targetBounds != null) { boolean ratioChanged = PipUtils.aspectRatioChanged( mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(), - finishBounds.width() / (float) finishBounds.height()); + targetBounds.width() / (float) targetBounds.height()); if (ratioChanged) { mPipBackground.animate() .alpha(1f) @@ -245,11 +175,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } // Update buttons. - final boolean vertical = finishBounds.height() > finishBounds.width(); + final boolean vertical = targetBounds.height() > targetBounds.width(); final boolean orientationChanged = vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onPipTransitionStarted(), orientation changed %b", TAG, orientationChanged); + "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b", + TAG, orientationChanged); if (!orientationChanged) { return; } @@ -261,18 +192,18 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { .setInterpolator(TvPipInterpolators.EXIT) .setDuration(mResizeAnimationDuration / 2) .withEndAction(() -> { - changeButtonScrollOrientation(finishBounds); - updateButtonGravity(finishBounds); + changeButtonScrollOrientation(targetBounds); + updateButtonGravity(targetBounds); // Only make buttons visible again in onPipTransitionFinished to keep in // sync with PiP content alpha animation. }); } else { - changeButtonScrollOrientation(finishBounds); - updateButtonGravity(finishBounds); + changeButtonScrollOrientation(targetBounds); + updateButtonGravity(targetBounds); } } - void onPipTransitionFinished(boolean isTvPipExpanded) { + void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); @@ -283,6 +214,10 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { .setInterpolator(TvPipInterpolators.ENTER) .start(); + if (enterTransition) { + mEduTextDrawer.init(); + } + setIsExpanded(isTvPipExpanded); // Update buttons. @@ -409,7 +344,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { Rect getPipMenuContainerBounds(Rect pipBounds) { final Rect menuUiBounds = new Rect(pipBounds); menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); - menuUiBounds.bottom += mEduTextHeight; + menuUiBounds.bottom += mEduTextDrawer.getHeight(); return menuUiBounds; } @@ -438,10 +373,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } - void setListener(@Nullable Listener listener) { - mListener = listener; - } - void setExpandedModeEnabled(boolean enabled) { mExpandButton.setVisibility(enabled ? VISIBLE : GONE); } @@ -460,21 +391,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { */ void showMoveMenu(int gravity) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - mButtonMenuIsVisible = false; - mMoveMenuIsVisible = true; showButtonsMenu(false); showMovementHints(gravity); setFrameHighlighted(true); mHorizontalScrollView.setFocusable(false); mScrollView.setFocusable(false); + + mEduTextDrawer.closeIfNeeded(); } void showButtonsMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showButtonsMenu()", TAG); - mButtonMenuIsVisible = true; - mMoveMenuIsVisible = false; showButtonsMenu(true); hideMovementHints(); setFrameHighlighted(true); @@ -501,8 +430,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); mFocusedButton = null; - mButtonMenuIsVisible = false; - mMoveMenuIsVisible = false; showButtonsMenu(false); hideMovementHints(); setFrameHighlighted(false); @@ -632,8 +559,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { @Override public void onClick(View v) { - if (mListener == null) return; - final int id = v.getId(); if (id == R.id.tv_pip_menu_fullscreen_button) { mListener.onFullscreenButtonClick(); @@ -662,7 +587,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (mListener != null && event.getAction() == ACTION_UP) { + if (event.getAction() == ACTION_UP) { if (!mMoveMenuIsVisible) { mFocusedButton = mActionButtonsContainer.getFocusedChild(); } @@ -700,6 +625,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity)); + if (mMoveMenuIsVisible) { + return; + } + mMoveMenuIsVisible = true; + animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); animateAlphaTo(checkGravity(gravity, Gravity.TOP) ? 1f : 0f, mArrowDown); animateAlphaTo(checkGravity(gravity, Gravity.RIGHT) ? 1f : 0f, mArrowLeft); @@ -714,9 +644,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton); if (a11yEnabled) { mA11yDoneButton.setOnClickListener(v -> { - if (mListener != null) { - mListener.onExitMoveMode(); - } + mListener.onExitMoveMode(); }); } } @@ -725,9 +653,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { arrowView.setClickable(enabled); if (enabled) { arrowView.setOnClickListener(v -> { - if (mListener != null) { - mListener.onPipMovement(keycode); - } + mListener.onPipMovement(keycode); }); } } @@ -743,6 +669,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideMovementHints()", TAG); + if (!mMoveMenuIsVisible) { + return; + } + mMoveMenuIsVisible = false; + animateAlphaTo(0, mArrowUp); animateAlphaTo(0, mArrowRight); animateAlphaTo(0, mArrowDown); @@ -756,19 +687,25 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { public void showButtonsMenu(boolean show) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showUserActions: %b", TAG, show); + if (mButtonMenuIsVisible == show) { + return; + } + mButtonMenuIsVisible = show; + if (show) { mActionButtonsContainer.setVisibility(VISIBLE); refocusPreviousButton(); } animateAlphaTo(show ? 1 : 0, mActionButtonsContainer); animateAlphaTo(show ? 1 : 0, mDimLayer); + mEduTextDrawer.closeIfNeeded(); } private void setFrameHighlighted(boolean highlighted) { mMenuFrameView.setActivated(highlighted); } - interface Listener { + interface Listener extends TvPipMenuEduTextDrawer.Listener { void onBackPress(); diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt index 4f9ab6f00838..c8aa6d20c91b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -16,14 +16,12 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory import com.android.server.wm.flicker.FlickerTestParameter import com.android.server.wm.flicker.dsl.FlickerBuilder -import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule @@ -108,14 +106,6 @@ class EnterPipOnUserLeaveHintTest(testSpec: FlickerTestParameter) : EnterPipTest @Presubmit @Test override fun entireScreenCovered() { - Assume.assumeFalse(isShellTransitionsEnabled) - super.entireScreenCovered() - } - - @FlakyTest(bugId = 227313015) - @Test - fun entireScreenCovered_ShellTransit() { - Assume.assumeTrue(isShellTransitionsEnabled) super.entireScreenCovered() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt index 56e5e27e21ce..2b629e73e750 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTest.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -83,8 +82,10 @@ open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec testSpec.assertWm { this.isAppWindowVisible(pipApp) } } - /** Checks [pipApp] layer remains visible throughout the animation */ - @FlakyTest(bugId = 239807171) + /** + * Checks [pipApp] layer remains visible throughout the animation + */ + @Presubmit @Test open fun pipAppLayerAlwaysVisible() { testSpec.assertLayers { this.isVisible(pipApp) } @@ -104,7 +105,7 @@ open class EnterPipTest(testSpec: FlickerTestParameter) : PipTransition(testSpec * Checks that the pip app layer remains inside the display bounds throughout the whole * animation */ - @FlakyTest(bugId = 239807171) + @Presubmit @Test open fun pipLayerRemainInsideVisibleBounds() { testSpec.assertLayersVisibleRegion(pipApp) { coversAtMost(displayBounds) } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt index 11bc1f7ea406..104b409731dd 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.pip import android.app.Activity import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice @@ -225,7 +224,7 @@ class EnterPipToOtherOrientationTest( } /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt index 8cb68facab08..5e3194ca4ee2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.flicker.pip -import android.platform.test.annotations.FlakyTest +import android.platform.test.annotations.Presubmit import android.view.Surface import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory @@ -76,12 +76,12 @@ class ExitPipViaExpandButtonClickTest( } /** {@inheritDoc} */ - @FlakyTest(bugId = 227313015) + @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() /** {@inheritDoc} */ - @FlakyTest(bugId = 197726610) + @Presubmit @Test override fun pipLayerExpands() = super.pipLayerExpands() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt index 18790713a828..3356d3e2ab2b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaIntentTest.kt @@ -74,8 +74,10 @@ class ExitPipViaIntentTest(testSpec: FlickerTestParameter) : ExitPipToAppTransit } } - /** {@inheritDoc} */ - @FlakyTest @Test override fun entireScreenCovered() = super.entireScreenCovered() + /** {@inheritDoc} */ + @Presubmit + @Test + override fun entireScreenCovered() = super.entireScreenCovered() /** {@inheritDoc} */ @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt new file mode 100644 index 000000000000..bcd01a414fd3 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExpandPipOnPinchOpenTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import android.platform.test.annotations.Postsubmit +import android.view.Surface +import androidx.test.filters.RequiresDevice +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test expanding a pip window via pinch out gesture. + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExpandPipOnPinchOpenTest(testSpec: FlickerTestParameter) : PipTransition(testSpec) { + override val transition: FlickerBuilder.() -> Unit + get() = buildTransition { + transitions { + pipApp.pinchOpenPipWindow(wmHelper, 0.4f, 30) + } + } + + /** + * Checks that the visible region area of [pipApp] always increases during the animation. + */ + @Postsubmit + @Test + fun pipLayerAreaIncreases() { + testSpec.assertLayers { + val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible } + pipLayerList.zipWithNext { previous, current -> + previous.visibleRegion.notBiggerThan(current.visibleRegion.region) + } + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestParameterFactory.getConfigNonRotationTests] for configuring + * repetitions, screen orientation and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests( + supportedRotations = listOf(Surface.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt index 1d12154d9be5..7de5494a7733 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipRotationTest.kt @@ -161,7 +161,7 @@ open class PipRotationTest(testSpec: FlickerTestParameter) : PipTransition(testS testSpec.assertWmEnd { isAboveWindow(pipApp, testApp) } } - @FlakyTest(bugId = 240499181) + @Presubmit @Test override fun navBarLayerIsVisibleAtStartAndEnd() { super.navBarLayerIsVisibleAtStartAndEnd() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt index 93be15484335..9b1247abfb71 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.IwTest -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice @@ -111,57 +110,60 @@ class CopyContentInSplit(testSpec: FlickerTestParameter) : SplitScreenBase(testS @Presubmit @Test fun textEditAppWindowKeepVisible() = testSpec.appWindowKeepVisible(textEditApp) /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + @Presubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerIsVisibleAtStartAndEnd() = super.statusBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt index 90e11f205ff1..cb49e18d672c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.IwTest -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice @@ -107,67 +106,67 @@ class DismissSplitScreenByGoHome( fun secondaryAppWindowBecomesInvisible() = testSpec.appWindowBecomesInvisible(secondaryApp) /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerIsVisibleAtStartAndEnd() = super.statusBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt index 6d821c491e82..504238f15b8c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt @@ -117,13 +117,13 @@ class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreen fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() @@ -135,49 +135,49 @@ class EnterSplitScreenFromOverview(testSpec: FlickerTestParameter) : SplitScreen super.navBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerIsVisibleAtStartAndEnd() = super.statusBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt index a340f6061ffb..2ecf81931e4a 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.IwTest -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice @@ -105,57 +104,60 @@ class SwitchBackToSplitFromAnotherApp(testSpec: FlickerTestParameter) : SplitScr fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + @Presubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerIsVisibleAtStartAndEnd() = super.statusBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt index 8fdc9d6cb3bf..384489d99de3 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.IwTest -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice @@ -104,57 +103,60 @@ class SwitchBackToSplitFromHome(testSpec: FlickerTestParameter) : SplitScreenBas fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + @Presubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerIsVisibleAtStartAndEnd() = super.statusBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt index 5180389b9e7c..04ebbf527b3d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.splitscreen import android.platform.test.annotations.IwTest -import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit import android.view.WindowManagerPolicyConstants import androidx.test.filters.RequiresDevice @@ -104,57 +103,60 @@ class SwitchBackToSplitFromRecent(testSpec: FlickerTestParameter) : SplitScreenB fun secondaryAppWindowBecomesVisible() = testSpec.appWindowBecomesVisible(secondaryApp) /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() + @Presubmit + @Test + override fun entireScreenCovered() = + super.entireScreenCovered() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerIsVisibleAtStartAndEnd() = super.statusBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() /** {@inheritDoc} */ - @Postsubmit + @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index f50168250c96..1a76943102b1 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -44,8 +44,6 @@ public class SystemSettings { Settings.System.DIM_SCREEN, Settings.System.SCREEN_OFF_TIMEOUT, Settings.System.SCREEN_BRIGHTNESS_MODE, - Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, - Settings.System.SCREEN_BRIGHTNESS_FOR_VR, Settings.System.ADAPTIVE_SLEEP, // moved to secure Settings.System.APPLY_RAMPING_RINGER, Settings.System.VIBRATE_INPUT_DEVICES, diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index b1979c905510..8f6924ced582 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -100,7 +100,9 @@ public class SettingsBackupTest { Settings.System.MIN_REFRESH_RATE, // depends on hardware capabilities Settings.System.PEAK_REFRESH_RATE, // depends on hardware capabilities Settings.System.SCREEN_BRIGHTNESS_FLOAT, + Settings.System.SCREEN_BRIGHTNESS_FOR_VR, Settings.System.SCREEN_BRIGHTNESS_FOR_VR_FLOAT, + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific ); diff --git a/packages/SystemUI/res-keyguard/values/ids.xml b/packages/SystemUI/res-keyguard/values/ids.xml new file mode 100644 index 000000000000..0dff4ffa3866 --- /dev/null +++ b/packages/SystemUI/res-keyguard/values/ids.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~ + --> + +<resources> + <item type="id" name="header_footer_views_added_tag_key" /> +</resources> diff --git a/packages/SystemUI/res/layout/media_ttt_chip.xml b/packages/SystemUI/res/layout/media_ttt_chip.xml index d88680669fe0..ae8e38e2634b 100644 --- a/packages/SystemUI/res/layout/media_ttt_chip.xml +++ b/packages/SystemUI/res/layout/media_ttt_chip.xml @@ -16,7 +16,7 @@ <!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view is the root view of a window, we cannot change the root view's margins.) --> <!-- Alphas start as 0 because the view will be animated in. --> -<FrameLayout +<com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/media_ttt_sender_chip" @@ -97,4 +97,4 @@ /> </LinearLayout> -</FrameLayout> +</com.android.systemui.media.taptotransfer.sender.MediaTttChipRootView> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 9324e8f3babb..637ac1911a85 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -246,6 +246,16 @@ <string name="screenrecord_start_label">Start Recording?</string> <!-- Message reminding the user that sensitive information may be captured during a screen recording [CHAR_LIMIT=NONE]--> <string name="screenrecord_description">While recording, Android System can capture any sensitive information that\u2019s visible on your screen or played on your device. This includes passwords, payment info, photos, messages, and audio.</string> + <!-- Dropdown option to record the entire screen [CHAR_LIMIT=30]--> + <string name="screenrecord_option_entire_screen">Record entire screen</string> + <!-- Dropdown option to record a single app [CHAR_LIMIT=30]--> + <string name="screenrecord_option_single_app">Record a single app</string> + <!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> + <string name="screenrecord_warning_entire_screen">While you\'re recording, Android has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string> + <!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]--> + <string name="screenrecord_warning_single_app">While you\'re recording an app, Android has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string> + <!-- Button to start a screen recording in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]--> + <string name="screenrecord_start_recording">Start recording</string> <!-- Label for a switch to enable recording audio [CHAR LIMIT=NONE]--> <string name="screenrecord_audio_label">Record audio</string> <!-- Label for the option to record audio from the device [CHAR LIMIT=NONE]--> @@ -958,7 +968,26 @@ <!-- Media projection permission dialog warning title. [CHAR LIMIT=NONE] --> <string name="media_projection_dialog_title">Start recording or casting with <xliff:g id="app_seeking_permission" example="Hangouts">%s</xliff:g>?</string> - <!-- Media projection permission dialog permanent grant check box. [CHAR LIMIT=NONE] --> + <!-- Media projection permission dialog title. [CHAR LIMIT=NONE] --> + <string name="media_projection_permission_dialog_title">Allow <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> to share or record?</string> + + <!-- Media projection permission dropdown option for capturing the whole screen. [CHAR LIMIT=30] --> + <string name="media_projection_permission_dialog_option_entire_screen">Entire screen</string> + + <!-- Media projection permission dropdown option for capturing single app. [CHAR LIMIT=30] --> + <string name="media_projection_permission_dialog_option_single_app">A single app</string> + + <!-- Media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] --> + <string name="media_projection_permission_dialog_warning_entire_screen">When you\'re sharing, recording, or casting, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything visible on your screen or played on your device. So be careful with passwords, payment details, messages, or other sensitive information.</string> + + <!-- Media projection permission warning for capturing an app. [CHAR LIMIT=350] --> + <string name="media_projection_permission_dialog_warning_single_app">When you\'re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with passwords, payment details, messages, or other sensitive information.</string> + + <!-- Media projection permission button to continue with app selection or recording [CHAR LIMIT=60] --> + <string name="media_projection_permission_dialog_continue">Continue</string> + + <!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] --> + <string name="media_projection_permission_app_selector_title">Share or record an app</string> <!-- The text to clear all notifications. [CHAR LIMIT=60] --> <string name="clear_all_notifications_text">Clear all</string> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl index e77c65079456..2b2b05ce2fbf 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl @@ -81,11 +81,6 @@ interface ISystemUiProxy { */ void stopScreenPinning() = 17; - /* - * Notifies that the swipe-to-home (recents animation) is finished. - */ - void notifySwipeToHomeFinished() = 23; - /** * Notifies that quickstep will switch to a new task * @param rotation indicates which Surface.Rotation the gesture was started in diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java index efa5558f5088..b793fd22aed1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherPopupMenu.java @@ -66,10 +66,13 @@ public class KeyguardUserSwitcherPopupMenu extends ListPopupWindow { listView.setDividerHeight(mContext.getResources().getDimensionPixelSize( R.dimen.bouncer_user_switcher_popup_divider_height)); - int height = mContext.getResources().getDimensionPixelSize( - R.dimen.bouncer_user_switcher_popup_header_height); - listView.addHeaderView(createSpacer(height), null, false); - listView.addFooterView(createSpacer(height), null, false); + if (listView.getTag(R.id.header_footer_views_added_tag_key) == null) { + int height = mContext.getResources().getDimensionPixelSize( + R.dimen.bouncer_user_switcher_popup_header_height); + listView.addHeaderView(createSpacer(height), null, false); + listView.addFooterView(createSpacer(height), null, false); + listView.setTag(R.id.header_footer_views_added_tag_key, new Object()); + } listView.setOnTouchListener((v, ev) -> { if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt new file mode 100644 index 000000000000..80b9c4e13c5c --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -0,0 +1,53 @@ +package com.android.keyguard.logging + +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.LogLevel +import com.android.systemui.log.LogLevel.DEBUG +import com.android.systemui.log.LogLevel.ERROR +import com.android.systemui.log.LogLevel.VERBOSE +import com.android.systemui.log.LogLevel.WARNING +import com.android.systemui.log.MessageInitializer +import com.android.systemui.log.MessagePrinter +import com.android.systemui.log.dagger.KeyguardLog +import com.google.errorprone.annotations.CompileTimeConstant +import javax.inject.Inject + +private const val TAG = "KeyguardLog" + +class KeyguardLogger @Inject constructor(@KeyguardLog private val buffer: LogBuffer) { + fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG) + + fun e(@CompileTimeConstant msg: String) = log(msg, ERROR) + + fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE) + + fun w(@CompileTimeConstant msg: String) = log(msg, WARNING) + + fun log(msg: String, level: LogLevel) = buffer.log(TAG, level, msg) + + private fun debugLog(messageInitializer: MessageInitializer, messagePrinter: MessagePrinter) { + buffer.log(TAG, DEBUG, messageInitializer, messagePrinter) + } + + // TODO: remove after b/237743330 is fixed + fun logStatusBarCalculatedAlpha(alpha: Float) { + debugLog({ double1 = alpha.toDouble() }, { "Calculated new alpha: $double1" }) + } + + // TODO: remove after b/237743330 is fixed + fun logStatusBarExplicitAlpha(alpha: Float) { + debugLog({ double1 = alpha.toDouble() }, { "new mExplicitAlpha value: $double1" }) + } + + // TODO: remove after b/237743330 is fixed + fun logStatusBarAlphaVisibility(visibility: Int, alpha: Float, state: String) { + debugLog( + { + int1 = visibility + double1 = alpha.toDouble() + str1 = state + }, + { "changing visibility to $int1 with alpha $double1 in state: $str1" } + ) + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 7a00cd930f2a..bf9f4c88bde3 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -45,7 +45,7 @@ class KeyguardUpdateMonitorLogger @Inject constructor( fun e(@CompileTimeConstant msg: String) = log(msg, ERROR) - fun v(@CompileTimeConstant msg: String) = log(msg, ERROR) + fun v(@CompileTimeConstant msg: String) = log(msg, VERBOSE) fun w(@CompileTimeConstant msg: String) = log(msg, WARNING) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 443d2774f0e0..06dbab980793 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -81,6 +81,7 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.statusbar.policy.dagger.SmartRepliesInflationModule; import com.android.systemui.statusbar.policy.dagger.StatusBarPolicyModule; import com.android.systemui.statusbar.window.StatusBarWindowModule; +import com.android.systemui.telephony.data.repository.TelephonyRepositoryModule; import com.android.systemui.tuner.dagger.TunerModule; import com.android.systemui.unfold.SysUIUnfoldModule; import com.android.systemui.user.UserModule; @@ -145,6 +146,7 @@ import dagger.Provides; StatusBarWindowModule.class, SysUIConcurrencyModule.class, SysUIUnfoldModule.class, + TelephonyRepositoryModule.class, TunerModule.class, UserModule.class, UtilModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 38d9d0210378..389a8c7700e2 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -68,6 +68,9 @@ public class Flags { public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true); + public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112, + false); + // next id: 112 /***************************************/ @@ -104,9 +107,26 @@ public class Flags { public static final ReleasedFlag MODERN_USER_SWITCHER_ACTIVITY = new ReleasedFlag(209, true); - /** Whether the new implementation of UserSwitcherController should be used. */ - public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER = - new UnreleasedFlag(210, false); + /** + * Whether the user interactor and repository should use `UserSwitcherController`. + * + * <p>If this is {@code false}, the interactor and repo skip the controller and directly access + * the framework APIs. + */ + public static final UnreleasedFlag USER_INTERACTOR_AND_REPO_USE_CONTROLLER = + new UnreleasedFlag(210, true); + + /** + * Whether `UserSwitcherController` should use the user interactor. + * + * <p>When this is {@code true}, the controller does not directly access framework APIs. + * Instead, it goes through the interactor. + * + * <p>Note: do not set this to true if {@link #USER_INTERACTOR_AND_REPO_USE_CONTROLLER} is + * {@code true} as it would created a cycle between controller -> interactor -> controller. + */ + public static final UnreleasedFlag USER_CONTROLLER_USES_INTERACTOR = + new UnreleasedFlag(211, false); /***************************************/ // 300 - power menu @@ -247,6 +267,13 @@ public class Flags { public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES = new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false); + @Keep + public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_BUBBLE = + new SysPropBooleanFlag(1108, "persist.wm.debug.fling_to_dismiss_bubble", true); + @Keep + public static final SysPropBooleanFlag ENABLE_FLING_TO_DISMISS_PIP = + new SysPropBooleanFlag(1109, "persist.wm.debug.fling_to_dismiss_pip", true); + // 1200 - predictive back @Keep public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag( 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 840a4b20a3f0..4c4b588888d1 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 @@ -85,6 +85,15 @@ interface KeyguardRepository { */ val dozeAmount: Flow<Float> + /** + * Returns `true` if the keyguard is showing; `false` otherwise. + * + * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in + * the z-order (which is not really above the system UI window, but rather - the lock-screen + * becomes invisible to reveal the "occluding activity"). + */ + fun isKeyguardShowing(): Boolean + /** Sets whether the bottom area UI should animate the transition out of doze state. */ fun setAnimateDozingTransitions(animate: Boolean) @@ -103,7 +112,7 @@ class KeyguardRepositoryImpl @Inject constructor( statusBarStateController: StatusBarStateController, - keyguardStateController: KeyguardStateController, + private val keyguardStateController: KeyguardStateController, dozeHost: DozeHost, ) : KeyguardRepository { private val _animateBottomAreaDozingTransitions = MutableStateFlow(false) @@ -168,6 +177,10 @@ constructor( awaitClose { statusBarStateController.removeCallback(callback) } } + override fun isKeyguardShowing(): Boolean { + return keyguardStateController.isShowing + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.value = animate } 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 dccc94178ed5..192919e32cf6 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 @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.Flow class KeyguardInteractor @Inject constructor( - repository: KeyguardRepository, + private val repository: KeyguardRepository, ) { /** * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at @@ -40,4 +40,8 @@ constructor( val isDozing: Flow<Boolean> = repository.isDozing /** Whether the keyguard is showing ot not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing + + fun isKeyguardShowing(): Boolean { + return repository.isKeyguardShowing() + } } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt new file mode 100644 index 000000000000..aef3471ea8ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardLog.kt @@ -0,0 +1,10 @@ +package com.android.systemui.log.dagger + +import javax.inject.Qualifier + +/** + * A [com.android.systemui.log.LogBuffer] for keyguard-related stuff. Should be used mostly for + * adding temporary logs or logging from smaller classes when creating new separate log class might + * be an overkill. + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index f2f6badde2cf..968dbe727c51 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -344,4 +344,14 @@ public class LogModule { public static LogBuffer provideUdfpsLogBuffer(LogBufferFactory factory) { return factory.create("UdfpsLog", 1000); } + + /** + * Provides a {@link LogBuffer} for general keyguard-related logs. + */ + @Provides + @SysUISingleton + @KeyguardLog + public static LogBuffer provideKeyguardLogBuffer(LogBufferFactory factory) { + return factory.create("KeyguardLog", 250); + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt index 4379d25406bf..aae973dcc1c7 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt @@ -25,6 +25,7 @@ import androidx.annotation.StringRes import com.android.internal.logging.UiEventLogger import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R +import com.android.systemui.plugins.FalsingManager import com.android.systemui.temporarydisplay.DEFAULT_TIMEOUT_MILLIS /** @@ -107,12 +108,15 @@ enum class ChipStateSender( controllerSender: MediaTttChipControllerSender, routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger + uiEventLogger: MediaTttSenderUiEventLogger, + falsingManager: FalsingManager, ): View.OnClickListener? { if (undoCallback == null) { return null } return View.OnClickListener { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + uiEventLogger.logUndoClicked( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_RECEIVER_CLICKED ) @@ -143,12 +147,15 @@ enum class ChipStateSender( controllerSender: MediaTttChipControllerSender, routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger + uiEventLogger: MediaTttSenderUiEventLogger, + falsingManager: FalsingManager, ): View.OnClickListener? { if (undoCallback == null) { return null } return View.OnClickListener { + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener + uiEventLogger.logUndoClicked( MediaTttSenderUiEvents.MEDIA_TTT_SENDER_UNDO_TRANSFER_TO_THIS_DEVICE_CLICKED ) @@ -215,7 +222,8 @@ enum class ChipStateSender( controllerSender: MediaTttChipControllerSender, routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, - uiEventLogger: MediaTttSenderUiEventLogger + uiEventLogger: MediaTttSenderUiEventLogger, + falsingManager: FalsingManager, ): View.OnClickListener? = null companion object { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt index e539f3fd842d..007eb8f8deee 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt @@ -22,25 +22,30 @@ import android.media.MediaRoute2Info import android.os.PowerManager import android.util.Log import android.view.Gravity +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.TextView import com.android.internal.statusbar.IUndoMediaTransferCallback +import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.animation.Interpolators import com.android.systemui.animation.ViewHierarchyAnimator +import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils +import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryDisplayRemovalReason import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.concurrency.DelayableExecutor +import dagger.Lazy import javax.inject.Inject /** @@ -57,7 +62,11 @@ class MediaTttChipControllerSender @Inject constructor( accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, - private val uiEventLogger: MediaTttSenderUiEventLogger + private val uiEventLogger: MediaTttSenderUiEventLogger, + // Added Lazy<> to delay the time we create Falsing instances. + // And overcome performance issue, check [b/247817628] for details. + private val falsingManager: Lazy<FalsingManager>, + private val falsingCollector: Lazy<FalsingCollector>, ) : TemporaryViewDisplayController<ChipSenderInfo, MediaTttLogger>( context, logger, @@ -70,6 +79,9 @@ class MediaTttChipControllerSender @Inject constructor( MediaTttUtils.WINDOW_TITLE, MediaTttUtils.WAKE_REASON, ) { + + private lateinit var parent: MediaTttChipRootView + override val windowLayoutParams = commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) } @@ -120,6 +132,15 @@ class MediaTttChipControllerSender @Inject constructor( val chipState = newInfo.state + // Detect falsing touches on the chip. + parent = currentView.requireViewById(R.id.media_ttt_sender_chip) + parent.touchHandler = object : Gefingerpoken { + override fun onTouchEvent(ev: MotionEvent?): Boolean { + falsingCollector.get().onTouchEvent(ev) + return false + } + } + // App icon val iconInfo = MediaTttUtils.getIconInfoFromPackageName( context, newInfo.routeInfo.clientPackageName, logger @@ -142,7 +163,11 @@ class MediaTttChipControllerSender @Inject constructor( // Undo val undoView = currentView.requireViewById<View>(R.id.undo) val undoClickListener = chipState.undoClickListener( - this, newInfo.routeInfo, newInfo.undoCallback, uiEventLogger + this, + newInfo.routeInfo, + newInfo.undoCallback, + uiEventLogger, + falsingManager.get(), ) undoView.setOnClickListener(undoClickListener) undoView.visibility = (undoClickListener != null).visibleIfTrue() diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt new file mode 100644 index 000000000000..3373159fba4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipRootView.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.taptotransfer.sender + +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.widget.FrameLayout +import com.android.systemui.Gefingerpoken + +/** A simple subclass that allows for observing touch events on chip. */ +class MediaTttChipRootView( + context: Context, + attrs: AttributeSet? +) : FrameLayout(context, attrs) { + + /** Assign this field to observe touch events. */ + var touchHandler: Gefingerpoken? = null + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + touchHandler?.onTouchEvent(ev) + return super.dispatchTouchEvent(ev) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 0f1338e4e872..b26b42c802f4 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -57,7 +57,6 @@ import android.window.BackEvent; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.policy.GestureNavigationSettingsObserver; -import com.android.internal.util.LatencyTracker; import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; @@ -199,7 +198,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final Rect mNavBarOverlayExcludedBounds = new Rect(); private final Region mExcludeRegion = new Region(); private final Region mUnrestrictedExcludeRegion = new Region(); - private final LatencyTracker mLatencyTracker; + private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider; private final Provider<BackGestureTfClassifierProvider> mBackGestureTfClassifierProviderProvider; private final FeatureFlags mFeatureFlags; @@ -339,7 +338,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker IWindowManager windowManagerService, Optional<Pip> pipOptional, FalsingManager falsingManager, - LatencyTracker latencyTracker, + Provider<NavigationBarEdgePanel> navigationBarEdgePanelProvider, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, FeatureFlags featureFlags) { super(broadcastDispatcher); @@ -358,7 +357,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mWindowManagerService = windowManagerService; mPipOptional = pipOptional; mFalsingManager = falsingManager; - mLatencyTracker = latencyTracker; + mNavBarEdgePanelProvider = navigationBarEdgePanelProvider; mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider; mFeatureFlags = featureFlags; mLastReportedConfig.setTo(mContext.getResources().getConfiguration()); @@ -583,8 +582,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker setEdgeBackPlugin( mBackPanelControllerFactory.create(mContext)); } else { - setEdgeBackPlugin( - new NavigationBarEdgePanel(mContext, mLatencyTracker)); + setEdgeBackPlugin(mNavBarEdgePanelProvider.get()); } } @@ -1091,7 +1089,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker private final IWindowManager mWindowManagerService; private final Optional<Pip> mPipOptional; private final FalsingManager mFalsingManager; - private final LatencyTracker mLatencyTracker; + private final Provider<NavigationBarEdgePanel> mNavBarEdgePanelProvider; private final Provider<BackGestureTfClassifierProvider> mBackGestureTfClassifierProviderProvider; private final FeatureFlags mFeatureFlags; @@ -1111,7 +1109,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker IWindowManager windowManagerService, Optional<Pip> pipOptional, FalsingManager falsingManager, - LatencyTracker latencyTracker, + Provider<NavigationBarEdgePanel> navBarEdgePanelProvider, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, FeatureFlags featureFlags) { @@ -1129,7 +1127,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mWindowManagerService = windowManagerService; mPipOptional = pipOptional; mFalsingManager = falsingManager; - mLatencyTracker = latencyTracker; + mNavBarEdgePanelProvider = navBarEdgePanelProvider; mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider; mFeatureFlags = featureFlags; } @@ -1152,7 +1150,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker mWindowManagerService, mPipOptional, mFalsingManager, - mLatencyTracker, + mNavBarEdgePanelProvider, mBackGestureTfClassifierProviderProvider, mFeatureFlags); } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java index 24efc762b39b..1230708d780a 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/NavigationBarEdgePanel.java @@ -52,9 +52,9 @@ import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.util.LatencyTracker; import com.android.settingslib.Utils; -import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.statusbar.VibratorHelper; @@ -62,6 +62,8 @@ import com.android.systemui.statusbar.VibratorHelper; import java.io.PrintWriter; import java.util.concurrent.Executor; +import javax.inject.Inject; + public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPlugin { private static final String TAG = "NavigationBarEdgePanel"; @@ -282,11 +284,16 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl }; private BackCallback mBackCallback; - public NavigationBarEdgePanel(Context context, LatencyTracker latencyTracker) { + @Inject + public NavigationBarEdgePanel( + Context context, + LatencyTracker latencyTracker, + VibratorHelper vibratorHelper, + @Background Executor backgroundExecutor) { super(context); mWindowManager = context.getSystemService(WindowManager.class); - mVibratorHelper = Dependency.get(VibratorHelper.class); + mVibratorHelper = vibratorHelper; mDensity = context.getResources().getDisplayMetrics().density; @@ -358,7 +365,6 @@ public class NavigationBarEdgePanel extends View implements NavigationEdgeBackPl setVisibility(GONE); - Executor backgroundExecutor = Dependency.get(Dependency.BACKGROUND_EXECUTOR); boolean isPrimaryDisplay = mContext.getDisplayId() == DEFAULT_DISPLAY; mRegionSamplingHelper = new RegionSamplingHelper(this, new RegionSamplingHelper.SamplingCallback() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index b3e6f8ab3de6..7a44058a46c7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -60,6 +60,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; @@ -82,7 +83,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private static final String EXTRA_VISIBLE = "visible"; private final Rect mQsBounds = new Rect(); - private final StatusBarStateController mStatusBarStateController; + private final SysuiStatusBarStateController mStatusBarStateController; private final FalsingManager mFalsingManager; private final KeyguardBypassController mBypassController; private boolean mQsExpanded; @@ -159,7 +160,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca * Progress of pull down from the center of the lock screen. * @see com.android.systemui.statusbar.LockscreenShadeTransitionController */ - private float mFullShadeProgress; + private float mLockscreenToShadeProgress; private boolean mOverScrolling; @@ -177,7 +178,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca @Inject public QSFragment(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, QSTileHost qsTileHost, - StatusBarStateController statusBarStateController, CommandQueue commandQueue, + SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue, @Named(QS_PANEL) MediaHost qsMediaHost, @Named(QUICK_QS_PANEL) MediaHost qqsMediaHost, KeyguardBypassController keyguardBypassController, @@ -442,20 +443,19 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } private void updateQsState() { - final boolean expanded = mQsExpanded || mInSplitShade; - final boolean expandVisually = expanded || mStackScrollerOverscrolling + final boolean expandVisually = mQsExpanded || mStackScrollerOverscrolling || mHeaderAnimating; - mQSPanelController.setExpanded(expanded); + mQSPanelController.setExpanded(mQsExpanded); boolean keyguardShowing = isKeyguardState(); - mHeader.setVisibility((expanded || !keyguardShowing || mHeaderAnimating + mHeader.setVisibility((mQsExpanded || !keyguardShowing || mHeaderAnimating || mShowCollapsedOnKeyguard) ? View.VISIBLE : View.INVISIBLE); mHeader.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) - || (expanded && !mStackScrollerOverscrolling), mQuickQSPanelController); + || (mQsExpanded && !mStackScrollerOverscrolling), mQuickQSPanelController); boolean qsPanelVisible = !mQsDisabled && expandVisually; - boolean footerVisible = qsPanelVisible && (expanded || !keyguardShowing || mHeaderAnimating - || mShowCollapsedOnKeyguard); + boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing + || mHeaderAnimating || mShowCollapsedOnKeyguard); mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); if (mQSFooterActionController != null) { mQSFooterActionController.setVisible(footerVisible); @@ -463,7 +463,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mQSFooterActionsViewModel.onVisibilityChangeRequested(footerVisible); } mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) - || (expanded && !mStackScrollerOverscrolling)); + || (mQsExpanded && !mStackScrollerOverscrolling)); mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE); if (DEBUG) { Log.d(TAG, "Footer: " + footerVisible + ", QS Panel: " + qsPanelVisible); @@ -586,7 +586,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mTransitioningToFullShade = isTransitioningToFullShade; updateShowCollapsedOnKeyguard(); } - mFullShadeProgress = qsTransitionFraction; + mLockscreenToShadeProgress = qsTransitionFraction; setQsExpansion(mLastQSExpansion, mLastPanelFraction, mLastHeaderTranslation, isTransitioningToFullShade ? qsSquishinessFraction : mSquishinessFraction); } @@ -710,10 +710,13 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } if (mInSplitShade) { // Large screens in landscape. - if (mTransitioningToFullShade || isKeyguardState()) { + // Need to check upcoming state as for unlocked -> AOD transition current state is + // not updated yet, but we're transitioning and UI should already follow KEYGUARD state + if (mTransitioningToFullShade || mStatusBarStateController.getCurrentOrUpcomingState() + == StatusBarState.KEYGUARD) { // Always use "mFullShadeProgress" on keyguard, because // "panelExpansionFractions" is always 1 on keyguard split shade. - return mFullShadeProgress; + return mLockscreenToShadeProgress; } else { return panelExpansionFraction; } @@ -722,7 +725,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca if (mTransitioningToFullShade) { // Only use this value during the standard lock screen shade expansion. During the // "quick" expansion from top, this value is 0. - return mFullShadeProgress; + return mLockscreenToShadeProgress; } else { return panelExpansionFraction; } @@ -930,7 +933,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation); indentingPw.println("mInSplitShade: " + mInSplitShade); indentingPw.println("mTransitioningToFullShade: " + mTransitioningToFullShade); - indentingPw.println("mFullShadeProgress: " + mFullShadeProgress); + indentingPw.println("mLockscreenToShadeProgress: " + mLockscreenToShadeProgress); indentingPw.println("mOverScrolling: " + mOverScrolling); indentingPw.println("isCustomizing: " + mQSCustomizerController.isCustomizing()); View view = getView(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 97476b2d1cde..d2d5063c7ae0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -134,7 +134,7 @@ public class UserDetailView extends PseudoGridView { v.bind(name, drawable, item.info.id); } v.setActivated(item.isCurrent); - v.setDisabledByAdmin(mController.isDisabledByAdmin(item)); + v.setDisabledByAdmin(item.isDisabledByAdmin()); v.setEnabled(item.isSwitchToEnabled); UserSwitcherController.setSelectableAlpha(v); @@ -173,16 +173,16 @@ public class UserDetailView extends PseudoGridView { Trace.beginSection("UserDetailView.Adapter#onClick"); UserRecord userRecord = (UserRecord) view.getTag(); - if (mController.isDisabledByAdmin(userRecord)) { + if (userRecord.isDisabledByAdmin()) { final Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent( - mContext, mController.getEnforcedAdmin(userRecord)); + mContext, userRecord.enforcedAdmin); mController.startActivity(intent); } else if (userRecord.isSwitchToEnabled) { MetricsLogger.action(mContext, MetricsEvent.QS_SWITCH_USER); mUiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH); if (!userRecord.isAddUser && !userRecord.isRestricted - && !mController.isDisabledByAdmin(userRecord)) { + && !userRecord.isDisabledByAdmin()) { if (mCurrentUserView != null) { mCurrentUserView.setActivated(false); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 7e2a5c51786d..899e57d7d0ae 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -342,14 +342,6 @@ public class OverviewProxyService extends CurrentUserTracker implements } @Override - public void notifySwipeToHomeFinished() { - verifyCallerAndClearCallingIdentity("notifySwipeToHomeFinished", () -> - mPipOptional.ifPresent( - pip -> pip.setPinnedStackAnimationType( - PipAnimationController.ANIM_TYPE_ALPHA))); - } - - @Override public void notifySwipeUpGestureStarted() { verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () -> notifySwipeUpGestureStartedInternal()); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java index 55602a98b8c5..e3658defc52a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ImageExporter.java @@ -19,6 +19,7 @@ package com.android.systemui.screenshot; import static android.os.FileUtils.closeQuietly; import android.annotation.IntRange; +import android.content.ContentProvider; import android.content.ContentResolver; import android.content.ContentValues; import android.graphics.Bitmap; @@ -29,6 +30,7 @@ import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.SystemClock; import android.os.Trace; +import android.os.UserHandle; import android.provider.MediaStore; import android.util.Log; @@ -142,8 +144,9 @@ class ImageExporter { * * @return a listenable future result */ - ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap) { - return export(executor, requestId, bitmap, ZonedDateTime.now()); + ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, + UserHandle owner) { + return export(executor, requestId, bitmap, ZonedDateTime.now(), owner); } /** @@ -155,10 +158,10 @@ class ImageExporter { * @return a listenable future result */ ListenableFuture<Result> export(Executor executor, UUID requestId, Bitmap bitmap, - ZonedDateTime captureTime) { + ZonedDateTime captureTime, UserHandle owner) { final Task task = new Task(mResolver, requestId, bitmap, captureTime, mCompressFormat, - mQuality, /* publish */ true); + mQuality, /* publish */ true, owner); return CallbackToFutureAdapter.getFuture( (completer) -> { @@ -174,28 +177,6 @@ class ImageExporter { ); } - /** - * Delete the entry. - * - * @param executor the thread for execution - * @param uri the uri of the image to publish - * - * @return a listenable future result - */ - ListenableFuture<Result> delete(Executor executor, Uri uri) { - return CallbackToFutureAdapter.getFuture((completer) -> { - executor.execute(() -> { - mResolver.delete(uri, null); - - Result result = new Result(); - result.uri = uri; - result.deleted = true; - completer.set(result); - }); - return "ContentResolver#delete"; - }); - } - static class Result { Uri uri; UUID requestId; @@ -203,7 +184,6 @@ class ImageExporter { long timestamp; CompressFormat format; boolean published; - boolean deleted; @Override public String toString() { @@ -214,7 +194,6 @@ class ImageExporter { sb.append(", timestamp=").append(timestamp); sb.append(", format=").append(format); sb.append(", published=").append(published); - sb.append(", deleted=").append(deleted); sb.append('}'); return sb.toString(); } @@ -227,17 +206,19 @@ class ImageExporter { private final ZonedDateTime mCaptureTime; private final CompressFormat mFormat; private final int mQuality; + private final UserHandle mOwner; private final String mFileName; private final boolean mPublish; Task(ContentResolver resolver, UUID requestId, Bitmap bitmap, ZonedDateTime captureTime, - CompressFormat format, int quality, boolean publish) { + CompressFormat format, int quality, boolean publish, UserHandle owner) { mResolver = resolver; mRequestId = requestId; mBitmap = bitmap; mCaptureTime = captureTime; mFormat = format; mQuality = quality; + mOwner = owner; mFileName = createFilename(mCaptureTime, mFormat); mPublish = publish; } @@ -253,7 +234,7 @@ class ImageExporter { start = Instant.now(); } - uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName); + uri = createEntry(mResolver, mFormat, mCaptureTime, mFileName, mOwner); throwIfInterrupted(); writeImage(mResolver, mBitmap, mFormat, mQuality, uri); @@ -297,15 +278,20 @@ class ImageExporter { } private static Uri createEntry(ContentResolver resolver, CompressFormat format, - ZonedDateTime time, String fileName) throws ImageExportException { + ZonedDateTime time, String fileName, UserHandle owner) throws ImageExportException { Trace.beginSection("ImageExporter_createEntry"); try { final ContentValues values = createMetadata(time, format, fileName); - Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + if (UserHandle.myUserId() != owner.getIdentifier()) { + baseUri = ContentProvider.maybeAddUserId(baseUri, owner.getIdentifier()); + } + Uri uri = resolver.insert(baseUri, values); if (uri == null) { throw new ImageExportException(RESOLVER_INSERT_RETURNED_NULL); } + Log.d(TAG, "Inserted new URI: " + uri); return uri; } finally { Trace.endSection(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java index ba6e98e79ac0..8bf956b86683 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java @@ -30,6 +30,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; +import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; @@ -387,7 +388,9 @@ public class LongScreenshotActivity extends Activity { mOutputBitmap = renderBitmap(drawable, bounds); ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export( - mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now()); + mBackgroundExecutor, UUID.randomUUID(), mOutputBitmap, ZonedDateTime.now(), + // TODO: Owner must match the owner of the captured window. + Process.myUserHandle()); exportFuture.addListener(() -> onExportCompleted(action, exportFuture), mUiExecutor); } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index f248d6913878..077ad35fd63f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -48,6 +48,8 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.systemui.R; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; import com.google.common.util.concurrent.ListenableFuture; @@ -71,6 +73,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; private final Context mContext; + private FeatureFlags mFlags; private final ScreenshotSmartActions mScreenshotSmartActions; private final ScreenshotController.SaveImageInBackgroundData mParams; private final ScreenshotController.SavedImageData mImageData; @@ -84,7 +87,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final ImageExporter mImageExporter; private long mImageTime; - SaveImageInBackgroundTask(Context context, ImageExporter exporter, + SaveImageInBackgroundTask( + Context context, + FeatureFlags flags, + ImageExporter exporter, ScreenshotSmartActions screenshotSmartActions, ScreenshotController.SaveImageInBackgroundData data, Supplier<ActionTransition> sharedElementTransition, @@ -92,6 +98,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { screenshotNotificationSmartActionsProvider ) { mContext = context; + mFlags = flags; mScreenshotSmartActions = screenshotSmartActions; mImageData = new ScreenshotController.SavedImageData(); mQuickShareData = new ScreenshotController.QuickShareData(); @@ -117,7 +124,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } // TODO: move to constructor / from ScreenshotRequest final UUID requestId = UUID.randomUUID(); - final UserHandle user = getUserHandleOfForegroundApplication(mContext); + final UserHandle user = mFlags.isEnabled(Flags.SCREENSHOT_WORK_PROFILE_POLICY) + ? mParams.owner : getUserHandleOfForegroundApplication(mContext); Thread.currentThread().setPriority(Thread.MAX_PRIORITY); @@ -133,8 +141,9 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Call synchronously here since already on a background thread. ListenableFuture<ImageExporter.Result> future = - mImageExporter.export(Runnable::run, requestId, image); + mImageExporter.export(Runnable::run, requestId, image, mParams.owner); ImageExporter.Result result = future.get(); + Log.d(TAG, "Saved screenshot: " + result); final Uri uri = result.uri; mImageTime = result.timestamp; @@ -157,6 +166,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } mImageData.uri = uri; + mImageData.owner = user; mImageData.smartActions = smartActions; mImageData.shareTransition = createShareAction(mContext, mContext.getResources(), uri); mImageData.editTransition = createEditAction(mContext, mContext.getResources(), uri); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index 3fee232b3465..df32d2081fde 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -34,6 +34,7 @@ import static java.util.Objects.requireNonNull; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.annotation.MainThread; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityOptions; @@ -57,7 +58,9 @@ import android.media.AudioSystem; import android.media.MediaPlayer; import android.net.Uri; import android.os.Bundle; +import android.os.Process; import android.os.RemoteException; +import android.os.UserHandle; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; @@ -90,6 +93,7 @@ import com.android.systemui.R; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.clipboardoverlay.ClipboardOverlayController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; import com.android.systemui.util.Assert; @@ -151,6 +155,7 @@ public class ScreenshotController { public Consumer<Uri> finisher; public ScreenshotController.ActionsReadyListener mActionsReadyListener; public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener; + public UserHandle owner; void clearImage() { image = null; @@ -167,6 +172,8 @@ public class ScreenshotController { public Notification.Action deleteAction; public List<Notification.Action> smartActions; public Notification.Action quickShareAction; + public UserHandle owner; + /** * POD for shared element transition. @@ -242,6 +249,7 @@ public class ScreenshotController { private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; private final WindowContext mContext; + private final FeatureFlags mFlags; private final ScreenshotNotificationsController mNotificationsController; private final ScreenshotSmartActions mScreenshotSmartActions; private final UiEventLogger mUiEventLogger; @@ -288,6 +296,7 @@ public class ScreenshotController { @Inject ScreenshotController( Context context, + FeatureFlags flags, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController screenshotNotificationsController, ScrollCaptureClient scrollCaptureClient, @@ -331,6 +340,7 @@ public class ScreenshotController { final Context displayContext = context.createDisplayContext(getDefaultDisplay()); mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); mWindowManager = mContext.getSystemService(WindowManager.class); + mFlags = flags; mAccessibilityManager = AccessibilityManager.getInstance(mContext); @@ -377,7 +387,6 @@ public class ScreenshotController { void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, Insets visibleInsets, int taskId, int userId, ComponentName topComponent, Consumer<Uri> finisher, RequestCallback requestCallback) { - // TODO: use task Id, userId, topComponent for smart handler Assert.isMainThread(); if (screenshot == null) { Log.e(TAG, "Got null bitmap from screenshot message"); @@ -395,7 +404,7 @@ public class ScreenshotController { } mCurrentRequestCallback = requestCallback; saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent, - showFlash); + showFlash, UserHandle.of(userId)); } /** @@ -543,14 +552,15 @@ public class ScreenshotController { return; } - saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true); + saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true, + Process.myUserHandle()); mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION), ClipboardOverlayController.SELF_PERMISSION); } private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, - Insets screenInsets, ComponentName topComponent, boolean showFlash) { + Insets screenInsets, ComponentName topComponent, boolean showFlash, UserHandle owner) { withWindowAttached(() -> mScreenshotView.announceForAccessibility( mContext.getResources().getString(R.string.screenshot_saving_title))); @@ -575,11 +585,11 @@ public class ScreenshotController { mScreenBitmap = screenshot; - if (!isUserSetupComplete()) { + if (!isUserSetupComplete(owner)) { Log.w(TAG, "User setup not complete, displaying toast only"); // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing // and sharing shouldn't be exposed to the user. - saveScreenshotAndToast(finisher); + saveScreenshotAndToast(owner, finisher); return; } @@ -587,7 +597,7 @@ public class ScreenshotController { mScreenBitmap.setHasAlpha(false); mScreenBitmap.prepareToDraw(); - saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady, + saveScreenshotInWorkerThread(owner, finisher, this::showUiOnActionsReady, this::showUiOnQuickShareActionReady); // The window is focusable by default @@ -853,11 +863,12 @@ public class ScreenshotController { * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on * failure). */ - private void saveScreenshotAndToast(Consumer<Uri> finisher) { + private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) { // Play the shutter sound to notify that we've taken a screenshot playCameraSound(); saveScreenshotInWorkerThread( + owner, /* onComplete */ finisher, /* actionsReadyListener */ imageData -> { if (DEBUG_CALLBACK) { @@ -925,9 +936,11 @@ public class ScreenshotController { /** * Creates a new worker thread and saves the screenshot to the media store. */ - private void saveScreenshotInWorkerThread(Consumer<Uri> finisher, - @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener, - @Nullable ScreenshotController.QuickShareActionReadyListener + private void saveScreenshotInWorkerThread( + UserHandle owner, + @NonNull Consumer<Uri> finisher, + @Nullable ActionsReadyListener actionsReadyListener, + @Nullable QuickShareActionReadyListener quickShareActionsReadyListener) { ScreenshotController.SaveImageInBackgroundData data = new ScreenshotController.SaveImageInBackgroundData(); @@ -935,13 +948,14 @@ public class ScreenshotController { data.finisher = finisher; data.mActionsReadyListener = actionsReadyListener; data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; + data.owner = owner; if (mSaveInBgTask != null) { // just log success/failure for the pre-existing screenshot mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); } - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter, + mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, mScreenshotSmartActions, data, getActionTransitionSupplier(), mScreenshotNotificationSmartActionsProvider); mSaveInBgTask.execute(); @@ -960,6 +974,15 @@ public class ScreenshotController { mScreenshotHandler.resetTimeout(); if (imageData.uri != null) { + if (!imageData.owner.equals(Process.myUserHandle())) { + // TODO: Handle non-primary user ownership (e.g. Work Profile) + // This image is owned by another user. Special treatment will be + // required in the UI (badging) as well as sending intents which can + // correctly forward those URIs on to be read (actions). + + Log.d(TAG, "*** Screenshot saved to a non-primary user (" + + imageData.owner + ") as " + imageData.uri); + } mScreenshotHandler.post(() -> { if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { @@ -1033,9 +1056,9 @@ public class ScreenshotController { } } - private boolean isUserSetupComplete() { - return Settings.Secure.getInt(mContext.getContentResolver(), - SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + private boolean isUserSetupComplete(UserHandle owner) { + return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) + .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; } /** diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt index c2a50609b6a5..3a3528606302 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt @@ -68,7 +68,9 @@ internal open class ScreenshotPolicyImpl @Inject constructor( } override suspend fun isManagedProfile(@UserIdInt userId: Int): Boolean { - return withContext(bgDispatcher) { userMgr.isManagedProfile(userId) } + val managed = withContext(bgDispatcher) { userMgr.isManagedProfile(userId) } + Log.d(TAG, "isManagedProfile: $managed") + return managed } private fun nonPipVisibleTask(info: RootTaskInfo): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java index 83b60fb23b90..30a0b8f2d76f 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java @@ -78,6 +78,7 @@ public class ScrollCaptureController { static class LongScreenshot { private final ImageTileSet mImageTileSet; private final Session mSession; + // TODO: Add UserHandle so LongScreenshots can adhere to work profile screenshot policy LongScreenshot(Session session, ImageTileSet imageTileSet) { mSession = session; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index d7e86b6e2919..f2d1b38fdb3a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -430,6 +430,7 @@ public final class NotificationPanelViewController extends PanelViewController { /** * Determines if QS should be already expanded when expanding shade. * Used for split shade, two finger gesture as well as accessibility shortcut to QS. + * It needs to be set when movement starts as it resets at the end of expansion/collapse. */ @VisibleForTesting boolean mQsExpandImmediate; @@ -1737,8 +1738,10 @@ public final class NotificationPanelViewController extends PanelViewController { } private void setQsExpandImmediate(boolean expandImmediate) { - mQsExpandImmediate = expandImmediate; - mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate); + if (expandImmediate != mQsExpandImmediate) { + mQsExpandImmediate = expandImmediate; + mPanelEventsEmitter.notifyExpandImmediateChange(expandImmediate); + } } private void setShowShelfOnly(boolean shelfOnly) { @@ -2479,17 +2482,23 @@ public final class NotificationPanelViewController extends PanelViewController { mDepthController.setQsPanelExpansion(qsExpansionFraction); mStatusBarKeyguardViewManager.setQsExpansion(qsExpansionFraction); - // updateQsExpansion will get called whenever mTransitionToFullShadeProgress or - // mLockscreenShadeTransitionController.getDragProgress change. - // When in lockscreen, getDragProgress indicates the true expanded fraction of QS - float shadeExpandedFraction = mTransitioningToFullShadeProgress > 0 - ? mLockscreenShadeTransitionController.getQSDragProgress() + float shadeExpandedFraction = isOnKeyguard() + ? getLockscreenShadeDragProgress() : getExpandedFraction(); mLargeScreenShadeHeaderController.setShadeExpandedFraction(shadeExpandedFraction); mLargeScreenShadeHeaderController.setQsExpandedFraction(qsExpansionFraction); mLargeScreenShadeHeaderController.setQsVisible(mQsVisible); } + private float getLockscreenShadeDragProgress() { + // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade + // transition. If that's not the case we should follow QS expansion fraction for when + // user is pulling from the same top to go directly to expanded QS + return mTransitioningToFullShadeProgress > 0 + ? mLockscreenShadeTransitionController.getQSDragProgress() + : computeQsExpansionFraction(); + } + private void onStackYChanged(boolean shouldAnimate) { if (mQs != null) { if (shouldAnimate) { @@ -3124,26 +3133,24 @@ public final class NotificationPanelViewController extends PanelViewController { } if (mQsExpandImmediate || (mQsExpanded && !mQsTracking && mQsExpansionAnimator == null && !mQsExpansionFromOverscroll)) { - float t; - if (mKeyguardShowing) { - + float qsExpansionFraction; + if (mSplitShadeEnabled) { + qsExpansionFraction = 1; + } else if (mKeyguardShowing) { // On Keyguard, interpolate the QS expansion linearly to the panel expansion - t = expandedHeight / (getMaxPanelHeight()); + qsExpansionFraction = expandedHeight / (getMaxPanelHeight()); } else { // In Shade, interpolate linearly such that QS is closed whenever panel height is // minimum QS expansion + minStackHeight - float - panelHeightQsCollapsed = + float panelHeightQsCollapsed = mNotificationStackScrollLayoutController.getIntrinsicPadding() + mNotificationStackScrollLayoutController.getLayoutMinHeight(); float panelHeightQsExpanded = calculatePanelHeightQsExpanded(); - t = - (expandedHeight - panelHeightQsCollapsed) / (panelHeightQsExpanded - - panelHeightQsCollapsed); + qsExpansionFraction = (expandedHeight - panelHeightQsCollapsed) + / (panelHeightQsExpanded - panelHeightQsCollapsed); } - float - targetHeight = - mQsMinExpansionHeight + t * (mQsMaxExpansionHeight - mQsMinExpansionHeight); + float targetHeight = mQsMinExpansionHeight + + qsExpansionFraction * (mQsMaxExpansionHeight - mQsMinExpansionHeight); setQsExpansion(targetHeight); } updateExpandedHeight(expandedHeight); @@ -3329,7 +3336,11 @@ public final class NotificationPanelViewController extends PanelViewController { } else { setListening(true); } - setQsExpandImmediate(false); + if (mBarState != SHADE) { + // updating qsExpandImmediate is done in onPanelStateChanged for unlocked shade but + // on keyguard panel state is always OPEN so we need to have that extra update + setQsExpandImmediate(false); + } setShowShelfOnly(false); mTwoFingerQsExpandPossible = false; updateTrackingHeadsUp(null); @@ -4678,12 +4689,19 @@ public final class NotificationPanelViewController extends PanelViewController { } } } else { + // this else branch means we are doing one of: + // - from KEYGUARD and SHADE (but not expanded shade) + // - from SHADE to KEYGUARD + // - from SHADE_LOCKED to SHADE + // - getting notified again about the current SHADE or KEYGUARD state final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE && statusBarState == KEYGUARD && mScreenOffAnimationController.isKeyguardShowDelayed(); if (!animatingUnlockedShadeToKeyguard) { // Only make the status bar visible if we're not animating the screen off, since // we only want to be showing the clock/notifications during the animation. + mShadeLog.v("Updating keyguard status bar state to " + + (keyguardShowing ? "visible" : "invisible")); mKeyguardStatusBarViewController.updateViewState( /* alpha= */ 1f, keyguardShowing ? View.VISIBLE : View.INVISIBLE); @@ -4749,9 +4767,7 @@ public final class NotificationPanelViewController extends PanelViewController { @Override public float getLockscreenShadeDragProgress() { - return mTransitioningToFullShadeProgress > 0 - ? mLockscreenShadeTransitionController.getQSDragProgress() - : computeQsExpansionFraction(); + return NotificationPanelViewController.this.getLockscreenShadeDragProgress(); } }; @@ -4988,6 +5004,7 @@ public final class NotificationPanelViewController extends PanelViewController { updateQSExpansionEnabledAmbient(); if (state == STATE_OPEN && mCurrentPanelState != state) { + setQsExpandImmediate(false); mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); } if (state == STATE_OPENING) { @@ -5000,6 +5017,7 @@ public final class NotificationPanelViewController extends PanelViewController { mCentralSurfaces.makeExpandedVisible(false); } if (state == STATE_CLOSED) { + setQsExpandImmediate(false); // Close the status bar in the next frame so we can show the end of the // animation. mView.post(mMaybeHideExpandedRunnable); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java index 8699441da726..c290ce260cc6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java @@ -42,7 +42,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.widget.LockPatternUtils; -import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; @@ -97,6 +96,7 @@ public class NotificationLockscreenUserManagerImpl implements private final List<UserChangedListener> mListeners = new ArrayList<>(); private final BroadcastDispatcher mBroadcastDispatcher; private final NotificationClickNotifier mClickNotifier; + private final Lazy<OverviewProxyService> mOverviewProxyServiceLazy; private boolean mShowLockscreenNotifications; private boolean mAllowLockscreenRemoteInput; @@ -157,7 +157,7 @@ public class NotificationLockscreenUserManagerImpl implements break; case Intent.ACTION_USER_UNLOCKED: // Start the overview connection to the launcher service - Dependency.get(OverviewProxyService.class).startConnectionToCurrentUser(); + mOverviewProxyServiceLazy.get().startConnectionToCurrentUser(); break; case NOTIFICATION_UNLOCKED_BY_WORK_CHALLENGE_ACTION: final IntentSender intentSender = intent.getParcelableExtra( @@ -199,6 +199,7 @@ public class NotificationLockscreenUserManagerImpl implements Lazy<NotificationVisibilityProvider> visibilityProviderLazy, Lazy<CommonNotifCollection> commonNotifCollectionLazy, NotificationClickNotifier clickNotifier, + Lazy<OverviewProxyService> overviewProxyServiceLazy, KeyguardManager keyguardManager, StatusBarStateController statusBarStateController, @Main Handler mainHandler, @@ -214,6 +215,7 @@ public class NotificationLockscreenUserManagerImpl implements mVisibilityProviderLazy = visibilityProviderLazy; mCommonNotifCollectionLazy = commonNotifCollectionLazy; mClickNotifier = clickNotifier; + mOverviewProxyServiceLazy = overviewProxyServiceLazy; statusBarStateController.addCallback(this); mLockPatternUtils = new LockPatternUtils(context); mKeyguardManager = keyguardManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index c900c5a2ff0b..4be5a1aa0215 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -43,7 +43,6 @@ import android.util.Log; import android.view.View; import android.widget.ImageView; -import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.animation.Interpolators; import com.android.systemui.colorextraction.SysuiColorExtractor; @@ -89,11 +88,9 @@ public class NotificationMediaManager implements Dumpable { private static final String TAG = "NotificationMediaManager"; public static final boolean DEBUG_MEDIA = false; - private final StatusBarStateController mStatusBarStateController - = Dependency.get(StatusBarStateController.class); - private final SysuiColorExtractor mColorExtractor = Dependency.get(SysuiColorExtractor.class); - private final KeyguardStateController mKeyguardStateController = Dependency.get( - KeyguardStateController.class); + private final StatusBarStateController mStatusBarStateController; + private final SysuiColorExtractor mColorExtractor; + private final KeyguardStateController mKeyguardStateController; private final KeyguardBypassController mKeyguardBypassController; private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>(); private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>(); @@ -179,6 +176,9 @@ public class NotificationMediaManager implements Dumpable { NotifCollection notifCollection, @Main DelayableExecutor mainExecutor, MediaDataManager mediaDataManager, + StatusBarStateController statusBarStateController, + SysuiColorExtractor colorExtractor, + KeyguardStateController keyguardStateController, DumpManager dumpManager) { mContext = context; mMediaArtworkProcessor = mediaArtworkProcessor; @@ -192,6 +192,9 @@ public class NotificationMediaManager implements Dumpable { mMediaDataManager = mediaDataManager; mNotifPipeline = notifPipeline; mNotifCollection = notifCollection; + mStatusBarStateController = statusBarStateController; + mColorExtractor = colorExtractor; + mKeyguardStateController = keyguardStateController; setupNotifPipeline(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 7cd79cac8928..11e3d1773c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -26,6 +26,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.animation.DialogLaunchAnimator; +import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; @@ -130,6 +131,9 @@ public interface CentralSurfacesDependenciesModule { NotifCollection notifCollection, @Main DelayableExecutor mainExecutor, MediaDataManager mediaDataManager, + StatusBarStateController statusBarStateController, + SysuiColorExtractor colorExtractor, + KeyguardStateController keyguardStateController, DumpManager dumpManager) { return new NotificationMediaManager( context, @@ -142,6 +146,9 @@ public interface CentralSurfacesDependenciesModule { notifCollection, mainExecutor, mediaDataManager, + statusBarStateController, + colorExtractor, + keyguardStateController, dumpManager); } 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 8278b549a7a0..ccf6feca6992 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 @@ -393,7 +393,7 @@ class HeadsUpCoordinator @Inject constructor( val posted = mPostedEntries.compute(entry.key) { _, value -> value?.also { update -> update.wasUpdated = true - update.shouldHeadsUpEver = update.shouldHeadsUpEver || shouldHeadsUpEver + update.shouldHeadsUpEver = shouldHeadsUpEver update.shouldHeadsUpAgain = update.shouldHeadsUpAgain || shouldHeadsUpAgain update.isAlerting = isAlerting update.isBinding = isBinding diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 801e544f03e6..8eef3f36433d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification import com.android.systemui.ForegroundServiceNotificationListener import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.people.widget.PeopleSpaceWidgetManager import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener @@ -38,6 +40,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.systemui.statusbar.phone.CentralSurfaces @@ -71,6 +74,8 @@ class NotificationsControllerImpl @Inject constructor( private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager, private val bubblesOptional: Optional<Bubbles>, private val fgsNotifListener: ForegroundServiceNotificationListener, + private val memoryMonitor: Lazy<NotificationMemoryMonitor>, + private val featureFlags: FeatureFlags ) : NotificationsController { override fun initialize( @@ -112,6 +117,9 @@ class NotificationsControllerImpl @Inject constructor( notificationLogger.setUpWithContainer(listContainer) peopleSpaceWidgetManager.attach(notificationListener) fgsNotifListener.init() + if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) { + memoryMonitor.get().init() + } } // TODO: Convert all functions below this line into listeners instead of public methods diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt new file mode 100644 index 000000000000..832a739a9080 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt @@ -0,0 +1,41 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +/** Describes usage of a notification. */ +data class NotificationMemoryUsage( + val packageName: String, + val notificationId: String, + val objectUsage: NotificationObjectUsage, +) + +/** + * Describes current memory usage of a [android.app.Notification] object. + * + * The values are in bytes. + */ +data class NotificationObjectUsage( + val smallIcon: Int, + val largeIcon: Int, + val extras: Int, + val style: String?, + val styleIcon: Int, + val bigPicture: Int, + val extender: Int, + val hasCustomView: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt new file mode 100644 index 000000000000..ef7fa335c3cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt @@ -0,0 +1,239 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.Person +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import android.util.Log +import androidx.annotation.WorkerThread +import androidx.core.util.contains +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import java.io.PrintWriter +import javax.inject.Inject + +/** This class monitors and logs current Notification memory use. */ +@SysUISingleton +class NotificationMemoryMonitor +@Inject +constructor( + val notificationPipeline: NotifPipeline, + val dumpManager: DumpManager, +) : Dumpable { + + companion object { + private const val TAG = "NotificationMemMonitor" + private const val CAR_EXTENSIONS = "android.car.EXTENSIONS" + private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon" + private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" + private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" + private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + } + + fun init() { + Log.d(TAG, "NotificationMemoryMonitor initialized.") + dumpManager.registerDumpable(javaClass.simpleName, this) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) } + } + + @WorkerThread + fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> { + return notificationMemoryUse(notificationPipeline.allNotifs) + } + + /** Returns a list of memory use entries for currently shown notifications. */ + @WorkerThread + fun notificationMemoryUse( + notifications: Collection<NotificationEntry> + ): List<NotificationMemoryUsage> { + return notifications.asSequence().map { entry -> + val packageName = entry.sbn.packageName + val notificationObjectUsage = + computeNotificationObjectUse(entry.sbn.notification, hashSetOf()) + NotificationMemoryUsage( + packageName, + NotificationUtils.logKey(entry.sbn.key), + notificationObjectUsage) + }.toList() + } + + /** + * Computes the estimated memory usage of a given [Notification] object. It'll attempt to + * inspect Bitmaps in the object and provide summary of memory usage. + */ + private fun computeNotificationObjectUse( + notification: Notification, + seenBitmaps: HashSet<Int> + ): NotificationObjectUsage { + val extras = notification.extras + val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps) + val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps) + + // Collect memory usage of extra styles + + // Big Picture + val bigPictureIconUse = + computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + + computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps) + val bigPictureUse = + computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) + + computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) + + // People + val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST) + val peopleUse = + peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0 + + // Calling + val callingPersonUse = + computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps) + val verificationIconUse = + computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps) + + // Messages + val messages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_MESSAGES) + ) + val messagesUse = + messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + val historicMessages = + Notification.MessagingStyle.Message.getMessagesFromBundleArray( + extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES) + ) + val historyicMessagesUse = + historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) } + + // Extenders + val carExtender = extras.getBundle(CAR_EXTENSIONS) + val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0 + val carExtenderIcon = + computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps) + + val tvExtender = extras.getBundle(TV_EXTENSIONS) + val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0 + + val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS) + val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0 + val wearExtenderBackground = + computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) + + val style = notification.notificationStyle + val hasCustomView = notification.contentView != null || notification.bigContentView != null + val extrasSize = computeBundleSize(extras) + + return NotificationObjectUsage( + smallIconUse, + largeIconUse, + extrasSize, + style?.simpleName, + bigPictureIconUse + + peopleUse + + callingPersonUse + + verificationIconUse + + messagesUse + + historyicMessagesUse, + bigPictureUse, + carExtenderSize + + carExtenderIcon + + tvExtenderSize + + wearExtenderSize + + wearExtenderBackground, + hasCustomView + ) + } + + /** + * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem + * bitmaps). Can be slow. + */ + private fun computeBundleSize(extras: Bundle): Int { + val parcel = Parcel.obtain() + try { + extras.writeToParcel(parcel, 0) + return parcel.dataSize() + } finally { + parcel.recycle() + } + } + + /** + * Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0 + * if the key does not exist in extras. + */ + private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int { + return when (val parcelable = extras?.getParcelable<Parcelable>(key)) { + is Bitmap -> computeBitmapUse(parcelable, seenBitmaps) + is Icon -> computeIconUse(parcelable, seenBitmaps) + is Person -> computeIconUse(parcelable.icon, seenBitmaps) + else -> 0 + } + } + + /** + * Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is + * defined via Uri or a resource. + * + * @return memory usage in bytes or 0 if the icon is Uri/Resource based + */ + private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) = + when (icon?.type) { + Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) + Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps) + else -> 0 + } + + /** + * Returns the amount of memory a given bitmap is using. If the bitmap reference is part of + * seenBitmaps set, this method returns 0 to avoid double counting. + * + * @return memory usage of the bitmap in bytes + */ + private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int { + val refId = System.identityHashCode(bitmap) + if (seenBitmaps?.contains(refId) == true) { + return 0 + } + + seenBitmaps?.add(refId) + return bitmap.allocationByteCount + } + + private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int { + val refId = System.identityHashCode(icon.dataBytes) + if (seenBitmaps.contains(refId)) { + return 0 + } + + seenBitmaps.add(refId) + return icon.dataLength + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java index 9faef1b43bc1..5ca13c95309f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java @@ -45,11 +45,21 @@ public interface NotificationPanelLogger { void logPanelShown(boolean isLockscreen, @Nullable List<NotificationEntry> visibleNotifications); + /** + * Log a NOTIFICATION_PANEL_REPORTED statsd event, with + * {@link NotificationPanelEvent#NOTIFICATION_DRAG} as the eventID. + * + * @param draggedNotification the notification that is being dragged + */ + void logNotificationDrag(NotificationEntry draggedNotification); + enum NotificationPanelEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "Notification panel shown from status bar.") NOTIFICATION_PANEL_OPEN_STATUS_BAR(200), @UiEvent(doc = "Notification panel shown from lockscreen.") - NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201); + NOTIFICATION_PANEL_OPEN_LOCKSCREEN(201), + @UiEvent(doc = "Notification was dragged") + NOTIFICATION_DRAG(1226); private final int mId; NotificationPanelEvent(int id) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java index 75a60194f2fa..9a632282ae16 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.notification.logging; +import static com.android.systemui.statusbar.notification.logging.NotificationPanelLogger.NotificationPanelEvent.NOTIFICATION_DRAG; + import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.nano.Notifications; import com.google.protobuf.nano.MessageNano; +import java.util.Collections; import java.util.List; /** @@ -38,4 +41,14 @@ public class NotificationPanelLoggerImpl implements NotificationPanelLogger { /* int num_notifications*/ proto.notifications.length, /* byte[] notifications*/ MessageNano.toByteArray(proto)); } + + @Override + public void logNotificationDrag(NotificationEntry draggedNotification) { + final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto( + Collections.singletonList(draggedNotification)); + SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED, + /* int event_id */ NOTIFICATION_DRAG.getId(), + /* int num_notifications*/ proto.notifications.length, + /* byte[] notifications*/ MessageNano.toByteArray(proto)); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java index 4939a9c22cf8..64f87cabaf74 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragController.java @@ -45,12 +45,17 @@ import android.widget.Toast; import androidx.annotation.VisibleForTesting; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.InstanceIdSequence; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; import com.android.systemui.statusbar.policy.HeadsUpManager; +import java.util.Collections; + import javax.inject.Inject; /** @@ -63,14 +68,17 @@ public class ExpandableNotificationRowDragController { private final Context mContext; private final HeadsUpManager mHeadsUpManager; private final ShadeController mShadeController; + private NotificationPanelLogger mNotificationPanelLogger; @Inject public ExpandableNotificationRowDragController(Context context, HeadsUpManager headsUpManager, - ShadeController shadeController) { + ShadeController shadeController, + NotificationPanelLogger notificationPanelLogger) { mContext = context; mHeadsUpManager = headsUpManager; mShadeController = shadeController; + mNotificationPanelLogger = notificationPanelLogger; init(); } @@ -120,12 +128,16 @@ public class ExpandableNotificationRowDragController { dragIntent.putExtra(ClipDescription.EXTRA_PENDING_INTENT, contentIntent); dragIntent.putExtra(Intent.EXTRA_USER, android.os.Process.myUserHandle()); ClipData.Item item = new ClipData.Item(dragIntent); + InstanceId instanceId = new InstanceIdSequence(Integer.MAX_VALUE).newInstanceId(); + item.getIntent().putExtra(ClipDescription.EXTRA_LOGGING_INSTANCE_ID, instanceId); ClipData dragData = new ClipData(clipDescription, item); View.DragShadowBuilder myShadow = new View.DragShadowBuilder(snapshot); view.setOnDragListener(getDraggedViewDragListener()); boolean result = view.startDragAndDrop(dragData, myShadow, null, View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_REQUEST_SURFACE_FOR_RETURN_ANIMATION); if (result) { + // Log notification drag only if it succeeds + mNotificationPanelLogger.logNotificationDrag(enr.getEntry()); view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); if (enr.isPinned()) { mHeadsUpManager.releaseAllImmediately(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 0026b71a5304..054bd28e003d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -40,6 +40,7 @@ import androidx.annotation.VisibleForTesting; import com.android.keyguard.CarrierTextController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.battery.BatteryMeterViewController; @@ -116,6 +117,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat private final CommandQueue mCommandQueue; private final Executor mMainExecutor; private final Object mLock = new Object(); + private final KeyguardLogger mLogger; private final ConfigurationController.ConfigurationListener mConfigurationListener = new ConfigurationController.ConfigurationListener() { @@ -279,7 +281,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat StatusBarUserInfoTracker statusBarUserInfoTracker, SecureSettings secureSettings, CommandQueue commandQueue, - @Main Executor mainExecutor + @Main Executor mainExecutor, + KeyguardLogger logger ) { super(view); mCarrierTextController = carrierTextController; @@ -304,6 +307,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat mSecureSettings = secureSettings; mCommandQueue = commandQueue; mMainExecutor = mainExecutor; + mLogger = logger; mFirstBypassAttempt = mKeyguardBypassController.getBypassEnabled(); mKeyguardStateController.addCallback( @@ -430,6 +434,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar in. */ public void animateKeyguardStatusBarIn() { + mLogger.d("animating status bar in"); if (mDisableStateTracker.isDisabled()) { // If our view is disabled, don't allow us to animate in. return; @@ -445,6 +450,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar out. */ public void animateKeyguardStatusBarOut(long startDelay, long duration) { + mLogger.d("animating status bar out"); ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f); anim.addUpdateListener(mAnimatorUpdateListener); anim.setStartDelay(startDelay); @@ -481,6 +487,9 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat newAlpha = Math.min(getKeyguardContentsAlpha(), alphaQsExpansion) * mKeyguardStatusBarAnimateAlpha * (1.0f - mKeyguardHeadsUpShowingAmount); + if (newAlpha != mView.getAlpha() && (newAlpha == 0 || newAlpha == 1)) { + mLogger.logStatusBarCalculatedAlpha(newAlpha); + } } boolean hideForBypass = @@ -503,6 +512,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat if (mDisableStateTracker.isDisabled()) { visibility = View.INVISIBLE; } + if (visibility != mView.getVisibility()) { + mLogger.logStatusBarAlphaVisibility(visibility, alpha, + StatusBarState.toString(mStatusBarState)); + } mView.setAlpha(alpha); mView.setVisibility(visibility); } @@ -596,6 +609,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat pw.println("KeyguardStatusBarView:"); pw.println(" mBatteryListening: " + mBatteryListening); pw.println(" mExplicitAlpha: " + mExplicitAlpha); + pw.println(" alpha: " + mView.getAlpha()); + pw.println(" visibility: " + mView.getVisibility()); mView.dump(pw, args); } @@ -605,6 +620,10 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat * @param alpha a value between 0 and 1. -1 if the value is to be reset/ignored. */ public void setAlpha(float alpha) { + if (mExplicitAlpha != alpha && (mExplicitAlpha == -1 || alpha == -1)) { + // logged if value changed to ignored or from ignored + mLogger.logStatusBarExplicitAlpha(alpha); + } mExplicitAlpha = alpha; updateViewState(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java index 0995a00533a8..712953e14d60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java @@ -505,7 +505,7 @@ public class KeyguardUserSwitcherController extends ViewController<KeyguardUserS v.bind(name, drawable, item.info.id); } v.setActivated(item.isCurrent); - v.setDisabledByAdmin(getController().isDisabledByAdmin(item)); + v.setDisabledByAdmin(item.isDisabledByAdmin()); v.setEnabled(item.isSwitchToEnabled); UserSwitcherController.setSelectableAlpha(v); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt index 843c2329092c..146b222c94ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.policy import android.annotation.UserIdInt import android.content.Intent import android.view.View -import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin import com.android.systemui.Dumpable import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower import com.android.systemui.user.data.source.UserRecord @@ -130,12 +129,6 @@ interface UserSwitcherController : Dumpable { /** Whether keyguard is showing. */ val isKeyguardShowing: Boolean - /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */ - fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin? - - /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */ - fun isDisabledByAdmin(record: UserRecord): Boolean - /** Starts an activity with the given [Intent]. */ fun startActivity(intent: Intent) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt index 12834f68c3b7..16926566105c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt @@ -17,13 +17,21 @@ package com.android.systemui.statusbar.policy +import android.content.Context import android.content.Intent import android.view.View -import com.android.settingslib.RestrictedLockUtils +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper +import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import dagger.Lazy import java.io.PrintWriter import java.lang.ref.WeakReference @@ -31,58 +39,76 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow /** Implementation of [UserSwitcherController]. */ +@SysUISingleton class UserSwitcherControllerImpl @Inject constructor( - private val flags: FeatureFlags, + @Application private val applicationContext: Context, + flags: FeatureFlags, @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>, + private val userInteractorLazy: Lazy<UserInteractor>, + private val guestUserInteractorLazy: Lazy<GuestUserInteractor>, + private val keyguardInteractorLazy: Lazy<KeyguardInteractor>, + private val activityStarter: ActivityStarter, ) : UserSwitcherController { - private val isNewImpl: Boolean - get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER) + private val useInteractor: Boolean = + flags.isEnabled(Flags.USER_CONTROLLER_USES_INTERACTOR) && + !flags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) private val _oldImpl: UserSwitcherControllerOldImpl get() = oldImpl.get() + private val userInteractor: UserInteractor by lazy { userInteractorLazy.get() } + private val guestUserInteractor: GuestUserInteractor by lazy { guestUserInteractorLazy.get() } + private val keyguardInteractor: KeyguardInteractor by lazy { keyguardInteractorLazy.get() } - private fun notYetImplemented(): Nothing { - error("Not yet implemented!") + private val callbackCompatMap = + mutableMapOf<UserSwitcherController.UserSwitchCallback, UserInteractor.UserCallback>() + + private fun notSupported(): Nothing { + error("Not supported in the new implementation!") } override val users: ArrayList<UserRecord> get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.userRecords.value } else { _oldImpl.users } override val isSimpleUserSwitcher: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.isSimpleUserSwitcher } else { _oldImpl.isSimpleUserSwitcher } override fun init(view: View) { - if (isNewImpl) { - notYetImplemented() - } else { + if (!useInteractor) { _oldImpl.init(view) } } override val currentUserRecord: UserRecord? get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.selectedUserRecord.value } else { _oldImpl.currentUserRecord } override val currentUserName: String? get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + currentUserRecord?.let { + LegacyUserUiHelper.getUserRecordName( + context = applicationContext, + record = it, + isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, + isGuestUserResetting = userInteractor.isGuestUserResetting, + ) + } } else { _oldImpl.currentUserName } @@ -91,8 +117,8 @@ constructor( userId: Int, dialogShower: UserSwitchDialogController.DialogShower? ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.selectUser(userId) } else { _oldImpl.onUserSelected(userId, dialogShower) } @@ -100,24 +126,24 @@ constructor( override val isAddUsersFromLockScreenEnabled: Flow<Boolean> get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.isAddUsersFromLockScreenEnabled } override val isGuestUserAutoCreated: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.isGuestUserAutoCreated } else { _oldImpl.isGuestUserAutoCreated } override val isGuestUserResetting: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.isGuestUserResetting } else { _oldImpl.isGuestUserResetting } @@ -125,40 +151,48 @@ constructor( override fun createAndSwitchToGuestUser( dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.createAndSwitchToGuestUser(dialogShower) } } override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.showAddUserDialog(dialogShower) } } override fun startSupervisedUserActivity() { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + notSupported() } else { _oldImpl.startSupervisedUserActivity() } } override fun onDensityOrFontScaleChanged() { - if (isNewImpl) { - notYetImplemented() - } else { + if (!useInteractor) { _oldImpl.onDensityOrFontScaleChanged() } } override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.addCallback( + object : UserInteractor.UserCallback { + override fun isEvictable(): Boolean { + return adapter.get() == null + } + + override fun onUserStateChanged() { + adapter.get()?.notifyDataSetChanged() + } + } + ) } else { _oldImpl.addAdapter(adapter) } @@ -168,16 +202,23 @@ constructor( record: UserRecord, dialogShower: UserSwitchDialogController.DialogShower?, ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + if (LegacyUserDataHelper.isUser(record)) { + userInteractor.selectUser(record.resolveId()) + } else { + userInteractor.executeAction(LegacyUserDataHelper.toUserActionModel(record)) + } } else { _oldImpl.onUserListItemClicked(record, dialogShower) } } override fun removeGuestUser(guestUserId: Int, targetUserId: Int) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.removeGuestUser( + guestUserId = guestUserId, + targetUserId = targetUserId, + ) } else { _oldImpl.removeGuestUser(guestUserId, targetUserId) } @@ -188,16 +229,16 @@ constructor( targetUserId: Int, forceRemoveGuestOnExit: Boolean ) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) } else { _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit) } } override fun schedulePostBootGuestCreation() { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + guestUserInteractor.onDeviceBootCompleted() } else { _oldImpl.schedulePostBootGuestCreation() } @@ -205,63 +246,57 @@ constructor( override val isKeyguardShowing: Boolean get() = - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + keyguardInteractor.isKeyguardShowing() } else { _oldImpl.isKeyguardShowing } - override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? { - return if (isNewImpl) { - notYetImplemented() - } else { - _oldImpl.getEnforcedAdmin(record) - } - } - - override fun isDisabledByAdmin(record: UserRecord): Boolean { - return if (isNewImpl) { - notYetImplemented() - } else { - _oldImpl.isDisabledByAdmin(record) - } - } - override fun startActivity(intent: Intent) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + activityStarter.startActivity(intent, /* dismissShade= */ false) } else { _oldImpl.startActivity(intent) } } override fun refreshUsers(forcePictureLoadForId: Int) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.refreshUsers() } else { _oldImpl.refreshUsers(forcePictureLoadForId) } } override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + val interactorCallback = + object : UserInteractor.UserCallback { + override fun onUserStateChanged() { + callback.onUserSwitched() + } + } + callbackCompatMap[callback] = interactorCallback + userInteractor.addCallback(interactorCallback) } else { _oldImpl.addUserSwitchCallback(callback) } } override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + val interactorCallback = callbackCompatMap.remove(callback) + if (interactorCallback != null) { + userInteractor.removeCallback(interactorCallback) + } } else { _oldImpl.removeUserSwitchCallback(callback) } } override fun dump(pw: PrintWriter, args: Array<out String>) { - if (isNewImpl) { - notYetImplemented() + if (useInteractor) { + userInteractor.dump(pw) } else { _oldImpl.dump(pw, args) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java index d365aa6f952d..46d2f3ac9ce4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java @@ -17,17 +17,13 @@ package com.android.systemui.statusbar.policy; import static android.os.UserManager.SWITCHABILITY_STATUS_OK; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; - import android.annotation.UserIdInt; -import android.app.ActivityManager; import android.app.AlertDialog; import android.app.Dialog; import android.app.IActivityManager; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.UserInfo; @@ -40,7 +36,6 @@ import android.os.UserManager; import android.provider.Settings; import android.telephony.TelephonyCallback; import android.text.TextUtils; -import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -49,17 +44,14 @@ import android.view.WindowManagerGlobal; import android.widget.Toast; import androidx.annotation.Nullable; -import androidx.collection.SimpleArrayMap; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.LatencyTracker; -import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.users.UserCreatingDialog; import com.android.systemui.GuestResetOrExitSessionReceiver; import com.android.systemui.GuestResumeSessionReceiver; -import com.android.systemui.R; import com.android.systemui.SystemUISecondaryUserService; import com.android.systemui.animation.DialogCuj; import com.android.systemui.animation.DialogLaunchAnimator; @@ -75,10 +67,12 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.telephony.TelephonyListenerManager; -import com.android.systemui.user.CreateUserActivity; import com.android.systemui.user.data.source.UserRecord; +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper; +import com.android.systemui.user.shared.model.UserActionModel; +import com.android.systemui.user.ui.dialog.AddUserDialog; +import com.android.systemui.user.ui.dialog.ExitGuestDialog; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -139,9 +133,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { private final InteractionJankMonitor mInteractionJankMonitor; private final LatencyTracker mLatencyTracker; private final DialogLaunchAnimator mDialogLaunchAnimator; - private final SimpleArrayMap<UserRecord, EnforcedAdmin> mEnforcedAdminByUserRecord = - new SimpleArrayMap<>(); - private final ArraySet<UserRecord> mDisabledByAdmin = new ArraySet<>(); private ArrayList<UserRecord> mUsers = new ArrayList<>(); @VisibleForTesting @@ -334,7 +325,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { for (UserInfo info : infos) { boolean isCurrent = currentId == info.id; - boolean switchToEnabled = canSwitchUsers || isCurrent; if (!mUserSwitcherEnabled && !info.isPrimary()) { continue; } @@ -343,25 +333,22 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (info.isGuest()) { // Tapping guest icon triggers remove and a user switch therefore // the icon shouldn't be enabled even if the user is current - guestRecord = new UserRecord(info, null /* picture */, - true /* isGuest */, isCurrent, false /* isAddUser */, - false /* isRestricted */, canSwitchUsers, - false /* isAddSupervisedUser */); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + null /* picture */, + info, + isCurrent, + canSwitchUsers); } else if (info.supportsSwitchToByUser()) { - Bitmap picture = bitmaps.get(info.id); - if (picture == null) { - picture = mUserManager.getUserIcon(info.id); - - if (picture != null) { - int avatarSize = mContext.getResources() - .getDimensionPixelSize(R.dimen.max_avatar_size); - picture = Bitmap.createScaledBitmap( - picture, avatarSize, avatarSize, true); - } - } - records.add(new UserRecord(info, picture, false /* isGuest */, - isCurrent, false /* isAddUser */, false /* isRestricted */, - switchToEnabled, false /* isAddSupervisedUser */)); + records.add( + LegacyUserDataHelper.createRecord( + mContext, + mUserManager, + bitmaps.get(info.id), + info, + isCurrent, + canSwitchUsers)); } } } @@ -372,18 +359,20 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { // we will just use it as an indicator for "Resetting guest...". // Otherwise, default to canSwitchUsers. boolean isSwitchToGuestEnabled = !mGuestIsResetting.get() && canSwitchUsers; - guestRecord = new UserRecord(null /* info */, null /* picture */, - true /* isGuest */, false /* isCurrent */, - false /* isAddUser */, false /* isRestricted */, - isSwitchToGuestEnabled, false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, + false /* isRestricted */, + isSwitchToGuestEnabled); records.add(guestRecord); } else if (canCreateGuest(guestRecord != null)) { - guestRecord = new UserRecord(null /* info */, null /* picture */, - true /* isGuest */, false /* isCurrent */, - false /* isAddUser */, createIsRestricted(), canSwitchUsers, - false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(guestRecord); + guestRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ENTER_GUEST_MODE, + false /* isRestricted */, + canSwitchUsers); records.add(guestRecord); } } else { @@ -391,20 +380,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } if (canCreateUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, true /* isAddUser */, - createIsRestricted(), canSwitchUsers, - false /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); - records.add(addUserRecord); + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_USER, + createIsRestricted(), + canSwitchUsers); + records.add(userRecord); } if (canCreateSupervisedUser()) { - UserRecord addUserRecord = new UserRecord(null /* info */, null /* picture */, - false /* isGuest */, false /* isCurrent */, false /* isAddUser */, - createIsRestricted(), canSwitchUsers, true /* isAddSupervisedUser */); - checkIfAddUserDisallowedByAdminOnly(addUserRecord); - records.add(addUserRecord); + final UserRecord userRecord = LegacyUserDataHelper.createRecord( + mContext, + currentId, + UserActionModel.ADD_SUPERVISED_USER, + createIsRestricted(), + canSwitchUsers); + records.add(userRecord); } mUiExecutor.execute(() -> { @@ -591,12 +583,23 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { showExitGuestDialog(id, isGuestEphemeral, newId, dialogShower); } - private void showExitGuestDialog(int id, boolean isGuestEphemeral, - int targetId, DialogShower dialogShower) { + private void showExitGuestDialog( + int id, + boolean isGuestEphemeral, + int targetId, + DialogShower dialogShower) { if (mExitGuestDialog != null && mExitGuestDialog.isShowing()) { mExitGuestDialog.cancel(); } - mExitGuestDialog = new ExitGuestDialog(mContext, id, isGuestEphemeral, targetId); + mExitGuestDialog = new ExitGuestDialog( + mContext, + id, + isGuestEphemeral, + targetId, + mKeyguardStateController.isShowing(), + mFalsingManager, + mDialogLaunchAnimator, + this::exitGuestUser); if (dialogShower != null) { dialogShower.showDialog(mExitGuestDialog, new DialogCuj( InteractionJankMonitor.CUJ_USER_DIALOG_OPEN, @@ -622,7 +625,15 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { if (mAddUserDialog != null && mAddUserDialog.isShowing()) { mAddUserDialog.cancel(); } - mAddUserDialog = new AddUserDialog(mContext); + final UserInfo currentUser = mUserTracker.getUserInfo(); + mAddUserDialog = new AddUserDialog( + mContext, + currentUser.getUserHandle(), + mKeyguardStateController.isShowing(), + /* showEphemeralMessage= */currentUser.isGuest() && currentUser.isEphemeral(), + mFalsingManager, + mBroadcastSender, + mDialogLaunchAnimator); if (dialogShower != null) { dialogShower.showDialog(mAddUserDialog, new DialogCuj( @@ -964,30 +975,6 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { return mKeyguardStateController.isShowing(); } - @Override - @Nullable - public EnforcedAdmin getEnforcedAdmin(UserRecord record) { - return mEnforcedAdminByUserRecord.get(record); - } - - @Override - public boolean isDisabledByAdmin(UserRecord record) { - return mDisabledByAdmin.contains(record); - } - - private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) { - EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId()); - if (admin != null && !RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, - UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId())) { - mDisabledByAdmin.add(record); - mEnforcedAdminByUserRecord.put(record, admin); - } else { - mDisabledByAdmin.remove(record); - mEnforcedAdminByUserRecord.put(record, null); - } - } - private boolean shouldUseSimpleUserSwitcher() { int defaultSimpleUserSwitcher = mContext.getResources().getBoolean( com.android.internal.R.bool.config_expandLockScreenUserSwitcher) ? 1 : 0; @@ -1052,133 +1039,4 @@ public class UserSwitcherControllerOldImpl implements UserSwitcherController { } } }; - - - private final class ExitGuestDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - private final int mGuestId; - private final int mTargetId; - private final boolean mIsGuestEphemeral; - - ExitGuestDialog(Context context, int guestId, boolean isGuestEphemeral, - int targetId) { - super(context); - if (isGuestEphemeral) { - setTitle(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_title)); - setMessage(context.getString( - com.android.settingslib.R.string.guest_exit_dialog_message)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_dialog_button), this); - } else { - setTitle(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_title_non_ephemeral)); - setMessage(context.getString( - com.android.settingslib - .R.string.guest_exit_dialog_message_non_ephemeral)); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_NEGATIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_clear_data_button), - this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString( - com.android.settingslib.R.string.guest_exit_save_data_button), - this); - } - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - setCanceledOnTouchOutside(false); - mGuestId = guestId; - mTargetId = targetId; - mIsGuestEphemeral = isGuestEphemeral; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.HIGH_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (mIsGuestEphemeral) { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Ephemeral guest: exit guest, guest is removed by the system - // on exit, since its marked ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - // Cancel clicked, do nothing - cancel(); - } - } else { - if (which == DialogInterface.BUTTON_POSITIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: exit guest, guest is not removed by the system - // on exit, since its marked non-ephemeral - exitGuestUser(mGuestId, mTargetId, false); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - mDialogLaunchAnimator.dismissStack(this); - // Non-ephemeral guest: remove guest and then exit - exitGuestUser(mGuestId, mTargetId, true); - } else if (which == DialogInterface.BUTTON_NEUTRAL) { - // Cancel clicked, do nothing - cancel(); - } - } - } - } - - @VisibleForTesting - final class AddUserDialog extends SystemUIDialog implements - DialogInterface.OnClickListener { - - AddUserDialog(Context context) { - super(context); - - setTitle(com.android.settingslib.R.string.user_add_user_title); - String message = context.getString( - com.android.settingslib.R.string.user_add_user_message_short); - UserInfo currentUser = mUserTracker.getUserInfo(); - if (currentUser != null && currentUser.isGuest() && currentUser.isEphemeral()) { - message += context.getString(R.string.user_add_user_message_guest_remove); - } - setMessage(message); - setButton(DialogInterface.BUTTON_NEUTRAL, - context.getString(android.R.string.cancel), this); - setButton(DialogInterface.BUTTON_POSITIVE, - context.getString(android.R.string.ok), this); - SystemUIDialog.setWindowOnTop(this, mKeyguardStateController.isShowing()); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - int penalty = which == BUTTON_NEGATIVE ? FalsingManager.NO_PENALTY - : FalsingManager.MODERATE_PENALTY; - if (mFalsingManager.isFalseTap(penalty)) { - return; - } - if (which == BUTTON_NEUTRAL) { - cancel(); - } else { - mDialogLaunchAnimator.dismissStack(this); - if (ActivityManager.isUserAMonkey()) { - return; - } - // Use broadcast instead of ShadeController, as this dialog may have started in - // another process and normal dagger bindings are not available - mBroadcastSender.sendBroadcastAsUser( - new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), UserHandle.CURRENT); - getContext().startActivityAsUser( - CreateUserActivity.createIntentForStart(getContext()), - mUserTracker.getUserHandle()); - } - } - } - } diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt new file mode 100644 index 000000000000..9c38dc0f8852 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import android.telephony.Annotation +import android.telephony.TelephonyCallback +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.TelephonyListenerManager +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Defines interface for classes that encapsulate _some_ telephony-related state. */ +interface TelephonyRepository { + /** The state of the current call. */ + @Annotation.CallState val callState: Flow<Int> +} + +/** + * NOTE: This repository tracks only telephony-related state regarding the default mobile + * subscription. `TelephonyListenerManager` does not create new instances of `TelephonyManager` on a + * per-subscription basis and thus will always be tracking telephony information regarding + * `SubscriptionManager.getDefaultSubscriptionId`. See `TelephonyManager` and `SubscriptionManager` + * for more documentation. + */ +@SysUISingleton +class TelephonyRepositoryImpl +@Inject +constructor( + private val manager: TelephonyListenerManager, +) : TelephonyRepository { + @Annotation.CallState + override val callState: Flow<Int> = conflatedCallbackFlow { + val listener = TelephonyCallback.CallStateListener { state -> trySend(state) } + + manager.addCallStateListener(listener) + + awaitClose { manager.removeCallStateListener(listener) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt new file mode 100644 index 000000000000..630fbf2d1a07 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface TelephonyRepositoryModule { + @Binds fun repository(impl: TelephonyRepositoryImpl): TelephonyRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt new file mode 100644 index 000000000000..86ca33df24dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/telephony/domain/interactor/TelephonyInteractor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.domain.interactor + +import android.telephony.Annotation +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.telephony.data.repository.TelephonyRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +/** Hosts business logic related to telephony. */ +@SysUISingleton +class TelephonyInteractor +@Inject +constructor( + repository: TelephonyRepository, +) { + @Annotation.CallState val callState: Flow<Int> = repository.callState +} diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 5b522dcc4885..0c72b78a3c46 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -20,6 +20,7 @@ import android.app.Activity; import com.android.settingslib.users.EditUserInfoController; import com.android.systemui.user.data.repository.UserRepositoryModule; +import com.android.systemui.user.ui.dialog.UserDialogModule; import dagger.Binds; import dagger.Module; @@ -32,6 +33,7 @@ import dagger.multibindings.IntoMap; */ @Module( includes = { + UserDialogModule.class, UserRepositoryModule.class, } ) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt new file mode 100644 index 000000000000..4fd55c0e21c8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/data/model/UserSwitcherSettingsModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.data.model + +/** Encapsulates the state of settings related to user switching. */ +data class UserSwitcherSettingsModel( + val isSimpleUserSwitcher: Boolean = false, + val isAddUsersFromLockscreen: Boolean = false, + val isUserSwitcherEnabled: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 035638800f9c..3014f39c17f8 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -18,9 +18,13 @@ package com.android.systemui.user.data.repository import android.content.Context +import android.content.pm.UserInfo import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable +import android.os.UserHandle import android.os.UserManager +import android.provider.Settings +import androidx.annotation.VisibleForTesting import androidx.appcompat.content.res.AppCompatResources import com.android.internal.util.UserIcons import com.android.systemui.R @@ -29,15 +33,36 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.source.UserRecord import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Acts as source of truth for user related data. @@ -55,6 +80,18 @@ interface UserRepository { /** List of available user-related actions. */ val actions: Flow<List<UserActionModel>> + /** User switcher related settings. */ + val userSwitcherSettings: Flow<UserSwitcherSettingsModel> + + /** List of all users on the device. */ + val userInfos: Flow<List<UserInfo>> + + /** [UserInfo] of the currently-selected user. */ + val selectedUserInfo: Flow<UserInfo> + + /** User ID of the last non-guest selected user. */ + val lastSelectedNonGuestUserId: Int + /** Whether actions are available even when locked. */ val isActionableWhenLocked: Flow<Boolean> @@ -62,7 +99,23 @@ interface UserRepository { val isGuestUserAutoCreated: Boolean /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean + var isGuestUserResetting: Boolean + + /** Whether we've scheduled the creation of a guest user. */ + val isGuestUserCreationScheduled: AtomicBoolean + + /** The user of the secondary service. */ + var secondaryUserId: Int + + /** Whether refresh users should be paused. */ + var isRefreshUsersPaused: Boolean + + /** Asynchronously refresh the list of users. This will cause [userInfos] to be updated. */ + fun refreshUsers() + + fun getSelectedUserInfo(): UserInfo + + fun isSimpleUserSwitcher(): Boolean } @SysUISingleton @@ -71,9 +124,31 @@ class UserRepositoryImpl constructor( @Application private val appContext: Context, private val manager: UserManager, - controller: UserSwitcherController, + private val controller: UserSwitcherController, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val globalSettings: GlobalSettings, + private val tracker: UserTracker, + private val featureFlags: FeatureFlags, ) : UserRepository { + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + + private val _userSwitcherSettings = MutableStateFlow<UserSwitcherSettingsModel?>(null) + override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = + _userSwitcherSettings.asStateFlow().filterNotNull() + + private val _userInfos = MutableStateFlow<List<UserInfo>?>(null) + override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull() + + private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) + override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private set + private val userRecords: Flow<List<UserRecord>> = conflatedCallbackFlow { fun send() { trySendWithFailureLogging( @@ -99,11 +174,148 @@ constructor( override val actions: Flow<List<UserActionModel>> = userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } } - override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled + override val isActionableWhenLocked: Flow<Boolean> = + if (isNewImpl) { + emptyFlow() + } else { + controller.isAddUsersFromLockScreenEnabled + } + + override val isGuestUserAutoCreated: Boolean = + if (isNewImpl) { + appContext.resources.getBoolean(com.android.internal.R.bool.config_guestUserAutoCreated) + } else { + controller.isGuestUserAutoCreated + } + + private var _isGuestUserResetting: Boolean = false + override var isGuestUserResetting: Boolean = + if (isNewImpl) { + _isGuestUserResetting + } else { + controller.isGuestUserResetting + } + set(value) = + if (isNewImpl) { + _isGuestUserResetting = value + } else { + error("Not supported in the old implementation!") + } + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL - override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated + override var isRefreshUsersPaused: Boolean = false - override val isGuestUserResetting: Boolean = controller.isGuestUserResetting + init { + if (isNewImpl) { + observeSelectedUser() + observeUserSettings() + } + } + + override fun refreshUsers() { + applicationScope.launch { + val result = withContext(backgroundDispatcher) { manager.aliveUsers } + + if (result != null) { + _userInfos.value = result + } + } + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return checkNotNull(_userSwitcherSettings.value?.isSimpleUserSwitcher) + } + + private fun observeSelectedUser() { + conflatedCallbackFlow { + fun send() { + trySendWithFailureLogging(tracker.userInfo, TAG) + } + + val callback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + send() + } + } + + tracker.addCallback(callback, mainDispatcher.asExecutor()) + send() + + awaitClose { tracker.removeCallback(callback) } + } + .onEach { + if (!it.isGuest) { + lastSelectedNonGuestUserId = it.id + } + + _selectedUserInfo.value = it + } + .launchIn(applicationScope) + } + + private fun observeUserSettings() { + globalSettings + .observerFlow( + names = + arrayOf( + SETTING_SIMPLE_USER_SWITCHER, + Settings.Global.ADD_USERS_WHEN_LOCKED, + Settings.Global.USER_SWITCHER_ENABLED, + ), + userId = UserHandle.USER_SYSTEM, + ) + .onStart { emit(Unit) } // Forces an initial update. + .map { getSettings() } + .onEach { _userSwitcherSettings.value = it } + .launchIn(applicationScope) + } + + private suspend fun getSettings(): UserSwitcherSettingsModel { + return withContext(backgroundDispatcher) { + val isSimpleUserSwitcher = + globalSettings.getIntForUser( + SETTING_SIMPLE_USER_SWITCHER, + if ( + appContext.resources.getBoolean( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher + ) + ) { + 1 + } else { + 0 + }, + UserHandle.USER_SYSTEM, + ) != 0 + + val isAddUsersFromLockscreen = + globalSettings.getIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + val isUserSwitcherEnabled = + globalSettings.getIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + 0, + UserHandle.USER_SYSTEM, + ) != 0 + + UserSwitcherSettingsModel( + isSimpleUserSwitcher = isSimpleUserSwitcher, + isAddUsersFromLockscreen = isAddUsersFromLockscreen, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } private fun UserRecord.isUser(): Boolean { return when { @@ -125,6 +337,7 @@ constructor( image = getUserImage(this), isSelected = isCurrent, isSelectable = isSwitchToEnabled || isGuest, + isGuest = isGuest, ) } @@ -162,5 +375,6 @@ constructor( companion object { private const val TAG = "UserRepository" + @VisibleForTesting const val SETTING_SIMPLE_USER_SWITCHER = "lockscreenSimpleUserSwitcher" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt index cf6da9a60d78..9370286d7ee7 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/source/UserRecord.kt @@ -19,6 +19,7 @@ package com.android.systemui.user.data.source import android.content.pm.UserInfo import android.graphics.Bitmap import android.os.UserHandle +import com.android.settingslib.RestrictedLockUtils /** Encapsulates raw data for a user or an option item related to managing users on the device. */ data class UserRecord( @@ -41,6 +42,11 @@ data class UserRecord( @JvmField val isSwitchToEnabled: Boolean = false, /** Whether this record represents an option to add another supervised user to the device. */ @JvmField val isAddSupervisedUser: Boolean = false, + /** + * An enforcing admin, if the user action represented by this record is disabled by the admin. + * If not disabled, this is `null`. + */ + @JvmField val enforcedAdmin: RestrictedLockUtils.EnforcedAdmin? = null, ) { /** Returns a new instance of [UserRecord] with its [isCurrent] set to the given value. */ fun copyWithIsCurrent(isCurrent: Boolean): UserRecord { @@ -59,6 +65,14 @@ data class UserRecord( } } + /** + * Returns `true` if the user action represented by this record has been disabled by an admin; + * `false` otherwise. + */ + fun isDisabledByAdmin(): Boolean { + return enforcedAdmin != null + } + companion object { @JvmStatic fun createForGuest(): UserRecord { diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt new file mode 100644 index 000000000000..07e5cf9d9df2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.annotation.UserIdInt +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.pm.UserInfo +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import android.view.WindowManagerGlobal +import android.widget.Toast +import com.android.internal.logging.UiEventLogger +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.QSUserSwitcherEvent +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext + +/** Encapsulates business logic to interact with guest user data and systems. */ +@SysUISingleton +class GuestUserInteractor +@Inject +constructor( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val manager: UserManager, + private val repository: UserRepository, + private val deviceProvisionedController: DeviceProvisionedController, + private val devicePolicyManager: DevicePolicyManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val uiEventLogger: UiEventLogger, +) { + /** Whether the device is configured to always have a guest user available. */ + val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + + /** Whether the guest user is currently being reset. */ + val isGuestUserResetting: Boolean = repository.isGuestUserResetting + + /** Notifies that the device has finished booting. */ + fun onDeviceBootCompleted() { + applicationScope.launch { + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + return@launch + } + + suspendCancellableCoroutine<Unit> { continuation -> + val callback = + object : DeviceProvisionedController.DeviceProvisionedListener { + override fun onDeviceProvisionedChanged() { + continuation.resumeWith(Result.success(Unit)) + deviceProvisionedController.removeCallback(this) + } + } + + deviceProvisionedController.addCallback(callback) + } + + if (isDeviceAllowedToAddGuest()) { + guaranteePresent() + } + } + } + + /** Creates a guest user and switches to it. */ + fun createAndSwitchTo( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + selectUser: (userId: Int) -> Unit, + ) { + applicationScope.launch { + val newGuestUserId = create(showDialog, dismissDialog) + if (newGuestUserId != UserHandle.USER_NULL) { + selectUser(newGuestUserId) + } + } + } + + /** Exits the guest user, switching back to the last non-guest user or to the default user. */ + fun exit( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUserInfo = repository.getSelectedUserInfo() + if (currentUserInfo.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " (${currentUserInfo.id})" + ) + return + } + + if (!currentUserInfo.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + applicationScope.launch { + var newUserId = UserHandle.USER_SYSTEM + if (targetUserId == UserHandle.USER_NULL) { + // When a target user is not specified switch to last non guest user: + val lastSelectedNonGuestUserHandle = repository.lastSelectedNonGuestUserId + if (lastSelectedNonGuestUserHandle != UserHandle.USER_SYSTEM) { + val info = + withContext(backgroundDispatcher) { + manager.getUserInfo(lastSelectedNonGuestUserHandle) + } + if (info != null && info.isEnabled && info.supportsSwitchToByUser()) { + newUserId = info.id + } + } + } else { + newUserId = targetUserId + } + + if (currentUserInfo.isEphemeral || forceRemoveGuestOnExit) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_REMOVE) + remove(currentUserInfo.id, newUserId, showDialog, dismissDialog, switchUser) + } else { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_SWITCH) + switchUser(newUserId) + } + } + } + + /** + * Guarantees that the guest user is present on the device, creating it if needed and if allowed + * to. + */ + suspend fun guaranteePresent() { + if (!isDeviceAllowedToAddGuest()) { + return + } + + val guestUser = withContext(backgroundDispatcher) { manager.findCurrentGuestUser() } + if (guestUser == null) { + scheduleCreation() + } + } + + /** Removes the guest user from the device. */ + suspend fun remove( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + switchUser: (userId: Int) -> Unit, + ) { + val currentUser: UserInfo = repository.getSelectedUserInfo() + if (currentUser.id != guestUserId) { + Log.w( + TAG, + "User requesting to start a new session ($guestUserId) is not current user" + + " ($currentUser.id)" + ) + return + } + + if (!currentUser.isGuest) { + Log.w(TAG, "User requesting to start a new session ($guestUserId) is not a guest") + return + } + + val marked = + withContext(backgroundDispatcher) { manager.markGuestForDeletion(currentUser.id) } + if (!marked) { + Log.w(TAG, "Couldn't mark the guest for deletion for user $guestUserId") + return + } + + if (targetUserId == UserHandle.USER_NULL) { + // Create a new guest in the foreground, and then immediately switch to it + val newGuestId = create(showDialog, dismissDialog) + if (newGuestId == UserHandle.USER_NULL) { + Log.e(TAG, "Could not create new guest, switching back to system user") + switchUser(UserHandle.USER_SYSTEM) + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + try { + WindowManagerGlobal.getWindowManagerService().lockNow(/* options= */ null) + } catch (e: RemoteException) { + Log.e( + TAG, + "Couldn't remove guest because ActivityManager or WindowManager is dead" + ) + } + return + } + + switchUser(newGuestId) + + withContext(backgroundDispatcher) { manager.removeUser(currentUser.id) } + } else { + if (repository.isGuestUserAutoCreated) { + repository.isGuestUserResetting = true + } + switchUser(targetUserId) + manager.removeUser(currentUser.id) + } + } + + /** + * Creates the guest user and adds it to the device. + * + * @param showDialog A function to invoke to show a dialog. + * @param dismissDialog A function to invoke to dismiss a dialog. + * @return The user ID of the newly-created guest user. + */ + private suspend fun create( + showDialog: (ShowDialogRequestModel) -> Unit, + dismissDialog: () -> Unit, + ): Int { + return withContext(mainDispatcher) { + showDialog(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + val guestUserId = createInBackground() + dismissDialog() + if (guestUserId != UserHandle.USER_NULL) { + uiEventLogger.log(QSUserSwitcherEvent.QS_USER_GUEST_ADD) + } else { + Toast.makeText( + applicationContext, + com.android.settingslib.R.string.add_guest_failed, + Toast.LENGTH_SHORT, + ) + .show() + } + + guestUserId + } + } + + /** Schedules the creation of the guest user. */ + private suspend fun scheduleCreation() { + if (!repository.isGuestUserCreationScheduled.compareAndSet(false, true)) { + return + } + + withContext(backgroundDispatcher) { + val newGuestUserId = createInBackground() + repository.isGuestUserCreationScheduled.set(false) + repository.isGuestUserResetting = false + if (newGuestUserId == UserHandle.USER_NULL) { + Log.w(TAG, "Could not create new guest while exiting existing guest") + // Refresh users so that we still display "Guest" if + // config_guestUserAutoCreated=true + refreshUsersScheduler.refreshIfNotPaused() + } + } + } + + /** + * Creates a guest user and return its multi-user user ID. + * + * This method does not check if a guest already exists before it makes a call to [UserManager] + * to create a new one. + * + * @return The multi-user user ID of the newly created guest user, or [UserHandle.USER_NULL] if + * the guest couldn't be created. + */ + @UserIdInt + private suspend fun createInBackground(): Int { + return withContext(backgroundDispatcher) { + try { + val guestUser = manager.createGuest(applicationContext) + if (guestUser != null) { + guestUser.id + } else { + Log.e( + TAG, + "Couldn't create guest, most likely because there already exists one!" + ) + UserHandle.USER_NULL + } + } catch (e: UserManager.UserOperationException) { + Log.e(TAG, "Couldn't create guest user!", e) + UserHandle.USER_NULL + } + } + } + + private fun isDeviceAllowedToAddGuest(): Boolean { + return deviceProvisionedController.isDeviceProvisioned && + !devicePolicyManager.isDeviceManaged + } + + companion object { + private const val TAG = "GuestUserInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt new file mode 100644 index 000000000000..8f36821a955e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/RefreshUsersScheduler.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** Encapsulates logic for pausing, unpausing, and scheduling a delayed job. */ +@SysUISingleton +class RefreshUsersScheduler +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Main private val mainDispatcher: CoroutineDispatcher, + private val repository: UserRepository, +) { + private var scheduledUnpauseJob: Job? = null + private var isPaused = false + + fun pause() { + applicationScope.launch(mainDispatcher) { + isPaused = true + scheduledUnpauseJob?.cancel() + scheduledUnpauseJob = + applicationScope.launch { + delay(PAUSE_REFRESH_USERS_TIMEOUT_MS) + unpauseAndRefresh() + } + } + } + + fun unpauseAndRefresh() { + applicationScope.launch(mainDispatcher) { + isPaused = false + refreshIfNotPaused() + } + } + + fun refreshIfNotPaused() { + applicationScope.launch(mainDispatcher) { + if (isPaused) { + return@launch + } + + repository.refreshUsers() + } + } + + companion object { + private const val PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt new file mode 100644 index 000000000000..1b4746a99f8f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.os.UserHandle +import android.os.UserManager +import com.android.systemui.user.data.repository.UserRepository + +/** Utilities related to user management actions. */ +object UserActionsUtil { + + /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */ + fun canCreateGuest( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + return currentUserCanCreateUsers(manager, repository) || + anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + } + + /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */ + fun canCreateUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + if (!isUserSwitcherEnabled) { + return false + } + + if ( + !currentUserCanCreateUsers(manager, repository) && + !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled) + ) { + return false + } + + return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY) + } + + /** + * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise. + */ + fun canCreateSupervisedUser( + manager: UserManager, + repository: UserRepository, + isUserSwitcherEnabled: Boolean, + isAddUsersFromLockScreenEnabled: Boolean, + supervisedUserPackageName: String? + ): Boolean { + if (supervisedUserPackageName.isNullOrEmpty()) { + return false + } + + return canCreateUser( + manager, + repository, + isUserSwitcherEnabled, + isAddUsersFromLockScreenEnabled + ) + } + + /** + * Returns `true` if the current user is allowed to add users to the device; `false` otherwise. + */ + private fun currentUserCanCreateUsers( + manager: UserManager, + repository: UserRepository, + ): Boolean { + val currentUser = repository.getSelectedUserInfo() + if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) { + return false + } + + return systemCanCreateUsers(manager) + } + + /** Returns `true` if the system can add users to the device; `false` otherwise. */ + private fun systemCanCreateUsers( + manager: UserManager, + ): Boolean { + return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM) + } + + /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */ + private fun anyoneCanCreateUsers( + manager: UserManager, + isAddUsersFromLockScreenEnabled: Boolean, + ): Boolean { + return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt index 3c5b9697c013..a84238c1559a 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserInteractor.kt @@ -17,94 +17,725 @@ package com.android.systemui.user.domain.interactor +import android.annotation.SuppressLint +import android.annotation.UserIdInt +import android.app.ActivityManager +import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.pm.UserInfo +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.RemoteException +import android.os.UserHandle +import android.os.UserManager import android.provider.Settings +import android.util.Log +import com.android.internal.util.UserIcons +import com.android.systemui.R +import com.android.systemui.SystemUISecondaryUserService +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.legacyhelper.data.LegacyUserDataHelper import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.kotlin.pairwise +import java.io.PrintWriter import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext /** Encapsulates business logic to interact with user data and systems. */ @SysUISingleton class UserInteractor @Inject constructor( - repository: UserRepository, + @Application private val applicationContext: Context, + private val repository: UserRepository, private val controller: UserSwitcherController, private val activityStarter: ActivityStarter, - keyguardInteractor: KeyguardInteractor, + private val keyguardInteractor: KeyguardInteractor, + private val featureFlags: FeatureFlags, + private val manager: UserManager, + @Application private val applicationScope: CoroutineScope, + telephonyInteractor: TelephonyInteractor, + broadcastDispatcher: BroadcastDispatcher, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val activityManager: ActivityManager, + private val refreshUsersScheduler: RefreshUsersScheduler, + private val guestUserInteractor: GuestUserInteractor, ) { + /** + * Defines interface for classes that can be notified when the state of users on the device is + * changed. + */ + interface UserCallback { + /** Returns `true` if this callback can be cleaned-up. */ + fun isEvictable(): Boolean = false + /** Notifies that the state of users on the device has changed. */ + fun onUserStateChanged() + } + + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + + private val supervisedUserPackageName: String? + get() = + applicationContext.getString( + com.android.internal.R.string.config_supervisedUserCreationPackage + ) + + private val callbackMutex = Mutex() + private val callbacks = mutableSetOf<UserCallback>() + /** List of current on-device users to select from. */ - val users: Flow<List<UserModel>> = repository.users + val users: Flow<List<UserModel>> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, settings -> + toUserModels( + userInfos = userInfos, + selectedUserId = selectedUserInfo.id, + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + } + } else { + repository.users + } /** The currently-selected user. */ - val selectedUser: Flow<UserModel> = repository.selectedUser + val selectedUser: Flow<UserModel> + get() = + if (isNewImpl) { + combine( + repository.selectedUserInfo, + repository.userSwitcherSettings, + ) { selectedUserInfo, settings -> + val selectedUserId = selectedUserInfo.id + checkNotNull( + toUserModel( + userInfo = selectedUserInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + isUserSwitcherEnabled = settings.isUserSwitcherEnabled, + ) + ) + } + } else { + repository.selectedUser + } /** List of user-switcher related actions that are available. */ - val actions: Flow<List<UserActionModel>> = - combine( - repository.isActionableWhenLocked, - keyguardInteractor.isKeyguardShowing, - ) { isActionableWhenLocked, isLocked -> - isActionableWhenLocked || !isLocked - } - .flatMapLatest { isActionable -> - if (isActionable) { - repository.actions.map { actions -> - actions + - if (actions.isNotEmpty()) { - // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because - // that's a user - // switcher specific action that is not known to the our data source - // or other - // features. - listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - } else { - // If no actions, don't add the navigate action. - emptyList() - } + val actions: Flow<List<UserActionModel>> + get() = + if (isNewImpl) { + combine( + repository.userInfos, + repository.userSwitcherSettings, + keyguardInteractor.isKeyguardShowing, + ) { userInfos, settings, isDeviceLocked -> + buildList { + val hasGuestUser = userInfos.any { it.isGuest } + if ( + !hasGuestUser && + (guestUserInteractor.isGuestUserAutoCreated || + UserActionsUtil.canCreateGuest( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + )) + ) { + add(UserActionModel.ENTER_GUEST_MODE) + } + + if (isDeviceLocked && !settings.isAddUsersFromLockscreen) { + // The device is locked and our setting to allow actions that add users + // from the lock-screen is not enabled. The guest action from above is + // always allowed, even when the device is locked, but the various "add + // user" actions below are not. We can finish building the list here. + return@buildList + } + + if ( + UserActionsUtil.canCreateUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + ) + ) { + add(UserActionModel.ADD_USER) + } + + if ( + UserActionsUtil.canCreateSupervisedUser( + manager, + repository, + settings.isUserSwitcherEnabled, + settings.isAddUsersFromLockscreen, + supervisedUserPackageName, + ) + ) { + add(UserActionModel.ADD_SUPERVISED_USER) + } } - } else { - // If not actionable it means that we're not allowed to show actions when locked - // and we - // are locked. Therefore, we should show no actions. - flowOf(emptyList()) } + } else { + combine( + repository.isActionableWhenLocked, + keyguardInteractor.isKeyguardShowing, + ) { isActionableWhenLocked, isLocked -> + isActionableWhenLocked || !isLocked + } + .flatMapLatest { isActionable -> + if (isActionable) { + repository.actions.map { actions -> + actions + + if (actions.isNotEmpty()) { + // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT + // because that's a user switcher specific action that is + // not known to the our data source or other features. + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } else { + // If no actions, don't add the navigate action. + emptyList() + } + } + } else { + // If not actionable it means that we're not allowed to show actions + // when + // locked and we are locked. Therefore, we should show no actions. + flowOf(emptyList()) + } + } } + val userRecords: StateFlow<ArrayList<UserRecord>> = + if (isNewImpl) { + combine( + repository.userInfos, + repository.selectedUserInfo, + actions, + repository.userSwitcherSettings, + ) { userInfos, selectedUserInfo, actionModels, settings -> + ArrayList( + userInfos.map { + toRecord( + userInfo = it, + selectedUserId = selectedUserInfo.id, + ) + } + + actionModels.map { + toRecord( + action = it, + selectedUserId = selectedUserInfo.id, + isAddFromLockscreenEnabled = settings.isAddUsersFromLockscreen, + ) + } + ) + } + .onEach { notifyCallbacks() } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ArrayList(), + ) + } else { + MutableStateFlow(ArrayList()) + } + + val selectedUserRecord: StateFlow<UserRecord?> = + if (isNewImpl) { + repository.selectedUserInfo + .map { selectedUserInfo -> + toRecord(userInfo = selectedUserInfo, selectedUserId = selectedUserInfo.id) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + } else { + MutableStateFlow(null) + } + /** Whether the device is configured to always have a guest user available. */ - val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated + val isGuestUserAutoCreated: Boolean = guestUserInteractor.isGuestUserAutoCreated /** Whether the guest user is currently being reset. */ - val isGuestUserResetting: Boolean = repository.isGuestUserResetting + val isGuestUserResetting: Boolean = guestUserInteractor.isGuestUserResetting + + private val _dialogShowRequests = MutableStateFlow<ShowDialogRequestModel?>(null) + val dialogShowRequests: Flow<ShowDialogRequestModel?> = _dialogShowRequests.asStateFlow() + + private val _dialogDismissRequests = MutableStateFlow<Unit?>(null) + val dialogDismissRequests: Flow<Unit?> = _dialogDismissRequests.asStateFlow() + + val isSimpleUserSwitcher: Boolean + get() = + if (isNewImpl) { + repository.isSimpleUserSwitcher() + } else { + error("Not supported in the old implementation!") + } + + init { + if (isNewImpl) { + refreshUsersScheduler.refreshIfNotPaused() + telephonyInteractor.callState + .distinctUntilChanged() + .onEach { refreshUsersScheduler.refreshIfNotPaused() } + .launchIn(applicationScope) + + combine( + broadcastDispatcher.broadcastFlow( + filter = + IntentFilter().apply { + addAction(Intent.ACTION_USER_ADDED) + addAction(Intent.ACTION_USER_REMOVED) + addAction(Intent.ACTION_USER_INFO_CHANGED) + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_USER_STOPPED) + addAction(Intent.ACTION_USER_UNLOCKED) + }, + user = UserHandle.SYSTEM, + map = { intent, _ -> intent }, + ), + repository.selectedUserInfo.pairwise(null), + ) { intent, selectedUserChange -> + Pair(intent, selectedUserChange.previousValue) + } + .onEach { (intent, previousSelectedUser) -> + onBroadcastReceived(intent, previousSelectedUser) + } + .launchIn(applicationScope) + } + } + + fun addCallback(callback: UserCallback) { + applicationScope.launch { callbackMutex.withLock { callbacks.add(callback) } } + } + + fun removeCallback(callback: UserCallback) { + applicationScope.launch { callbackMutex.withLock { callbacks.remove(callback) } } + } + + fun refreshUsers() { + refreshUsersScheduler.refreshIfNotPaused() + } + + fun onDialogShown() { + _dialogShowRequests.value = null + } + + fun onDialogDismissed() { + _dialogDismissRequests.value = null + } + + fun dump(pw: PrintWriter) { + pw.println("UserInteractor state:") + pw.println(" lastSelectedNonGuestUserId=${repository.lastSelectedNonGuestUserId}") + + val users = userRecords.value.filter { it.info != null } + pw.println(" userCount=${userRecords.value.count { LegacyUserDataHelper.isUser(it) }}") + for (i in users.indices) { + pw.println(" ${users[i]}") + } + + val actions = userRecords.value.filter { it.info == null } + pw.println(" actionCount=${userRecords.value.count { !LegacyUserDataHelper.isUser(it) }}") + for (i in actions.indices) { + pw.println(" ${actions[i]}") + } + + pw.println("isSimpleUserSwitcher=$isSimpleUserSwitcher") + pw.println("isGuestUserAutoCreated=$isGuestUserAutoCreated") + } + + fun onDeviceBootCompleted() { + guestUserInteractor.onDeviceBootCompleted() + } /** Switches to the user with the given user ID. */ fun selectUser( - userId: Int, + newlySelectedUserId: Int, ) { - controller.onUserSelected(userId, /* dialogShower= */ null) + if (isNewImpl) { + val currentlySelectedUserInfo = repository.getSelectedUserInfo() + if ( + newlySelectedUserId == currentlySelectedUserInfo.id && + currentlySelectedUserInfo.isGuest + ) { + // Here when clicking on the currently-selected guest user to leave guest mode + // and return to the previously-selected non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = repository.lastSelectedNonGuestUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + if (currentlySelectedUserInfo.isGuest) { + // Here when switching from guest to a non-guest user. + showDialog( + ShowDialogRequestModel.ShowExitGuestDialog( + guestUserId = currentlySelectedUserInfo.id, + targetUserId = newlySelectedUserId, + isGuestEphemeral = currentlySelectedUserInfo.isEphemeral, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + onExitGuestUser = this::exitGuestUser, + ) + ) + return + } + + switchUser(newlySelectedUserId) + } else { + controller.onUserSelected(newlySelectedUserId, /* dialogShower= */ null) + } } /** Executes the given action. */ fun executeAction(action: UserActionModel) { - when (action) { - UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) - UserActionModel.ADD_USER -> controller.showAddUserDialog(null) - UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> - activityStarter.startActivity( - Intent(Settings.ACTION_USER_SETTINGS), - /* dismissShade= */ false, + if (isNewImpl) { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> + guestUserInteractor.createAndSwitchTo( + this::showDialog, + this::dismissDialog, + this::selectUser, + ) + UserActionModel.ADD_USER -> { + val currentUser = repository.getSelectedUserInfo() + showDialog( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = currentUser.userHandle, + isKeyguardShowing = keyguardInteractor.isKeyguardShowing(), + showEphemeralMessage = currentUser.isGuest && currentUser.isEphemeral, + ) + ) + } + UserActionModel.ADD_SUPERVISED_USER -> + activityStarter.startActivity( + Intent() + .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER) + .setPackage(supervisedUserPackageName) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + /* dismissShade= */ false, + ) + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } else { + when (action) { + UserActionModel.ENTER_GUEST_MODE -> controller.createAndSwitchToGuestUser(null) + UserActionModel.ADD_USER -> controller.showAddUserDialog(null) + UserActionModel.ADD_SUPERVISED_USER -> controller.startSupervisedUserActivity() + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT -> + activityStarter.startActivity( + Intent(Settings.ACTION_USER_SETTINGS), + /* dismissShade= */ false, + ) + } + } + } + + fun exitGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + forceRemoveGuestOnExit: Boolean, + ) { + guestUserInteractor.exit( + guestUserId = guestUserId, + targetUserId = targetUserId, + forceRemoveGuestOnExit = forceRemoveGuestOnExit, + showDialog = this::showDialog, + dismissDialog = this::dismissDialog, + switchUser = this::switchUser, + ) + } + + fun removeGuestUser( + @UserIdInt guestUserId: Int, + @UserIdInt targetUserId: Int, + ) { + applicationScope.launch { + guestUserInteractor.remove( + guestUserId = guestUserId, + targetUserId = targetUserId, + ::showDialog, + ::dismissDialog, + ::selectUser, + ) + } + } + + private fun showDialog(request: ShowDialogRequestModel) { + _dialogShowRequests.value = request + } + + private fun dismissDialog() { + _dialogDismissRequests.value = Unit + } + + private fun notifyCallbacks() { + applicationScope.launch { + callbackMutex.withLock { + val iterator = callbacks.iterator() + while (iterator.hasNext()) { + val callback = iterator.next() + if (!callback.isEvictable()) { + callback.onUserStateChanged() + } else { + iterator.remove() + } + } + } + } + } + + private suspend fun toRecord( + userInfo: UserInfo, + selectedUserId: Int, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + manager = manager, + userInfo = userInfo, + picture = null, + isCurrent = userInfo.id == selectedUserId, + canSwitchUsers = canSwitchUsers(selectedUserId), + ) + } + + private suspend fun toRecord( + action: UserActionModel, + selectedUserId: Int, + isAddFromLockscreenEnabled: Boolean, + ): UserRecord { + return LegacyUserDataHelper.createRecord( + context = applicationContext, + selectedUserId = selectedUserId, + actionType = action, + isRestricted = + if (action == UserActionModel.ENTER_GUEST_MODE) { + // Entering guest mode is never restricted, so it's allowed to happen from the + // lockscreen even if the "add from lockscreen" system setting is off. + false + } else { + !isAddFromLockscreenEnabled + }, + isSwitchToEnabled = + canSwitchUsers(selectedUserId) && + // If the user is auto-created is must not be currently resetting. + !(isGuestUserAutoCreated && isGuestUserResetting), + ) + } + + private fun switchUser(userId: Int) { + // TODO(b/246631653): track jank and lantecy like in the old impl. + refreshUsersScheduler.pause() + try { + activityManager.switchUser(userId) + } catch (e: RemoteException) { + Log.e(TAG, "Couldn't switch user.", e) + } + } + + private suspend fun onBroadcastReceived( + intent: Intent, + previousUserInfo: UserInfo?, + ) { + val shouldRefreshAllUsers = + when (intent.action) { + Intent.ACTION_USER_SWITCHED -> { + dismissDialog() + val selectedUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) + if (previousUserInfo?.id != selectedUserId) { + notifyCallbacks() + restartSecondaryService(selectedUserId) + } + if (guestUserInteractor.isGuestUserAutoCreated) { + guestUserInteractor.guaranteePresent() + } + true + } + Intent.ACTION_USER_INFO_CHANGED -> true + Intent.ACTION_USER_UNLOCKED -> { + // If we unlocked the system user, we should refresh all users. + intent.getIntExtra( + Intent.EXTRA_USER_HANDLE, + UserHandle.USER_NULL, + ) == UserHandle.USER_SYSTEM + } + else -> true + } + + if (shouldRefreshAllUsers) { + refreshUsersScheduler.unpauseAndRefresh() + } + } + + private fun restartSecondaryService(@UserIdInt userId: Int) { + val intent = Intent(applicationContext, SystemUISecondaryUserService::class.java) + // Disconnect from the old secondary user's service + val secondaryUserId = repository.secondaryUserId + if (secondaryUserId != UserHandle.USER_NULL) { + applicationContext.stopServiceAsUser( + intent, + UserHandle.of(secondaryUserId), + ) + repository.secondaryUserId = UserHandle.USER_NULL + } + + // Connect to the new secondary user's service (purely to ensure that a persistent + // SystemUI application is created for that user) + if (userId != UserHandle.USER_SYSTEM) { + applicationContext.startServiceAsUser( + intent, + UserHandle.of(userId), + ) + repository.secondaryUserId = userId + } + } + + private suspend fun toUserModels( + userInfos: List<UserInfo>, + selectedUserId: Int, + isUserSwitcherEnabled: Boolean, + ): List<UserModel> { + val canSwitchUsers = canSwitchUsers(selectedUserId) + + return userInfos + // The guest user should go in the last position. + .sortedBy { it.isGuest } + .mapNotNull { userInfo -> + toUserModel( + userInfo = userInfo, + selectedUserId = selectedUserId, + canSwitchUsers = canSwitchUsers, + isUserSwitcherEnabled = isUserSwitcherEnabled, + ) + } + } + + private suspend fun toUserModel( + userInfo: UserInfo, + selectedUserId: Int, + canSwitchUsers: Boolean, + isUserSwitcherEnabled: Boolean, + ): UserModel? { + val userId = userInfo.id + val isSelected = userId == selectedUserId + + return when { + // When the user switcher is not enabled in settings, we only show the primary user. + !isUserSwitcherEnabled && !userInfo.isPrimary -> null + + // We avoid showing disabled users. + !userInfo.isEnabled -> null + userInfo.isGuest -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = true, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers, + isGuest = true, ) + userInfo.supportsSwitchToByUser() -> + UserModel( + id = userId, + name = Text.Loaded(userInfo.name), + image = + getUserImage( + isGuest = false, + userId = userId, + ), + isSelected = isSelected, + isSelectable = canSwitchUsers || isSelected, + isGuest = false, + ) + else -> null + } + } + + private suspend fun canSwitchUsers(selectedUserId: Int): Boolean { + return withContext(backgroundDispatcher) { + manager.getUserSwitchability(UserHandle.of(selectedUserId)) + } == UserManager.SWITCHABILITY_STATUS_OK + } + + @SuppressLint("UseCompatLoadingForDrawables") + private suspend fun getUserImage( + isGuest: Boolean, + userId: Int, + ): Drawable { + if (isGuest) { + return checkNotNull(applicationContext.getDrawable(R.drawable.ic_account_circle)) + } + + // TODO(b/246631653): cache the bitmaps to avoid the background work to fetch them. + // TODO(b/246631653): downscale the bitmaps to R.dimen.max_avatar_size if requested. + val userIcon = withContext(backgroundDispatcher) { manager.getUserIcon(userId) } + if (userIcon != null) { + return BitmapDrawable(userIcon) } + + return UserIcons.getDefaultUserIcon( + applicationContext.resources, + userId, + /* light= */ false + ) + } + + companion object { + private const val TAG = "UserInteractor" } } diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt new file mode 100644 index 000000000000..08d7c5a26a25 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/domain/model/ShowDialogRequestModel.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.model + +import android.os.UserHandle + +/** Encapsulates a request to show a dialog. */ +sealed class ShowDialogRequestModel { + data class ShowAddUserDialog( + val userHandle: UserHandle, + val isKeyguardShowing: Boolean, + val showEphemeralMessage: Boolean, + ) : ShowDialogRequestModel() + + data class ShowUserCreationDialog( + val isGuest: Boolean, + ) : ShowDialogRequestModel() + + data class ShowExitGuestDialog( + val guestUserId: Int, + val targetUserId: Int, + val isGuestEphemeral: Boolean, + val isKeyguardShowing: Boolean, + val onExitGuestUser: (guestId: Int, targetId: Int, forceRemoveGuest: Boolean) -> Unit, + ) : ShowDialogRequestModel() +} diff --git a/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt new file mode 100644 index 000000000000..137de1544b2d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/legacyhelper/data/LegacyUserDataHelper.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.legacyhelper.data + +import android.content.Context +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.os.UserManager +import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin +import com.android.settingslib.RestrictedLockUtilsInternal +import com.android.systemui.R +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel + +/** + * Defines utility functions for helping with legacy data code for users. + * + * We need these to avoid code duplication between logic inside the UserSwitcherController and in + * modern architecture classes such as repositories, interactors, and view-models. If we ever + * simplify UserSwitcherController (or delete it), the code here could be moved into its call-sites. + */ +object LegacyUserDataHelper { + + @JvmStatic + fun createRecord( + context: Context, + manager: UserManager, + picture: Bitmap?, + userInfo: UserInfo, + isCurrent: Boolean, + canSwitchUsers: Boolean, + ): UserRecord { + val isGuest = userInfo.isGuest + return UserRecord( + info = userInfo, + picture = + getPicture( + manager = manager, + context = context, + userInfo = userInfo, + picture = picture, + ), + isGuest = isGuest, + isCurrent = isCurrent, + isSwitchToEnabled = canSwitchUsers || (isCurrent && !isGuest), + ) + } + + @JvmStatic + fun createRecord( + context: Context, + selectedUserId: Int, + actionType: UserActionModel, + isRestricted: Boolean, + isSwitchToEnabled: Boolean, + ): UserRecord { + return UserRecord( + isGuest = actionType == UserActionModel.ENTER_GUEST_MODE, + isAddUser = actionType == UserActionModel.ADD_USER, + isAddSupervisedUser = actionType == UserActionModel.ADD_SUPERVISED_USER, + isRestricted = isRestricted, + isSwitchToEnabled = isSwitchToEnabled, + enforcedAdmin = + getEnforcedAdmin( + context = context, + selectedUserId = selectedUserId, + ), + ) + } + + fun toUserActionModel(record: UserRecord): UserActionModel { + check(!isUser(record)) + + return when { + record.isAddUser -> UserActionModel.ADD_USER + record.isAddSupervisedUser -> UserActionModel.ADD_SUPERVISED_USER + record.isGuest -> UserActionModel.ENTER_GUEST_MODE + else -> error("Not a known action: $record") + } + } + + fun isUser(record: UserRecord): Boolean { + return record.info != null + } + + private fun getEnforcedAdmin( + context: Context, + selectedUserId: Int, + ): EnforcedAdmin? { + val admin = + RestrictedLockUtilsInternal.checkIfRestrictionEnforced( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ?: return null + + return if ( + !RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, + UserManager.DISALLOW_ADD_USER, + selectedUserId, + ) + ) { + admin + } else { + null + } + } + + private fun getPicture( + context: Context, + manager: UserManager, + userInfo: UserInfo, + picture: Bitmap?, + ): Bitmap? { + if (userInfo.isGuest) { + return null + } + + if (picture != null) { + return picture + } + + val unscaledOrNull = manager.getUserIcon(userInfo.id) ?: return null + + val avatarSize = context.resources.getDimensionPixelSize(R.dimen.max_avatar_size) + return Bitmap.createScaledBitmap( + unscaledOrNull, + avatarSize, + avatarSize, + /* filter= */ true, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt index bf7977a600e9..2095683ccb4c 100644 --- a/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/shared/model/UserModel.kt @@ -32,4 +32,6 @@ data class UserModel( val isSelected: Boolean, /** Whether this use is selectable. A non-selectable user cannot be switched to. */ val isSelectable: Boolean, + /** Whether this model represents the guest user. */ + val isGuest: Boolean, ) diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt new file mode 100644 index 000000000000..a9d66de118e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/AddUserDialog.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.user.ui.dialog + +import android.app.ActivityManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.os.UserHandle +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.user.CreateUserActivity + +/** Dialog for adding a new user to the device. */ +class AddUserDialog( + context: Context, + userHandle: UserHandle, + isKeyguardShowing: Boolean, + showEphemeralMessage: Boolean, + private val falsingManager: FalsingManager, + private val broadcastSender: BroadcastSender, + private val dialogLaunchAnimator: DialogLaunchAnimator +) : SystemUIDialog(context) { + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (which == BUTTON_NEUTRAL) { + cancel() + return + } + + dialogLaunchAnimator.dismissStack(this@AddUserDialog) + if (ActivityManager.isUserAMonkey()) { + return + } + + // Use broadcast instead of ShadeController, as this dialog may have started in + // another + // process where normal dagger bindings are not available. + broadcastSender.sendBroadcastAsUser( + Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), + UserHandle.CURRENT + ) + + context.startActivityAsUser( + CreateUserActivity.createIntentForStart(context), + userHandle, + ) + } + } + + init { + setTitle(R.string.user_add_user_title) + val message = + context.getString(R.string.user_add_user_message_short) + + if (showEphemeralMessage) { + context.getString( + com.android.systemui.R.string.user_add_user_message_guest_remove + ) + } else { + "" + } + setMessage(message) + + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + + setButton( + BUTTON_POSITIVE, + context.getString(android.R.string.ok), + onClickListener, + ) + + setWindowOnTop(this, isKeyguardShowing) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt new file mode 100644 index 000000000000..19ad44d8649f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/ExitGuestDialog.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.android.systemui.user.ui.dialog + +import android.annotation.UserIdInt +import android.content.Context +import android.content.DialogInterface +import com.android.settingslib.R +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.statusbar.phone.SystemUIDialog + +/** Dialog for exiting the guest user. */ +class ExitGuestDialog( + context: Context, + private val guestUserId: Int, + private val isGuestEphemeral: Boolean, + private val targetUserId: Int, + isKeyguardShowing: Boolean, + private val falsingManager: FalsingManager, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val onExitGuestUserListener: OnExitGuestUserListener, +) : SystemUIDialog(context) { + + fun interface OnExitGuestUserListener { + fun onExitGuestUser( + @UserIdInt guestId: Int, + @UserIdInt targetId: Int, + forceRemoveGuest: Boolean, + ) + } + + private val onClickListener = + object : DialogInterface.OnClickListener { + override fun onClick(dialog: DialogInterface, which: Int) { + val penalty = + if (which == BUTTON_NEGATIVE) { + FalsingManager.NO_PENALTY + } else { + FalsingManager.MODERATE_PENALTY + } + if (falsingManager.isFalseTap(penalty)) { + return + } + + if (isGuestEphemeral) { + if (which == BUTTON_POSITIVE) { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Ephemeral guest: exit guest, guest is removed by the system + // on exit, since its marked ephemeral + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, false) + } else if (which == BUTTON_NEGATIVE) { + // Cancel clicked, do nothing + cancel() + } + } else { + when (which) { + BUTTON_POSITIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: exit guest, guest is not removed by the system + // on exit, since its marked non-ephemeral + onExitGuestUserListener.onExitGuestUser( + guestUserId, + targetUserId, + false + ) + } + BUTTON_NEGATIVE -> { + dialogLaunchAnimator.dismissStack(this@ExitGuestDialog) + // Non-ephemeral guest: remove guest and then exit + onExitGuestUserListener.onExitGuestUser(guestUserId, targetUserId, true) + } + BUTTON_NEUTRAL -> { + // Cancel clicked, do nothing + cancel() + } + } + } + } + } + + init { + if (isGuestEphemeral) { + setTitle(context.getString(R.string.guest_exit_dialog_title)) + setMessage(context.getString(R.string.guest_exit_dialog_message)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_dialog_button), + onClickListener, + ) + } else { + setTitle(context.getString(R.string.guest_exit_dialog_title_non_ephemeral)) + setMessage(context.getString(R.string.guest_exit_dialog_message_non_ephemeral)) + setButton( + BUTTON_NEUTRAL, + context.getString(android.R.string.cancel), + onClickListener, + ) + setButton( + BUTTON_NEGATIVE, + context.getString(R.string.guest_exit_clear_data_button), + onClickListener, + ) + setButton( + BUTTON_POSITIVE, + context.getString(R.string.guest_exit_save_data_button), + onClickListener, + ) + } + setWindowOnTop(this, isKeyguardShowing) + setCanceledOnTouchOutside(false) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt new file mode 100644 index 000000000000..c1d2f4788147 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserDialogModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.ui.dialog + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface UserDialogModule { + + @Binds + @IntoMap + @ClassKey(UserSwitcherDialogCoordinator::class) + fun bindFeature(impl: UserSwitcherDialogCoordinator): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt new file mode 100644 index 000000000000..6e7b5232d818 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.ui.dialog + +import android.app.Dialog +import android.content.Context +import com.android.settingslib.users.UserCreatingDialog +import com.android.systemui.CoreStartable +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.launch + +/** Coordinates dialogs for user switcher logic. */ +@SysUISingleton +class UserSwitcherDialogCoordinator +@Inject +constructor( + @Application private val context: Context, + @Application private val applicationScope: CoroutineScope, + private val falsingManager: FalsingManager, + private val broadcastSender: BroadcastSender, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val interactor: UserInteractor, + private val featureFlags: FeatureFlags, +) : CoreStartable(context) { + + private var currentDialog: Dialog? = null + + override fun start() { + if (featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER)) { + return + } + + startHandlingDialogShowRequests() + startHandlingDialogDismissRequests() + } + + private fun startHandlingDialogShowRequests() { + applicationScope.launch { + interactor.dialogShowRequests.filterNotNull().collect { request -> + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + currentDialog = + when (request) { + is ShowDialogRequestModel.ShowAddUserDialog -> + AddUserDialog( + context = context, + userHandle = request.userHandle, + isKeyguardShowing = request.isKeyguardShowing, + showEphemeralMessage = request.showEphemeralMessage, + falsingManager = falsingManager, + broadcastSender = broadcastSender, + dialogLaunchAnimator = dialogLaunchAnimator, + ) + is ShowDialogRequestModel.ShowUserCreationDialog -> + UserCreatingDialog( + context, + request.isGuest, + ) + is ShowDialogRequestModel.ShowExitGuestDialog -> + ExitGuestDialog( + context = context, + guestUserId = request.guestUserId, + isGuestEphemeral = request.isGuestEphemeral, + targetUserId = request.targetUserId, + isKeyguardShowing = request.isKeyguardShowing, + falsingManager = falsingManager, + dialogLaunchAnimator = dialogLaunchAnimator, + onExitGuestUserListener = request.onExitGuestUser, + ) + } + + currentDialog?.show() + interactor.onDialogShown() + } + } + } + + private fun startHandlingDialogDismissRequests() { + applicationScope.launch { + interactor.dialogDismissRequests.filterNotNull().collect { + currentDialog?.let { + if (it.isShowing) { + it.cancel() + } + } + + interactor.onDialogDismissed() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt index 398341d256d2..5b83df7b4a36 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModel.kt @@ -21,7 +21,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.android.systemui.R import com.android.systemui.common.ui.drawable.CircularDrawable +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.user.domain.interactor.GuestUserInteractor import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -36,9 +39,14 @@ import kotlinx.coroutines.flow.map class UserSwitcherViewModel private constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModel() { + private val isNewImpl: Boolean + get() = !featureFlags.isEnabled(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER) + /** On-device users. */ val users: Flow<List<UserViewModel>> = userInteractor.users.map { models -> models.map { user -> toViewModel(user) } } @@ -47,9 +55,6 @@ private constructor( val maximumUserColumns: Flow<Int> = users.map { LegacyUserUiHelper.getMaxUserSwitcherItemColumns(it.size) } - /** Whether the button to open the user action menu is visible. */ - val isOpenMenuButtonVisible: Flow<Boolean> = userInteractor.actions.map { it.isNotEmpty() } - private val _isMenuVisible = MutableStateFlow(false) /** * Whether the user action menu should be shown. Once the action menu is dismissed/closed, the @@ -58,9 +63,23 @@ private constructor( val isMenuVisible: Flow<Boolean> = _isMenuVisible /** The user action menu. */ val menu: Flow<List<UserActionViewModel>> = - userInteractor.actions.map { actions -> actions.map { action -> toViewModel(action) } } + userInteractor.actions.map { actions -> + if (isNewImpl && actions.isNotEmpty()) { + // If we have actions, we add NAVIGATE_TO_USER_MANAGEMENT because that's a user + // switcher specific action that is not known to the our data source or other + // features. + actions + listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + } else { + actions + } + .map { action -> toViewModel(action) } + } + + /** Whether the button to open the user action menu is visible. */ + val isOpenMenuButtonVisible: Flow<Boolean> = menu.map { it.isNotEmpty() } private val hasCancelButtonBeenClicked = MutableStateFlow(false) + private val isFinishRequiredDueToExecutedAction = MutableStateFlow(false) /** * Whether the observer should finish the experience. Once consumed, [onFinished] must be called @@ -81,6 +100,7 @@ private constructor( */ fun onFinished() { hasCancelButtonBeenClicked.value = false + isFinishRequiredDueToExecutedAction.value = false } /** Notifies that the user has clicked the "open menu" button. */ @@ -120,8 +140,10 @@ private constructor( }, // When the cancel button is clicked, we should finish. hasCancelButtonBeenClicked, - ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked -> - selectedUserChanged || screenTurnedOff || cancelButtonClicked + // If an executed action told us to finish, we should finish, + isFinishRequiredDueToExecutedAction, + ) { selectedUserChanged, screenTurnedOff, cancelButtonClicked, executedActionFinish -> + selectedUserChanged || screenTurnedOff || cancelButtonClicked || executedActionFinish } } @@ -164,13 +186,25 @@ private constructor( } else { LegacyUserUiHelper.getUserSwitcherActionTextResourceId( isGuest = model == UserActionModel.ENTER_GUEST_MODE, - isGuestUserAutoCreated = userInteractor.isGuestUserAutoCreated, - isGuestUserResetting = userInteractor.isGuestUserResetting, + isGuestUserAutoCreated = guestUserInteractor.isGuestUserAutoCreated, + isGuestUserResetting = guestUserInteractor.isGuestUserResetting, isAddSupervisedUser = model == UserActionModel.ADD_SUPERVISED_USER, isAddUser = model == UserActionModel.ADD_USER, ) }, - onClicked = { userInteractor.executeAction(action = model) }, + onClicked = { + userInteractor.executeAction(action = model) + // We don't finish because we want to show a dialog over the full-screen UI and + // that dialog can be dismissed in case the user changes their mind and decides not + // to add a user. + // + // We finish for all other actions because they navigate us away from the + // full-screen experience or are destructive (like changing to the guest user). + val shouldFinish = model != UserActionModel.ADD_USER + if (shouldFinish) { + isFinishRequiredDueToExecutedAction.value = true + } + }, ) } @@ -186,13 +220,17 @@ private constructor( @Inject constructor( private val userInteractor: UserInteractor, + private val guestUserInteractor: GuestUserInteractor, private val powerInteractor: PowerInteractor, + private val featureFlags: FeatureFlags, ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { @Suppress("UNCHECKED_CAST") return UserSwitcherViewModel( userInteractor = userInteractor, + guestUserInteractor = guestUserInteractor, powerInteractor = powerInteractor, + featureFlags = featureFlags, ) as T } diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt new file mode 100644 index 000000000000..0b8257da8fb5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxyExt.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.util.settings + +import android.annotation.UserIdInt +import android.database.ContentObserver +import android.os.UserHandle +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Kotlin extension functions for [SettingsProxy]. */ +object SettingsProxyExt { + + /** Returns a flow of [Unit] that is invoked each time that content is updated. */ + fun SettingsProxy.observerFlow( + vararg names: String, + @UserIdInt userId: Int = UserHandle.USER_CURRENT, + ): Flow<Unit> { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + names.forEach { name -> registerContentObserverForUser(name, observer, userId) } + + awaitClose { unregisterContentObserver(observer) } + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java index 43f6f1aac097..c1036e356cfa 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java @@ -411,7 +411,7 @@ public class KeyguardSecurityContainerTest extends SysuiTestCase { 0 /* flags */); users.add(new UserRecord(info, null, false /* isGuest */, false /* isCurrent */, false /* isAddUser */, false /* isRestricted */, true /* isSwitchToEnabled */, - false /* isAddSupervisedUser */)); + false /* isAddSupervisedUser */, null /* enforcedAdmin */)); } return users; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index ba1e168bc316..eea2e952c81f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -116,6 +116,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this) assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() val captor = argumentCaptor<KeyguardStateController.Callback>() verify(keyguardStateController).addCallback(captor.capture()) @@ -123,10 +124,12 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { whenever(keyguardStateController.isShowing).thenReturn(true) captor.value.onKeyguardShowingChanged() assertThat(latest).isTrue() + assertThat(underTest.isKeyguardShowing()).isTrue() whenever(keyguardStateController.isShowing).thenReturn(false) captor.value.onKeyguardShowingChanged() assertThat(latest).isFalse() + assertThat(underTest.isKeyguardShowing()).isFalse() job.cancel() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt index ff0faf98fe1e..098086a61cd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt @@ -35,7 +35,9 @@ import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.FalsingCollector import com.android.systemui.media.taptotransfer.common.MediaTttLogger +import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.FakeExecutor @@ -43,10 +45,12 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import dagger.Lazy import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.never import org.mockito.Mockito.verify @@ -75,6 +79,14 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { private lateinit var windowManager: WindowManager @Mock private lateinit var commandQueue: CommandQueue + @Mock + private lateinit var lazyFalsingManager: Lazy<FalsingManager> + @Mock + private lateinit var falsingManager: FalsingManager + @Mock + private lateinit var lazyFalsingCollector: Lazy<FalsingCollector> + @Mock + private lateinit var falsingCollector: FalsingCollector private lateinit var commandQueueCallback: CommandQueue.Callbacks private lateinit var fakeAppIconDrawable: Drawable private lateinit var fakeClock: FakeSystemClock @@ -101,6 +113,8 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { senderUiEventLogger = MediaTttSenderUiEventLogger(uiEventLoggerFake) whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())).thenReturn(TIMEOUT) + whenever(lazyFalsingManager.get()).thenReturn(falsingManager) + whenever(lazyFalsingCollector.get()).thenReturn(falsingCollector) controllerSender = MediaTttChipControllerSender( commandQueue, @@ -111,7 +125,9 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { accessibilityManager, configurationController, powerManager, - senderUiEventLogger + senderUiEventLogger, + lazyFalsingManager, + lazyFalsingCollector ) val callbackCaptor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) @@ -417,6 +433,38 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() { } @Test + fun transferToReceiverSucceeded_withUndoRunnable_falseTap_callbackNotRun() { + whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(true) + var undoCallbackCalled = false + val undoCallback = object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + } + + controllerSender.displayView(transferToReceiverSucceeded(undoCallback)) + getChipView().getUndoButton().performClick() + + assertThat(undoCallbackCalled).isFalse() + } + + @Test + fun transferToReceiverSucceeded_withUndoRunnable_realTap_callbackRun() { + whenever(lazyFalsingManager.get().isFalseTap(anyInt())).thenReturn(false) + var undoCallbackCalled = false + val undoCallback = object : IUndoMediaTransferCallback.Stub() { + override fun onUndoTriggered() { + undoCallbackCalled = true + } + } + + controllerSender.displayView(transferToReceiverSucceeded(undoCallback)) + getChipView().getUndoButton().performClick() + + assertThat(undoCallbackCalled).isTrue() + } + + @Test fun transferToReceiverSucceeded_undoButtonClick_switchesToTransferToThisDeviceTriggered() { val undoCallback = object : IUndoMediaTransferCallback.Stub() { override fun onUndoTriggered() {} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index 5d5918de3d9e..d2c2d58820bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -14,6 +14,9 @@ package com.android.systemui.qs; +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; +import static com.android.systemui.statusbar.StatusBarState.SHADE; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; @@ -49,13 +52,13 @@ import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.media.MediaHost; import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.customize.QSCustomizerController; import com.android.systemui.qs.dagger.QSFragmentComponent; import com.android.systemui.qs.external.TileServiceRequestController; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; @@ -93,7 +96,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { @Mock private QSPanel.QSTileLayout mQsTileLayout; @Mock private QSPanel.QSTileLayout mQQsTileLayout; @Mock private QSAnimator mQSAnimator; - @Mock private StatusBarStateController mStatusBarStateController; + @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private QSSquishinessController mSquishinessController; private View mQsFragmentView; @@ -158,7 +161,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { public void transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() { QSFragment fragment = resumeAndGetFragment(); - setStatusBarState(StatusBarState.KEYGUARD); + setStatusBarState(KEYGUARD); when(mQSPanelController.isBouncerInTransit()).thenReturn(false); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; @@ -174,7 +177,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { public void transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() { QSFragment fragment = resumeAndGetFragment(); - setStatusBarState(StatusBarState.KEYGUARD); + setStatusBarState(KEYGUARD); when(mQSPanelController.isBouncerInTransit()).thenReturn(true); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; @@ -262,6 +265,27 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { } @Test + public void setQsExpansion_inSplitShade_whenTransitioningToKeyguard_setsAlphaBasedOnShadeTransitionProgress() { + QSFragment fragment = resumeAndGetFragment(); + enableSplitShade(); + when(mStatusBarStateController.getState()).thenReturn(SHADE); + when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(KEYGUARD); + boolean isTransitioningToFullShade = false; + float transitionProgress = 0; + float squishinessFraction = 0f; + + fragment.setTransitionToFullShadeProgress(isTransitioningToFullShade, transitionProgress, + squishinessFraction); + + // trigger alpha refresh with non-zero expansion and fraction values + fragment.setQsExpansion(/* expansion= */ 1, /* panelExpansionFraction= */1, + /* proposedTranslation= */ 0, /* squishinessFraction= */ 1); + + // alpha should follow lockscreen to shade progress, not panel expansion fraction + assertThat(mQsFragmentView.getAlpha()).isEqualTo(transitionProgress); + } + + @Test public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() { QSFragment fragment = resumeAndGetFragment(); disableSplitShade(); @@ -402,6 +426,19 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { verify(mQSPanelController).setListening(eq(true), anyBoolean()); } + @Test + public void passCorrectExpansionState_inSplitShade() { + QSFragment fragment = resumeAndGetFragment(); + enableSplitShade(); + clearInvocations(mQSPanelController); + + fragment.setExpanded(true); + verify(mQSPanelController).setExpanded(true); + + fragment.setExpanded(false); + verify(mQSPanelController).setExpanded(false); + } + @Override protected Fragment instantiate(Context context, String className, Bundle arguments) { MockitoAnnotations.initMocks(this); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java index 7d563399ee1c..4c44dacab1a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ImageExporterTest.java @@ -33,6 +33,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.os.Process; import android.provider.MediaStore; import android.testing.AndroidTestingRunner; @@ -97,7 +98,8 @@ public class ImageExporterTest extends SysuiTestCase { Bitmap original = createCheckerBitmap(10, 10, 10); ListenableFuture<ImageExporter.Result> direct = - exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME); + exporter.export(DIRECT_EXECUTOR, requestId, original, CAPTURE_TIME, + Process.myUserHandle()); assertTrue("future should be done", direct.isDone()); assertFalse("future should not be canceled", direct.isCancelled()); ImageExporter.Result result = direct.get(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java index 69b7b88b0524..8c9404e336ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java @@ -180,7 +180,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data, ActionTransition::new, mSmartActionsProvider); Notification.Action shareAction = task.createShareAction(mContext, mContext.getResources(), @@ -208,7 +208,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data, ActionTransition::new, mSmartActionsProvider); Notification.Action editAction = task.createEditAction(mContext, mContext.getResources(), @@ -236,7 +236,7 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase { data.finisher = null; data.mActionsReadyListener = null; SaveImageInBackgroundTask task = - new SaveImageInBackgroundTask(mContext, null, mScreenshotSmartActions, data, + new SaveImageInBackgroundTask(mContext, null, null, mScreenshotSmartActions, data, ActionTransition::new, mSmartActionsProvider); Notification.Action deleteAction = task.createDeleteAction(mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index b40d5ac69d7b..0c60d3c72c2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -23,6 +23,9 @@ import static com.android.keyguard.KeyguardClockSwitch.SMALL; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; +import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_CLOSED; +import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPEN; +import static com.android.systemui.statusbar.phone.panelstate.PanelExpansionStateManagerKt.STATE_OPENING; import static com.google.common.truth.Truth.assertThat; @@ -1249,14 +1252,10 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testQsToBeImmediatelyExpandedWhenOpeningPanelInSplitShade() { enableSplitShade(/* enabled= */ true); - // set panel state to CLOSED - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0, - /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0); + mPanelExpansionStateManager.updateState(STATE_CLOSED); assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); - // change panel state to OPENING - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0.5f, - /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 100); + mPanelExpansionStateManager.updateState(STATE_OPENING); assertThat(mNotificationPanelViewController.mQsExpandImmediate).isTrue(); } @@ -1264,15 +1263,23 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { @Test public void testQsNotToBeImmediatelyExpandedWhenGoingFromUnlockedToLocked() { enableSplitShade(/* enabled= */ true); - // set panel state to CLOSED - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 0, - /* expanded= */ false, /* tracking= */ false, /* dragDownPxAmount= */ 0); + mPanelExpansionStateManager.updateState(STATE_CLOSED); - // go to lockscreen, which also sets fraction to 1.0f and makes shade "expanded" mStatusBarStateController.setState(KEYGUARD); - mPanelExpansionStateManager.onPanelExpansionChanged(/* fraction= */ 1, - /* expanded= */ true, /* tracking= */ true, /* dragDownPxAmount= */ 0); + // going to lockscreen would trigger STATE_OPENING + mPanelExpansionStateManager.updateState(STATE_OPENING); + + assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + } + + @Test + public void testQsImmediateResetsWhenPanelOpensOrCloses() { + mNotificationPanelViewController.mQsExpandImmediate = true; + mPanelExpansionStateManager.updateState(STATE_OPEN); + assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); + mNotificationPanelViewController.mQsExpandImmediate = true; + mPanelExpansionStateManager.updateState(STATE_CLOSED); assertThat(mNotificationPanelViewController.mQsExpandImmediate).isFalse(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java deleted file mode 100644 index eb4cca822c7f..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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 static org.junit.Assert.assertFalse; - -import android.os.Handler; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; - -import androidx.test.filters.SmallTest; - -import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.Dependency; -import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.notification.logging.NotificationLogger; -import com.android.systemui.statusbar.notification.row.NotificationGutsManager; -import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; -import com.android.systemui.statusbar.notification.stack.NotificationListContainer; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Verifies that particular sets of dependencies don't have dependencies on others. For example, - * code managing notifications shouldn't directly depend on CentralSurfaces, since there are - * platforms which want to manage notifications, but don't use CentralSurfaces. - */ -@SmallTest -@RunWith(AndroidTestingRunner.class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) -public class NonPhoneDependencyTest extends SysuiTestCase { - @Mock private NotificationPresenter mPresenter; - @Mock private NotificationListContainer mListContainer; - @Mock private RemoteInputController.Delegate mDelegate; - @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback; - @Mock private OnSettingsClickListener mOnSettingsClickListener; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - mDependency.injectMockDependency(KeyguardUpdateMonitor.class); - mDependency.injectTestDependency(Dependency.MAIN_HANDLER, - new Handler(TestableLooper.get(this).getLooper())); - } - - @Ignore("Causes binder calls which fail") - @Test - public void testNotificationManagementCodeHasNoDependencyOnStatusBarWindowManager() { - NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class); - NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class); - NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class); - NotificationRemoteInputManager remoteInputManager = - Dependency.get(NotificationRemoteInputManager.class); - NotificationLockscreenUserManager lockscreenUserManager = - Dependency.get(NotificationLockscreenUserManager.class); - gutsManager.setUpWithPresenter(mPresenter, mListContainer, - mOnSettingsClickListener); - notificationLogger.setUpWithContainer(mListContainer); - mediaManager.setUpWithPresenter(mPresenter); - remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback, - mDelegate); - lockscreenUserManager.setUpWithPresenter(mPresenter); - - TestableLooper.get(this).processAllMessages(); - assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class)); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java index 853d1df4b9bc..bdafa4893c9e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java @@ -52,6 +52,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; @@ -88,6 +89,8 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { @Mock private NotificationClickNotifier mClickNotifier; @Mock + private OverviewProxyService mOverviewProxyService; + @Mock private KeyguardManager mKeyguardManager; @Mock private DeviceProvisionedController mDeviceProvisionedController; @@ -344,6 +347,7 @@ public class NotificationLockscreenUserManagerTest extends SysuiTestCase { (() -> mVisibilityProvider), (() -> mNotifCollection), mClickNotifier, + (() -> mOverviewProxyService), NotificationLockscreenUserManagerTest.this.mKeyguardManager, mStatusBarStateController, Handler.createAsync(Looper.myLooper()), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index 2ee31265ff7b..2970807afb36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -337,6 +337,40 @@ class HeadsUpCoordinatorTest : SysuiTestCase() { } @Test + fun testOnEntryUpdated_toAlert() { + // GIVEN that an entry is posted that should not heads up + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryAdded(mEntry) + + // WHEN it's updated to heads up + setShouldHeadsUp(mEntry) + mCollectionListener.onEntryUpdated(mEntry) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification alerts + finishBind(mEntry) + verify(mHeadsUpManager).showNotification(mEntry) + } + + @Test + fun testOnEntryUpdated_toNotAlert() { + // GIVEN that an entry is posted that should heads up + setShouldHeadsUp(mEntry) + mCollectionListener.onEntryAdded(mEntry) + + // WHEN it's updated to not heads up + setShouldHeadsUp(mEntry, false) + mCollectionListener.onEntryUpdated(mEntry) + mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry)) + mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry)) + + // THEN the notification is never bound or shown + verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any()) + verify(mHeadsUpManager, never()).showNotification(any()) + } + + @Test fun testOnEntryRemovedRemovesHeadsUpNotification() { // GIVEN the current HUN is mEntry addHUN(mEntry) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt new file mode 100644 index 000000000000..f4d61c8e2a46 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitorTest.kt @@ -0,0 +1,323 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.widget.RemoteViews +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.NotificationUtils +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationMemoryMonitorTest : SysuiTestCase() { + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun currentNotificationMemoryUse_plainNotification() { + val notification = createBasicNotification().build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3316, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)) + val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = 0, + extras = 3316, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_customViewNotification_marksTrue() { + val notification = + createBasicNotification() + .setCustomContentView( + RemoteViews(context.packageName, android.R.layout.list_content) + ) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3384, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = true, + ) + } + + @Test + fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() { + val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444) + val notification = + createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = 444444, + largeIcon = 0, + extras = 3212, + bigPicture = 0, + extender = 0, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_bigPictureStyle() { + val bigPicture = + Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888)) + val bigPictureIcon = + Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val notification = + createBasicNotification() + .setStyle( + Notification.BigPictureStyle() + .bigPicture(bigPicture) + .bigLargeIcon(bigPictureIcon) + ) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 4092, + bigPicture = 960000, + extender = 0, + style = "BigPictureStyle", + styleIcon = bigPictureIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_callingStyle() { + val personIcon = + Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val person = Person.Builder().setIcon(personIcon).setName("Person").build() + val fakeIntent = + PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) + val notification = + createBasicNotification() + .setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent)) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 4084, + bigPicture = 0, + extender = 0, + style = "CallStyle", + styleIcon = personIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_messagingStyle() { + val personIcon = + Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888)) + val person = Person.Builder().setIcon(personIcon).setName("Person").build() + val message = Notification.MessagingStyle.Message("Message!", 4323, person) + val historicPersonIcon = + Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888)) + val historicPerson = + Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build() + val historicMessage = + Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson) + + val notification = + createBasicNotification() + .setStyle( + Notification.MessagingStyle(person) + .addMessage(message) + .addHistoricMessage(historicMessage) + ) + .build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 5024, + bigPicture = 0, + extender = 0, + style = "MessagingStyle", + styleIcon = + personIcon.bitmap.allocationByteCount + + historicPersonIcon.bitmap.allocationByteCount, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_carExtender() { + val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888) + val extender = Notification.CarExtender().setLargeIcon(carIcon) + val notification = createBasicNotification().extend(extender).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3612, + bigPicture = 0, + extender = 556656, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_tvWearExtender() { + val tvExtender = Notification.TvExtender().setChannel("channel2") + val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888) + val wearExtender = Notification.WearableExtender().setBackground(wearBackground) + val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build() + val nmm = createNMMWithNotifications(listOf(notification)) + val memoryUse = getUseObject(nmm.currentNotificationMemoryUse()) + assertNotificationObjectSizes( + memoryUse = memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3820, + bigPicture = 0, + extender = 388 + wearBackground.allocationByteCount, + style = null, + styleIcon = 0, + hasCustomView = false, + ) + } + + private fun createBasicNotification(): Notification.Builder { + val smallIcon = + Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888)) + val largeIcon = + Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888)) + return Notification.Builder(context) + .setSmallIcon(smallIcon) + .setLargeIcon(largeIcon) + .setContentTitle("This is a title") + .setContentText("This is content text.") + } + + /** This will generate a nicer error message than comparing objects */ + private fun assertNotificationObjectSizes( + memoryUse: NotificationMemoryUsage, + smallIcon: Int, + largeIcon: Int, + extras: Int, + bigPicture: Int, + extender: Int, + style: String?, + styleIcon: Int, + hasCustomView: Boolean + ) { + assertThat(memoryUse.packageName).isEqualTo("test_pkg") + assertThat(memoryUse.notificationId) + .isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0")) + assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) + assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) + assertThat(memoryUse.objectUsage.extras).isEqualTo(extras) + assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture) + assertThat(memoryUse.objectUsage.extender).isEqualTo(extender) + if (style == null) { + assertThat(memoryUse.objectUsage.style).isNull() + } else { + assertThat(memoryUse.objectUsage.style).isEqualTo(style) + } + assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon) + assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView) + } + + private fun getUseObject( + singleItemUseList: List<NotificationMemoryUsage> + ): NotificationMemoryUsage { + assertThat(singleItemUseList).hasSize(1) + return singleItemUseList[0] + } + + private fun createNMMWithNotifications( + notifications: List<Notification> + ): NotificationMemoryMonitor { + val notifPipeline: NotifPipeline = mock() + val notificationEntries = + notifications.map { n -> + NotificationEntryBuilder().setTag("test").setNotification(n).build() + } + whenever(notifPipeline.allNotifs).thenReturn(notificationEntries) + return NotificationMemoryMonitor(notifPipeline, mock()) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java index 7e97629e82e2..dae0aa229dbf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java @@ -40,6 +40,10 @@ public class NotificationPanelLoggerFake implements NotificationPanelLogger { NotificationPanelLogger.toNotificationProto(visibleNotifications))); } + @Override + public void logNotificationDrag(NotificationEntry draggedNotification) { + } + public static class CallRecord { public boolean isLockscreen; public Notifications.NotificationList list; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java index 922e93d06efc..ed2afe753a5e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java @@ -40,6 +40,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.shade.ShadeController; +import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; +import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerFake; import com.android.systemui.statusbar.policy.HeadsUpManager; import org.junit.Before; @@ -63,6 +65,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { private NotificationMenuRowPlugin.MenuItem mMenuItem = mock(NotificationMenuRowPlugin.MenuItem.class); private ShadeController mShadeController = mock(ShadeController.class); + private NotificationPanelLogger mNotificationPanelLogger = mock(NotificationPanelLogger.class); @Before public void setUp() throws Exception { @@ -82,7 +85,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { when(mMenuRow.getLongpressMenuItem(any(Context.class))).thenReturn(mMenuItem); mController = new ExpandableNotificationRowDragController(mContext, mHeadsUpManager, - mShadeController); + mShadeController, mNotificationPanelLogger); } @Test @@ -96,6 +99,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { mRow.doDragCallback(0, 0); verify(controller).startDragAndDrop(mRow); verify(mHeadsUpManager, times(1)).releaseAllImmediately(); + verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); } @Test @@ -107,6 +111,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { verify(controller).startDragAndDrop(mRow); verify(mShadeController).animateCollapsePanels(eq(0), eq(true), eq(false), anyFloat()); + verify(mNotificationPanelLogger, times(1)).logNotificationDrag(any()); } @Test @@ -124,6 +129,7 @@ public class ExpandableNotificationRowDragControllerTest extends SysuiTestCase { // Verify that we never start the actual drag since there is no content verify(mRow, never()).startDragAndDrop(any(), any(), any(), anyInt()); + verify(mNotificationPanelLogger, never()).logNotificationDrag(any()); } private ExpandableNotificationRowDragController createSpyController() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index cfaa4707ef76..6ec5cf82a81e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -47,6 +47,7 @@ import androidx.test.filters.SmallTest; import com.android.keyguard.CarrierTextController; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.battery.BatteryMeterViewController; @@ -123,6 +124,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { private StatusBarUserInfoTracker mStatusBarUserInfoTracker; @Mock private SecureSettings mSecureSettings; @Mock private CommandQueue mCommandQueue; + @Mock private KeyguardLogger mLogger; private TestNotificationPanelViewStateProvider mNotificationPanelViewStateProvider; private KeyguardStatusBarView mKeyguardStatusBarView; @@ -172,7 +174,8 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mStatusBarUserInfoTracker, mSecureSettings, mCommandQueue, - mFakeExecutor + mFakeExecutor, + mLogger ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt new file mode 100644 index 000000000000..773a0d8ceb64 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import android.telephony.TelephonyCallback +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.telephony.TelephonyListenerManager +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class TelephonyRepositoryImplTest : SysuiTestCase() { + + @Mock private lateinit var manager: TelephonyListenerManager + + private lateinit var underTest: TelephonyRepositoryImpl + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + TelephonyRepositoryImpl( + manager = manager, + ) + } + + @Test + fun callState() = + runBlocking(IMMEDIATE) { + var callState: Int? = null + val job = underTest.callState.onEach { callState = it }.launchIn(this) + val listenerCaptor = kotlinArgumentCaptor<TelephonyCallback.CallStateListener>() + verify(manager).addCallStateListener(listenerCaptor.capture()) + val listener = listenerCaptor.value + + listener.onCallStateChanged(0) + assertThat(callState).isEqualTo(0) + + listener.onCallStateChanged(1) + assertThat(callState).isEqualTo(1) + + listener.onCallStateChanged(2) + assertThat(callState).isEqualTo(2) + + job.cancel() + + verify(manager).removeCallStateListener(listener) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt new file mode 100644 index 000000000000..4a8e0552d778 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplRefactoredTest.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.data.repository + +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplRefactoredTest : UserRepositoryImplTest() { + + @Before + fun setUp() { + super.setUp(isRefactored = true) + } + + @Test + fun userSwitcherSettings() = runSelfCancelingTest { + setUpGlobalSettings( + isSimpleUserSwitcher = true, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + underTest = create(this) + + var value: UserSwitcherSettingsModel? = null + underTest.userSwitcherSettings.onEach { value = it }.launchIn(this) + + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = true, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + + setUpGlobalSettings( + isSimpleUserSwitcher = false, + isAddUsersFromLockscreen = true, + isUserSwitcherEnabled = true, + ) + assertUserSwitcherSettings( + model = value, + expectedSimpleUserSwitcher = false, + expectedAddUsersFromLockscreen = true, + expectedUserSwitcherEnabled = true, + ) + } + + @Test + fun refreshUsers() = runSelfCancelingTest { + underTest = create(this) + val initialExpectedValue = + setUpUsers( + count = 3, + selectedIndex = 0, + ) + var userInfos: List<UserInfo>? = null + var selectedUserInfo: UserInfo? = null + underTest.userInfos.onEach { userInfos = it }.launchIn(this) + underTest.selectedUserInfo.onEach { selectedUserInfo = it }.launchIn(this) + + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(initialExpectedValue) + assertThat(selectedUserInfo).isEqualTo(initialExpectedValue[0]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val secondExpectedValue = + setUpUsers( + count = 4, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(secondExpectedValue) + assertThat(selectedUserInfo).isEqualTo(secondExpectedValue[1]) + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedUserInfo?.id) + + val selectedNonGuestUserId = selectedUserInfo?.id + val thirdExpectedValue = + setUpUsers( + count = 2, + hasGuest = true, + selectedIndex = 1, + ) + underTest.refreshUsers() + assertThat(userInfos).isEqualTo(thirdExpectedValue) + assertThat(selectedUserInfo).isEqualTo(thirdExpectedValue[1]) + assertThat(selectedUserInfo?.isGuest).isTrue() + assertThat(underTest.lastSelectedNonGuestUserId).isEqualTo(selectedNonGuestUserId) + } + + private fun setUpUsers( + count: Int, + hasGuest: Boolean = false, + selectedIndex: Int = 0, + ): List<UserInfo> { + val userInfos = + (0 until count).map { index -> + createUserInfo( + index, + isGuest = hasGuest && index == count - 1, + ) + } + whenever(manager.aliveUsers).thenReturn(userInfos) + tracker.set(userInfos, selectedIndex) + return userInfos + } + + private fun createUserInfo( + id: Int, + isGuest: Boolean, + ): UserInfo { + val flags = 0 + return UserInfo( + id, + "user_$id", + /* iconPath= */ "", + flags, + if (isGuest) UserManager.USER_TYPE_FULL_GUEST else UserInfo.getDefaultUserType(flags), + ) + } + + private fun setUpGlobalSettings( + isSimpleUserSwitcher: Boolean = false, + isAddUsersFromLockscreen: Boolean = false, + isUserSwitcherEnabled: Boolean = true, + ) { + context.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_expandLockScreenUserSwitcher, + true, + ) + globalSettings.putIntForUser( + UserRepositoryImpl.SETTING_SIMPLE_USER_SWITCHER, + if (isSimpleUserSwitcher) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.ADD_USERS_WHEN_LOCKED, + if (isAddUsersFromLockscreen) 1 else 0, + UserHandle.USER_SYSTEM, + ) + globalSettings.putIntForUser( + Settings.Global.USER_SWITCHER_ENABLED, + if (isUserSwitcherEnabled) 1 else 0, + UserHandle.USER_SYSTEM, + ) + } + + private fun assertUserSwitcherSettings( + model: UserSwitcherSettingsModel?, + expectedSimpleUserSwitcher: Boolean, + expectedAddUsersFromLockscreen: Boolean, + expectedUserSwitcherEnabled: Boolean, + ) { + checkNotNull(model) + assertThat(model.isSimpleUserSwitcher).isEqualTo(expectedSimpleUserSwitcher) + assertThat(model.isAddUsersFromLockscreen).isEqualTo(expectedAddUsersFromLockscreen) + assertThat(model.isUserSwitcherEnabled).isEqualTo(expectedUserSwitcherEnabled) + } + + /** + * Executes the given block of execution within the scope of a dedicated [CoroutineScope] which + * is then automatically canceled and cleaned-up. + */ + private fun runSelfCancelingTest( + block: suspend CoroutineScope.() -> Unit, + ) = + runBlocking(Dispatchers.Main.immediate) { + val scope = CoroutineScope(coroutineContext + Job()) + block(scope) + scope.cancel() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index 6fec343d036c..dcea83a55a74 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -17,201 +17,54 @@ package com.android.systemui.user.data.repository -import android.content.pm.UserInfo import android.os.UserManager -import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.settings.FakeUserTracker import com.android.systemui.statusbar.policy.UserSwitcherController -import com.android.systemui.user.data.source.UserRecord -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.user.shared.model.UserModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.capture -import com.google.common.truth.Truth.assertThat +import com.android.systemui.util.settings.FakeSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentCaptor -import org.mockito.Captor +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserRepositoryImplTest : SysuiTestCase() { +abstract class UserRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var manager: UserManager - @Mock private lateinit var controller: UserSwitcherController - @Captor - private lateinit var userSwitchCallbackCaptor: - ArgumentCaptor<UserSwitcherController.UserSwitchCallback> + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var controller: UserSwitcherController - private lateinit var underTest: UserRepositoryImpl + protected lateinit var underTest: UserRepositoryImpl - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) - whenever(controller.isGuestUserAutoCreated).thenReturn(false) - whenever(controller.isGuestUserResetting).thenReturn(false) - - underTest = - UserRepositoryImpl( - appContext = context, - manager = manager, - controller = controller, - ) - } - - @Test - fun `users - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `users - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.users.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `users - does not include actions`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List<UserModel>? = null - val job = underTest.users.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)?.id).isEqualTo(0) - assertThat(models?.get(0)?.isSelected).isTrue() - assertThat(models?.get(1)?.id).isEqualTo(1) - assertThat(models?.get(1)?.isSelected).isFalse() - assertThat(models?.get(2)?.id).isEqualTo(2) - assertThat(models?.get(2)?.isSelected).isFalse() - job.cancel() - } - - @Test - fun selectedUser() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createUserRecord(1), - createUserRecord(2), - ) - ) - var id: Int? = null - val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + protected lateinit var globalSettings: FakeSettings + protected lateinit var tracker: FakeUserTracker + protected lateinit var featureFlags: FakeFeatureFlags - assertThat(id).isEqualTo(0) - - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0), - createUserRecord(1), - createUserRecord(2, isSelected = true), - ) - ) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - userSwitchCallbackCaptor.value.onUserSwitched() - assertThat(id).isEqualTo(2) - - job.cancel() - } - - @Test - fun `actions - unregisters from updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) - - job.cancel() - - verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) - } - - @Test - fun `actions - registers for updates`() = - runBlocking(IMMEDIATE) { - val job = underTest.actions.onEach {}.launchIn(this) - - verify(controller).addUserSwitchCallback(any()) - - job.cancel() - } - - @Test - fun `actopms - does not include users`() = - runBlocking(IMMEDIATE) { - whenever(controller.users) - .thenReturn( - arrayListOf( - createUserRecord(0, isSelected = true), - createActionRecord(UserActionModel.ADD_USER), - createUserRecord(1), - createUserRecord(2), - createActionRecord(UserActionModel.ADD_SUPERVISED_USER), - createActionRecord(UserActionModel.ENTER_GUEST_MODE), - ) - ) - var models: List<UserActionModel>? = null - val job = underTest.actions.onEach { models = it }.launchIn(this) - - assertThat(models).hasSize(3) - assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) - assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) - assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) - job.cancel() - } + protected fun setUp(isRefactored: Boolean) { + MockitoAnnotations.initMocks(this) - private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { - return UserRecord( - info = UserInfo(id, "name$id", 0), - isCurrent = isSelected, - ) + globalSettings = FakeSettings() + tracker = FakeUserTracker() + featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored) } - private fun createActionRecord(action: UserActionModel): UserRecord { - return UserRecord( - isAddUser = action == UserActionModel.ADD_USER, - isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, - isGuest = action == UserActionModel.ENTER_GUEST_MODE, + protected fun create(scope: CoroutineScope = TestCoroutineScope()): UserRepositoryImpl { + return UserRepositoryImpl( + appContext = context, + manager = manager, + controller = controller, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + globalSettings = globalSettings, + tracker = tracker, + featureFlags = featureFlags, ) } companion object { - private val IMMEDIATE = Dispatchers.Main.immediate + @JvmStatic protected val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt new file mode 100644 index 000000000000..d4b41c18e123 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplUnrefactoredTest.kt @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.data.repository + +import android.content.pm.UserInfo +import androidx.test.filters.SmallTest +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever + +@SmallTest +@RunWith(JUnit4::class) +class UserRepositoryImplUnrefactoredTest : UserRepositoryImplTest() { + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } + + @Captor + private lateinit var userSwitchCallbackCaptor: + ArgumentCaptor<UserSwitcherController.UserSwitchCallback> + + @Before + fun setUp() { + super.setUp(isRefactored = false) + + whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false)) + whenever(controller.isGuestUserAutoCreated).thenReturn(false) + whenever(controller.isGuestUserResetting).thenReturn(false) + + underTest = create() + } + + @Test + fun `users - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `users - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.users.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `users - does not include actions`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List<UserModel>? = null + val job = underTest.users.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)?.id).isEqualTo(0) + assertThat(models?.get(0)?.isSelected).isTrue() + assertThat(models?.get(1)?.id).isEqualTo(1) + assertThat(models?.get(1)?.isSelected).isFalse() + assertThat(models?.get(2)?.id).isEqualTo(2) + assertThat(models?.get(2)?.isSelected).isFalse() + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createUserRecord(1), + createUserRecord(2), + ) + ) + var id: Int? = null + val job = underTest.selectedUser.map { it.id }.onEach { id = it }.launchIn(this) + + assertThat(id).isEqualTo(0) + + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0), + createUserRecord(1), + createUserRecord(2, isSelected = true), + ) + ) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + userSwitchCallbackCaptor.value.onUserSwitched() + assertThat(id).isEqualTo(2) + + job.cancel() + } + + @Test + fun `actions - unregisters from updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + verify(controller).addUserSwitchCallback(capture(userSwitchCallbackCaptor)) + + job.cancel() + + verify(controller).removeUserSwitchCallback(userSwitchCallbackCaptor.value) + } + + @Test + fun `actions - registers for updates`() = + runBlocking(IMMEDIATE) { + val job = underTest.actions.onEach {}.launchIn(this) + + verify(controller).addUserSwitchCallback(any()) + + job.cancel() + } + + @Test + fun `actions - does not include users`() = + runBlocking(IMMEDIATE) { + whenever(controller.users) + .thenReturn( + arrayListOf( + createUserRecord(0, isSelected = true), + createActionRecord(UserActionModel.ADD_USER), + createUserRecord(1), + createUserRecord(2), + createActionRecord(UserActionModel.ADD_SUPERVISED_USER), + createActionRecord(UserActionModel.ENTER_GUEST_MODE), + ) + ) + var models: List<UserActionModel>? = null + val job = underTest.actions.onEach { models = it }.launchIn(this) + + assertThat(models).hasSize(3) + assertThat(models?.get(0)).isEqualTo(UserActionModel.ADD_USER) + assertThat(models?.get(1)).isEqualTo(UserActionModel.ADD_SUPERVISED_USER) + assertThat(models?.get(2)).isEqualTo(UserActionModel.ENTER_GUEST_MODE) + job.cancel() + } + + private fun createUserRecord(id: Int, isSelected: Boolean = false): UserRecord { + return UserRecord( + info = UserInfo(id, "name$id", 0), + isCurrent = isSelected, + ) + } + + private fun createActionRecord(action: UserActionModel): UserRecord { + return UserRecord( + isAddUser = action == UserActionModel.ADD_USER, + isAddSupervisedUser = action == UserActionModel.ADD_SUPERVISED_USER, + isGuest = action == UserActionModel.ENTER_GUEST_MODE, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt new file mode 100644 index 000000000000..120bf791c462 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.app.admin.DevicePolicyManager +import android.content.pm.UserInfo +import android.os.UserHandle +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class GuestUserInteractorTest : SysuiTestCase() { + + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var showDialog: (ShowDialogRequestModel) -> Unit + @Mock private lateinit var dismissDialog: () -> Unit + @Mock private lateinit var selectUser: (Int) -> Unit + @Mock private lateinit var switchUser: (Int) -> Unit + + private lateinit var underTest: GuestUserInteractor + + private lateinit var scope: TestCoroutineScope + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(manager.createGuest(any())).thenReturn(GUEST_USER_INFO) + + scope = TestCoroutineScope() + repository = FakeUserRepository() + repository.setUserInfos(ALL_USERS) + + underTest = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = repository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = repository, + ), + uiEventLogger = uiEventLogger, + ) + } + + @Test + fun `onDeviceBootCompleted - allowed to add - create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd() + + underTest.onDeviceBootCompleted() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController, never()).addCallback(any()) + } + + @Test + fun `onDeviceBootCompleted - await provisioning - and create guest`() = + runBlocking(IMMEDIATE) { + setAllowedToAdd(isAllowed = false) + underTest.onDeviceBootCompleted() + val captor = + kotlinArgumentCaptor<DeviceProvisionedController.DeviceProvisionedListener>() + verify(deviceProvisionedController).addCallback(captor.capture()) + + setAllowedToAdd(isAllowed = true) + captor.value.onDeviceProvisionedChanged() + + verify(manager).createGuest(any()) + verify(deviceProvisionedController).removeCallback(captor.value) + } + + @Test + fun createAndSwitchTo() = + runBlocking(IMMEDIATE) { + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser).invoke(GUEST_USER_INFO.id) + } + + @Test + fun `createAndSwitchTo - fails to create - does not switch to`() = + runBlocking(IMMEDIATE) { + whenever(manager.createGuest(any())).thenReturn(null) + + underTest.createAndSwitchTo( + showDialog = showDialog, + dismissDialog = dismissDialog, + selectUser = selectUser, + ) + + verify(showDialog).invoke(ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true)) + verify(manager).createGuest(any()) + verify(dismissDialog).invoke() + verify(selectUser, never()).invoke(anyInt()) + } + + @Test + fun `exit - returns to target user`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(GUEST_USER_INFO) + + val targetUserId = NON_GUEST_USER_INFO.id + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - returns to last non-guest`() = + runBlocking(IMMEDIATE) { + val expectedUserId = NON_GUEST_USER_INFO.id + whenever(manager.getUserInfo(expectedUserId)).thenReturn(NON_GUEST_USER_INFO) + repository.lastSelectedNonGuestUserId = expectedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(expectedUserId) + } + + @Test + fun `exit - last non-guest was removed - returns to system`() = + runBlocking(IMMEDIATE) { + val removedUserId = 310 + repository.lastSelectedNonGuestUserId = removedUserId + repository.setSelectedUserInfo(GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = UserHandle.USER_NULL, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(manager, never()).removeUser(anyInt()) + verify(switchUser).invoke(UserHandle.USER_SYSTEM) + } + + @Test + fun `exit - guest was ephemeral - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setUserInfos(listOf(NON_GUEST_USER_INFO, EPHEMERAL_GUEST_USER_INFO)) + repository.setSelectedUserInfo(EPHEMERAL_GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(EPHEMERAL_GUEST_USER_INFO.id) + verify(manager).removeUser(EPHEMERAL_GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - force remove guest - it is removed`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(GUEST_USER_INFO) + val targetUserId = NON_GUEST_USER_INFO.id + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + forceRemoveGuestOnExit = true, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) + verify(manager).removeUser(GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `exit - selected different from guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + @Test + fun `exit - selected is actually not a guest user - do nothing`() = + runBlocking(IMMEDIATE) { + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.exit( + guestUserId = NON_GUEST_USER_INFO.id, + targetUserId = 123, + forceRemoveGuestOnExit = false, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotExit() + } + + @Test + fun `remove - returns to target user`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(GUEST_USER_INFO) + + val targetUserId = NON_GUEST_USER_INFO.id + underTest.remove( + guestUserId = GUEST_USER_INFO.id, + targetUserId = targetUserId, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verify(manager).markGuestForDeletion(GUEST_USER_INFO.id) + verify(manager).removeUser(GUEST_USER_INFO.id) + verify(switchUser).invoke(targetUserId) + } + + @Test + fun `remove - selected different from guest user - do nothing`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.remove( + guestUserId = GUEST_USER_INFO.id, + targetUserId = 123, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotRemove() + } + + @Test + fun `remove - selected is actually not a guest user - do nothing`() = + runBlocking(IMMEDIATE) { + whenever(manager.markGuestForDeletion(anyInt())).thenReturn(true) + repository.setSelectedUserInfo(NON_GUEST_USER_INFO) + + underTest.remove( + guestUserId = NON_GUEST_USER_INFO.id, + targetUserId = 123, + showDialog = showDialog, + dismissDialog = dismissDialog, + switchUser = switchUser, + ) + + verifyDidNotRemove() + } + + private fun setAllowedToAdd(isAllowed: Boolean = true) { + whenever(deviceProvisionedController.isDeviceProvisioned).thenReturn(isAllowed) + whenever(devicePolicyManager.isDeviceManaged).thenReturn(!isAllowed) + } + + private fun verifyDidNotExit() { + verifyDidNotRemove() + verify(manager, never()).getUserInfo(anyInt()) + verify(uiEventLogger, never()).log(any()) + } + + private fun verifyDidNotRemove() { + verify(manager, never()).markGuestForDeletion(anyInt()) + verify(showDialog, never()).invoke(any()) + verify(dismissDialog, never()).invoke() + verify(switchUser, never()).invoke(anyInt()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val NON_GUEST_USER_INFO = + UserInfo( + /* id= */ 818, + /* name= */ "non_guest", + /* flags= */ 0, + ) + private val GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ 0, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val EPHEMERAL_GUEST_USER_INFO = + UserInfo( + /* id= */ 669, + /* name= */ "guest", + /* iconPath= */ "", + /* flags= */ UserInfo.FLAG_EPHEMERAL, + UserManager.USER_TYPE_FULL_GUEST, + ) + private val ALL_USERS = + listOf( + NON_GUEST_USER_INFO, + GUEST_USER_INFO, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt new file mode 100644 index 000000000000..593ce1f0a2f5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/RefreshUsersSchedulerTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.user.data.repository.FakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class RefreshUsersSchedulerTest : SysuiTestCase() { + + private lateinit var underTest: RefreshUsersScheduler + + private lateinit var repository: FakeUserRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + repository = FakeUserRepository() + } + + @Test + fun `pause - prevents the next refresh from happening`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.refreshIfNotPaused() + assertThat(repository.refreshUsersCallCount).isEqualTo(0) + } + + @Test + fun `unpauseAndRefresh - forces the refresh even when paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.pause() + + underTest.unpauseAndRefresh() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + @Test + fun `refreshIfNotPaused - refreshes when not paused`() = + runBlocking(IMMEDIATE) { + underTest = + RefreshUsersScheduler( + applicationScope = this, + mainDispatcher = IMMEDIATE, + repository = repository, + ) + underTest.refreshIfNotPaused() + + assertThat(repository.refreshUsersCallCount).isEqualTo(1) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt new file mode 100644 index 000000000000..3d5695a09ebc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorRefactoredTest.kt @@ -0,0 +1,658 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import android.content.Intent +import android.content.pm.UserInfo +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import androidx.test.filters.SmallTest +import com.android.internal.R.drawable.ic_account_circle +import com.android.systemui.R +import com.android.systemui.common.shared.model.Text +import com.android.systemui.user.data.model.UserSwitcherSettingsModel +import com.android.systemui.user.data.source.UserRecord +import com.android.systemui.user.domain.model.ShowDialogRequestModel +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.user.shared.model.UserModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.verify + +@SmallTest +@RunWith(JUnit4::class) +class UserInteractorRefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return true + } + + @Before + override fun setUp() { + super.setUp() + + overrideResource(R.drawable.ic_account_circle, GUEST_ICON) + overrideResource(R.dimen.max_avatar_size, 10) + overrideResource( + com.android.internal.R.string.config_supervisedUserCreationPackage, + SUPERVISED_USER_CREATION_APP_PACKAGE, + ) + whenever(manager.getUserIcon(anyInt())).thenReturn(ICON) + whenever(manager.canAddMoreUsers(any())).thenReturn(true) + } + + @Test + fun `users - switcher enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 3, includeGuest = true) + + job.cancel() + } + + @Test + fun `users - switches to second user`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + userRepository.setSelectedUserInfo(userInfos[1]) + + assertUsers(models = value, count = 2, selectedIndex = 1) + job.cancel() + } + + @Test + fun `users - switcher not enabled`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = false)) + + var value: List<UserModel>? = null + val job = underTest.users.onEach { value = it }.launchIn(this) + assertUsers(models = value, count = 1) + + job.cancel() + } + + @Test + fun selectedUser() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + + var value: UserModel? = null + val job = underTest.selectedUser.onEach { value = it }.launchIn(this) + assertUser(value, id = 0, isSelected = true) + + userRepository.setSelectedUserInfo(userInfos[1]) + assertUser(value, id = 1, isSelected = true) + + job.cancel() + } + + @Test + fun `actions - device unlocked`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device unlocked user not primary - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device unlocked user is guest - empty list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + assertThat(userInfos[1].isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[1]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(emptyList<UserActionModel>()) + + job.cancel() + } + + @Test + fun `actions - device locked add from lockscreen set - full list`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings( + UserSwitcherSettingsModel( + isUserSwitcherEnabled = true, + isAddUsersFromLockscreen = true, + ) + ) + keyguardRepository.setKeyguardShowing(false) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + + job.cancel() + } + + @Test + fun `actions - device locked - only guest action is shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + keyguardRepository.setKeyguardShowing(true) + var value: List<UserActionModel>? = null + val job = underTest.actions.onEach { value = it }.launchIn(this) + + assertThat(value).isEqualTo(listOf(UserActionModel.ENTER_GUEST_MODE)) + + job.cancel() + } + + @Test + fun `executeAction - add user - dialog shown`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.executeAction(UserActionModel.ADD_USER) + assertThat(dialogRequest) + .isEqualTo( + ShowDialogRequestModel.ShowAddUserDialog( + userHandle = userInfos[0].userHandle, + isKeyguardShowing = false, + showEphemeralMessage = false, + ) + ) + + underTest.onDialogShown() + assertThat(dialogRequest).isNull() + + job.cancel() + } + + @Test + fun `executeAction - add supervised user - starts activity`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action) + .isEqualTo(UserManager.ACTION_CREATE_SUPERVISED_USER) + assertThat(intentCaptor.value.`package`).isEqualTo(SUPERVISED_USER_CREATION_APP_PACKAGE) + } + + @Test + fun `executeAction - navigate to manage users`() = + runBlocking(IMMEDIATE) { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + val intentCaptor = kotlinArgumentCaptor<Intent>() + verify(activityStarter).startActivity(intentCaptor.capture(), eq(false)) + assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_USER_SETTINGS) + } + + @Test + fun `executeAction - guest mode`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val guestUserInfo = createUserInfo(id = 1337, name = "guest", isGuest = true) + whenever(manager.createGuest(any())).thenReturn(guestUserInfo) + val dialogRequests = mutableListOf<ShowDialogRequestModel?>() + val showDialogsJob = + underTest.dialogShowRequests + .onEach { + dialogRequests.add(it) + if (it != null) { + underTest.onDialogShown() + } + } + .launchIn(this) + val dismissDialogsJob = + underTest.dialogDismissRequests + .onEach { + if (it != null) { + underTest.onDialogDismissed() + } + } + .launchIn(this) + + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + assertThat(dialogRequests) + .contains( + ShowDialogRequestModel.ShowUserCreationDialog(isGuest = true), + ) + verify(activityManager).switchUser(guestUserInfo.id) + + showDialogsJob.cancel() + dismissDialogsJob.cancel() + } + + @Test + fun `selectUser - already selected guest re-selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = guestUserInfo.id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - currently guest non-guest selected - exit guest dialog`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = true) + val guestUserInfo = userInfos[1] + assertThat(guestUserInfo.isGuest).isTrue() + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(guestUserInfo) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[0].id) + + assertThat(dialogRequest) + .isInstanceOf(ShowDialogRequestModel.ShowExitGuestDialog::class.java) + job.cancel() + } + + @Test + fun `selectUser - not currently guest - switches users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + var dialogRequest: ShowDialogRequestModel? = null + val job = underTest.dialogShowRequests.onEach { dialogRequest = it }.launchIn(this) + + underTest.selectUser(newlySelectedUserId = userInfos[1].id) + + assertThat(dialogRequest).isNull() + verify(activityManager).switchUser(userInfos[1].id) + job.cancel() + } + + @Test + fun `Telephony call state changes - refreshes users`() = + runBlocking(IMMEDIATE) { + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + telephonyRepository.setCallState(1) + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User switched broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + val callback1: UserInteractor.UserCallback = mock() + val callback2: UserInteractor.UserCallback = mock() + underTest.addCallback(callback1) + underTest.addCallback(callback2) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + userRepository.setSelectedUserInfo(userInfos[1]) + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_SWITCHED) + .putExtra(Intent.EXTRA_USER_HANDLE, userInfos[1].id), + ) + } + + verify(callback1).onUserStateChanged() + verify(callback2).onUserStateChanged() + assertThat(userRepository.secondaryUserId).isEqualTo(userInfos[1].id) + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `User info changed broadcast`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_INFO_CHANGED), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `System user unlocked broadcast - refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) + } + + @Test + fun `Non-system user unlocked broadcast - do not refresh users`() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 2, includeGuest = false) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + val refreshUsersCallCount = userRepository.refreshUsersCallCount + + fakeBroadcastDispatcher.registeredReceivers.forEach { + it.onReceive( + context, + Intent(Intent.ACTION_USER_UNLOCKED).putExtra(Intent.EXTRA_USER_HANDLE, 1337), + ) + } + + assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount) + } + + @Test + fun userRecords() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = false) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + testCoroutineScope.advanceUntilIdle() + + assertRecords( + records = underTest.userRecords.value, + userIds = listOf(0, 1, 2), + selectedUserIndex = 0, + includeGuest = false, + expectedActions = + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ), + ) + } + + @Test + fun selectedUserRecord() = + runBlocking(IMMEDIATE) { + val userInfos = createUserInfos(count = 3, includeGuest = true) + userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) + userRepository.setUserInfos(userInfos) + userRepository.setSelectedUserInfo(userInfos[0]) + keyguardRepository.setKeyguardShowing(false) + + assertRecordForUser( + record = underTest.selectedUserRecord.value, + id = 0, + hasPicture = true, + isCurrent = true, + isSwitchToEnabled = true, + ) + } + + private fun assertUsers( + models: List<UserModel>?, + count: Int, + selectedIndex: Int = 0, + includeGuest: Boolean = false, + ) { + checkNotNull(models) + assertThat(models.size).isEqualTo(count) + models.forEachIndexed { index, model -> + assertUser( + model = model, + id = index, + isSelected = index == selectedIndex, + isGuest = includeGuest && index == count - 1 + ) + } + } + + private fun assertUser( + model: UserModel?, + id: Int, + isSelected: Boolean = false, + isGuest: Boolean = false, + ) { + checkNotNull(model) + assertThat(model.id).isEqualTo(id) + assertThat(model.name).isEqualTo(Text.Loaded(if (isGuest) "guest" else "user_$id")) + assertThat(model.isSelected).isEqualTo(isSelected) + assertThat(model.isSelectable).isTrue() + assertThat(model.isGuest).isEqualTo(isGuest) + } + + private fun assertRecords( + records: List<UserRecord>, + userIds: List<Int>, + selectedUserIndex: Int = 0, + includeGuest: Boolean = false, + expectedActions: List<UserActionModel> = emptyList(), + ) { + assertThat(records.size >= userIds.size).isTrue() + userIds.indices.forEach { userIndex -> + val record = records[userIndex] + assertThat(record.info).isNotNull() + val isGuest = includeGuest && userIndex == userIds.size - 1 + assertRecordForUser( + record = record, + id = userIds[userIndex], + hasPicture = !isGuest, + isCurrent = userIndex == selectedUserIndex, + isGuest = isGuest, + isSwitchToEnabled = true, + ) + } + + assertThat(records.size - userIds.size).isEqualTo(expectedActions.size) + (userIds.size until userIds.size + expectedActions.size).forEach { actionIndex -> + val record = records[actionIndex] + assertThat(record.info).isNull() + assertRecordForAction( + record = record, + type = expectedActions[actionIndex - userIds.size], + ) + } + } + + private fun assertRecordForUser( + record: UserRecord?, + id: Int? = null, + hasPicture: Boolean = false, + isCurrent: Boolean = false, + isGuest: Boolean = false, + isSwitchToEnabled: Boolean = false, + ) { + checkNotNull(record) + assertThat(record.info?.id).isEqualTo(id) + assertThat(record.picture != null).isEqualTo(hasPicture) + assertThat(record.isCurrent).isEqualTo(isCurrent) + assertThat(record.isGuest).isEqualTo(isGuest) + assertThat(record.isSwitchToEnabled).isEqualTo(isSwitchToEnabled) + } + + private fun assertRecordForAction( + record: UserRecord, + type: UserActionModel, + ) { + assertThat(record.isGuest).isEqualTo(type == UserActionModel.ENTER_GUEST_MODE) + assertThat(record.isAddUser).isEqualTo(type == UserActionModel.ADD_USER) + assertThat(record.isAddSupervisedUser) + .isEqualTo(type == UserActionModel.ADD_SUPERVISED_USER) + } + + private fun createUserInfos( + count: Int, + includeGuest: Boolean, + ): List<UserInfo> { + return (0 until count).map { index -> + val isGuest = includeGuest && index == count - 1 + createUserInfo( + id = index, + name = + if (isGuest) { + "guest" + } else { + "user_$index" + }, + isPrimary = !isGuest && index == 0, + isGuest = isGuest, + ) + } + } + + private fun createUserInfo( + id: Int, + name: String, + isPrimary: Boolean = false, + isGuest: Boolean = false, + ): UserInfo { + return UserInfo( + id, + name, + /* iconPath= */ "", + /* flags= */ if (isPrimary) { + UserInfo.FLAG_PRIMARY + } else { + 0 + }, + if (isGuest) { + UserManager.USER_TYPE_FULL_GUEST + } else { + UserManager.USER_TYPE_FULL_SYSTEM + }, + ) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private val ICON = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val GUEST_ICON: Drawable = mock() + private const val SUPERVISED_USER_CREATION_APP_PACKAGE = "supervisedUserCreation" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt index e914e2e0a1da..8465f4f46d62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt @@ -17,51 +17,61 @@ package com.android.systemui.user.domain.interactor -import androidx.test.filters.SmallTest +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.os.UserManager +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.shared.model.UserActionModel -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.nullable -import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import kotlinx.coroutines.test.TestCoroutineScope import org.mockito.Mock -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@SmallTest -@RunWith(JUnit4::class) -class UserInteractorTest : SysuiTestCase() { +abstract class UserInteractorTest : SysuiTestCase() { - @Mock private lateinit var controller: UserSwitcherController - @Mock private lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var controller: UserSwitcherController + @Mock protected lateinit var activityStarter: ActivityStarter + @Mock protected lateinit var manager: UserManager + @Mock protected lateinit var activityManager: ActivityManager + @Mock protected lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock protected lateinit var devicePolicyManager: DevicePolicyManager + @Mock protected lateinit var uiEventLogger: UiEventLogger - private lateinit var underTest: UserInteractor + protected lateinit var underTest: UserInteractor - private lateinit var userRepository: FakeUserRepository - private lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var testCoroutineScope: TestCoroutineScope + protected lateinit var userRepository: FakeUserRepository + protected lateinit var keyguardRepository: FakeKeyguardRepository + protected lateinit var telephonyRepository: FakeTelephonyRepository - @Before - fun setUp() { + abstract fun isRefactored(): Boolean + + open fun setUp() { MockitoAnnotations.initMocks(this) userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() + telephonyRepository = FakeTelephonyRepository() + testCoroutineScope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = testCoroutineScope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) underTest = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, @@ -69,142 +79,34 @@ class UserInteractorTest : SysuiTestCase() { KeyguardInteractor( repository = keyguardRepository, ), - ) - } - - @Test - fun `actions - not actionable when locked and locked - no actions`() = - runBlocking(IMMEDIATE) { - userRepository.setActions(UserActionModel.values().toList()) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions).isEmpty() - job.cancel() - } - - @Test - fun `actions - not actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(false) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and not locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) - ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(false) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + featureFlags = + FakeFeatureFlags().apply { + set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, !isRefactored()) + }, + manager = manager, + applicationScope = testCoroutineScope, + telephonyInteractor = + TelephonyInteractor( + repository = telephonyRepository, + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = testCoroutineScope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, ) - ) - job.cancel() - } - - @Test - fun `actions - actionable when locked and locked`() = - runBlocking(IMMEDIATE) { - userRepository.setActions( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - ) ) - userRepository.setActionableWhenLocked(true) - keyguardRepository.setKeyguardShowing(true) - - var actions: List<UserActionModel>? = null - val job = underTest.actions.onEach { actions = it }.launchIn(this) - - assertThat(actions) - .isEqualTo( - listOf( - UserActionModel.ENTER_GUEST_MODE, - UserActionModel.ADD_USER, - UserActionModel.ADD_SUPERVISED_USER, - UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, - ) - ) - job.cancel() - } - - @Test - fun selectUser() { - val userId = 3 - - underTest.selectUser(userId) - - verify(controller).onUserSelected(eq(userId), nullable()) - } - - @Test - fun `executeAction - guest`() { - underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) - - verify(controller).createAndSwitchToGuestUser(nullable()) - } - - @Test - fun `executeAction - add user`() { - underTest.executeAction(UserActionModel.ADD_USER) - - verify(controller).showAddUserDialog(nullable()) - } - - @Test - fun `executeAction - add supervised user`() { - underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) - - verify(controller).startSupervisedUserActivity() - } - - @Test - fun `executeAction - manage users`() { - underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) - - verify(activityStarter).startActivity(any(), anyBoolean()) } companion object { diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt new file mode 100644 index 000000000000..c3a9705bf6ba --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorUnrefactoredTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.user.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.user.shared.model.UserActionModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.nullable +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +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.verify + +@SmallTest +@RunWith(JUnit4::class) +open class UserInteractorUnrefactoredTest : UserInteractorTest() { + + override fun isRefactored(): Boolean { + return false + } + + @Before + override fun setUp() { + super.setUp() + } + + @Test + fun `actions - not actionable when locked and locked - no actions`() = + runBlocking(IMMEDIATE) { + userRepository.setActions(UserActionModel.values().toList()) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(true) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions).isEmpty() + job.cancel() + } + + @Test + fun `actions - not actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(false) + keyguardRepository.setKeyguardShowing(false) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and not locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(false) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun `actions - actionable when locked and locked`() = + runBlocking(IMMEDIATE) { + userRepository.setActions( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + ) + ) + userRepository.setActionableWhenLocked(true) + keyguardRepository.setKeyguardShowing(true) + + var actions: List<UserActionModel>? = null + val job = underTest.actions.onEach { actions = it }.launchIn(this) + + assertThat(actions) + .isEqualTo( + listOf( + UserActionModel.ENTER_GUEST_MODE, + UserActionModel.ADD_USER, + UserActionModel.ADD_SUPERVISED_USER, + UserActionModel.NAVIGATE_TO_USER_MANAGEMENT, + ) + ) + job.cancel() + } + + @Test + fun selectUser() { + val userId = 3 + + underTest.selectUser(userId) + + verify(controller).onUserSelected(eq(userId), nullable()) + } + + @Test + fun `executeAction - guest`() { + underTest.executeAction(UserActionModel.ENTER_GUEST_MODE) + + verify(controller).createAndSwitchToGuestUser(nullable()) + } + + @Test + fun `executeAction - add user`() { + underTest.executeAction(UserActionModel.ADD_USER) + + verify(controller).showAddUserDialog(nullable()) + } + + @Test + fun `executeAction - add supervised user`() { + underTest.executeAction(UserActionModel.ADD_SUPERVISED_USER) + + verify(controller).startSupervisedUserActivity() + } + + @Test + fun `executeAction - manage users`() { + underTest.executeAction(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) + + verify(activityStarter).startActivity(any(), anyBoolean()) + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt index ef4500df3600..0344e3f991e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt @@ -17,17 +17,28 @@ package com.android.systemui.user.ui.viewmodel +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager import android.graphics.drawable.Drawable +import android.os.UserManager import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Text +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.telephony.data.repository.FakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.TelephonyInteractor import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.domain.interactor.GuestUserInteractor +import com.android.systemui.user.domain.interactor.RefreshUsersScheduler import com.android.systemui.user.domain.interactor.UserInteractor import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper import com.android.systemui.user.shared.model.UserActionModel @@ -38,6 +49,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.yield import org.junit.Before import org.junit.Test @@ -52,6 +64,11 @@ class UserSwitcherViewModelTest : SysuiTestCase() { @Mock private lateinit var controller: UserSwitcherController @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var activityManager: ActivityManager + @Mock private lateinit var manager: UserManager + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var uiEventLogger: UiEventLogger private lateinit var underTest: UserSwitcherViewModel @@ -66,22 +83,60 @@ class UserSwitcherViewModelTest : SysuiTestCase() { userRepository = FakeUserRepository() keyguardRepository = FakeKeyguardRepository() powerRepository = FakePowerRepository() + val featureFlags = FakeFeatureFlags() + featureFlags.set(Flags.USER_INTERACTOR_AND_REPO_USE_CONTROLLER, true) + val scope = TestCoroutineScope() + val refreshUsersScheduler = + RefreshUsersScheduler( + applicationScope = scope, + mainDispatcher = IMMEDIATE, + repository = userRepository, + ) + val guestUserInteractor = + GuestUserInteractor( + applicationContext = context, + applicationScope = scope, + mainDispatcher = IMMEDIATE, + backgroundDispatcher = IMMEDIATE, + manager = manager, + repository = userRepository, + deviceProvisionedController = deviceProvisionedController, + devicePolicyManager = devicePolicyManager, + refreshUsersScheduler = refreshUsersScheduler, + uiEventLogger = uiEventLogger, + ) + underTest = UserSwitcherViewModel.Factory( userInteractor = UserInteractor( + applicationContext = context, repository = userRepository, controller = controller, activityStarter = activityStarter, keyguardInteractor = KeyguardInteractor( repository = keyguardRepository, - ) + ), + featureFlags = featureFlags, + manager = manager, + applicationScope = scope, + telephonyInteractor = + TelephonyInteractor( + repository = FakeTelephonyRepository(), + ), + broadcastDispatcher = fakeBroadcastDispatcher, + backgroundDispatcher = IMMEDIATE, + activityManager = activityManager, + refreshUsersScheduler = refreshUsersScheduler, + guestUserInteractor = guestUserInteractor, ), powerInteractor = PowerInteractor( repository = powerRepository, ), + featureFlags = featureFlags, + guestUserInteractor = guestUserInteractor, ) .create(UserSwitcherViewModel::class.java) } @@ -97,6 +152,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = true, isSelectable = true, + isGuest = false, ), UserModel( id = 1, @@ -104,6 +160,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = true, + isGuest = false, ), UserModel( id = 2, @@ -111,6 +168,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = false, isSelectable = false, + isGuest = false, ), ) ) @@ -260,7 +318,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { job.cancel() } - private fun setUsers(count: Int) { + private suspend fun setUsers(count: Int) { userRepository.setUsers( (0 until count).map { index -> UserModel( @@ -269,6 +327,7 @@ class UserSwitcherViewModelTest : SysuiTestCase() { image = USER_IMAGE, isSelected = index == 0, isSelectable = true, + isGuest = false, ) } ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt index 53dcc8d269c9..bb646f09b774 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt @@ -37,10 +37,18 @@ class FakeBroadcastDispatcher( dumpManager: DumpManager, logger: BroadcastDispatcherLogger, userTracker: UserTracker -) : BroadcastDispatcher( - context, looper, executor, dumpManager, logger, userTracker, PendingRemovalStore(logger)) { +) : + BroadcastDispatcher( + context, + looper, + executor, + dumpManager, + logger, + userTracker, + PendingRemovalStore(logger) + ) { - private val registeredReceivers = ArraySet<BroadcastReceiver>() + val registeredReceivers = ArraySet<BroadcastReceiver>() override fun registerReceiverWithHandler( receiver: BroadcastReceiver, @@ -78,4 +86,4 @@ class FakeBroadcastDispatcher( } registeredReceivers.clear() } -}
\ 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 42b434a9deaf..725b1f41372c 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 @@ -44,6 +44,10 @@ class FakeKeyguardRepository : KeyguardRepository { private val _dozeAmount = MutableStateFlow(0f) override val dozeAmount: Flow<Float> = _dozeAmount + override fun isKeyguardShowing(): Boolean { + return _isKeyguardShowing.value + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.tryEmit(animate) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt index b2b176420e40..9726bf83b263 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt @@ -26,20 +26,24 @@ import java.util.concurrent.Executor /** A fake [UserTracker] to be used in tests. */ class FakeUserTracker( - userId: Int = 0, - userHandle: UserHandle = UserHandle.of(userId), - userInfo: UserInfo = mock(), - userProfiles: List<UserInfo> = emptyList(), + private var _userId: Int = 0, + private var _userHandle: UserHandle = UserHandle.of(_userId), + private var _userInfo: UserInfo = mock(), + private var _userProfiles: List<UserInfo> = emptyList(), userContentResolver: ContentResolver = MockContentResolver(), userContext: Context = mock(), private val onCreateCurrentUserContext: (Context) -> Context = { mock() }, ) : UserTracker { val callbacks = mutableListOf<UserTracker.Callback>() - override val userId: Int = userId - override val userHandle: UserHandle = userHandle - override val userInfo: UserInfo = userInfo - override val userProfiles: List<UserInfo> = userProfiles + override val userId: Int + get() = _userId + override val userHandle: UserHandle + get() = _userHandle + override val userInfo: UserInfo + get() = _userInfo + override val userProfiles: List<UserInfo> + get() = _userProfiles override val userContentResolver: ContentResolver = userContentResolver override val userContext: Context = userContext @@ -55,4 +59,13 @@ class FakeUserTracker( override fun createCurrentUserContext(context: Context): Context { return onCreateCurrentUserContext(context) } + + fun set(userInfos: List<UserInfo>, selectedUserIndex: Int) { + _userProfiles = userInfos + _userInfo = userInfos[selectedUserIndex] + _userId = _userInfo.id + _userHandle = UserHandle.of(_userId) + + callbacks.forEach { it.onUserChanged(_userId, userContext) } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt new file mode 100644 index 000000000000..59f24ef2a706 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/telephony/data/repository/FakeTelephonyRepository.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.telephony.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeTelephonyRepository : TelephonyRepository { + + private val _callState = MutableStateFlow(0) + override val callState: Flow<Int> = _callState.asStateFlow() + + fun setCallState(value: Int) { + _callState.value = value + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 20f1e367944f..4df8aa42ea2f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -17,12 +17,18 @@ package com.android.systemui.user.data.repository +import android.content.pm.UserInfo +import android.os.UserHandle +import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.shared.model.UserActionModel import com.android.systemui.user.shared.model.UserModel +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.yield class FakeUserRepository : UserRepository { @@ -34,21 +40,71 @@ class FakeUserRepository : UserRepository { private val _actions = MutableStateFlow<List<UserActionModel>>(emptyList()) override val actions: Flow<List<UserActionModel>> = _actions.asStateFlow() + private val _userSwitcherSettings = MutableStateFlow(UserSwitcherSettingsModel()) + override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = + _userSwitcherSettings.asStateFlow() + + private val _userInfos = MutableStateFlow<List<UserInfo>>(emptyList()) + override val userInfos: Flow<List<UserInfo>> = _userInfos.asStateFlow() + + private val _selectedUserInfo = MutableStateFlow<UserInfo?>(null) + override val selectedUserInfo: Flow<UserInfo> = _selectedUserInfo.filterNotNull() + + override var lastSelectedNonGuestUserId: Int = UserHandle.USER_SYSTEM + private val _isActionableWhenLocked = MutableStateFlow(false) override val isActionableWhenLocked: Flow<Boolean> = _isActionableWhenLocked.asStateFlow() private var _isGuestUserAutoCreated: Boolean = false override val isGuestUserAutoCreated: Boolean get() = _isGuestUserAutoCreated - private var _isGuestUserResetting: Boolean = false - override val isGuestUserResetting: Boolean - get() = _isGuestUserResetting + + override var isGuestUserResetting: Boolean = false + + override val isGuestUserCreationScheduled = AtomicBoolean() + + override var secondaryUserId: Int = UserHandle.USER_NULL + + override var isRefreshUsersPaused: Boolean = false + + var refreshUsersCallCount: Int = 0 + private set + + override fun refreshUsers() { + refreshUsersCallCount++ + } + + override fun getSelectedUserInfo(): UserInfo { + return checkNotNull(_selectedUserInfo.value) + } + + override fun isSimpleUserSwitcher(): Boolean { + return _userSwitcherSettings.value.isSimpleUserSwitcher + } + + fun setUserInfos(infos: List<UserInfo>) { + _userInfos.value = infos + } + + suspend fun setSelectedUserInfo(userInfo: UserInfo) { + check(_userInfos.value.contains(userInfo)) { + "Cannot select the following user, it is not in the list of user infos: $userInfo!" + } + + _selectedUserInfo.value = userInfo + yield() + } + + suspend fun setSettings(settings: UserSwitcherSettingsModel) { + _userSwitcherSettings.value = settings + yield() + } fun setUsers(models: List<UserModel>) { _users.value = models } - fun setSelectedUser(userId: Int) { + suspend fun setSelectedUser(userId: Int) { check(_users.value.find { it.id == userId } != null) { "Cannot select a user with ID $userId - no user with that ID found!" } @@ -62,6 +118,7 @@ class FakeUserRepository : UserRepository { } } ) + yield() } fun setActions(models: List<UserActionModel>) { @@ -75,8 +132,4 @@ class FakeUserRepository : UserRepository { fun setGuestUserAutoCreated(value: Boolean) { _isGuestUserAutoCreated = value } - - fun setGuestUserResetting(value: Boolean) { - _isGuestUserResetting = value - } } diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 56a5a8f829b7..0426c79358ba 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -1175,6 +1175,9 @@ public class CompanionDeviceManagerService extends SystemService { } private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) { + if (packageInfo == null) { + return; + } if (containsEither(packageInfo.requestedPermissions, android.Manifest.permission.RUN_IN_BACKGROUND, android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java index 9bad45bd82f6..3ab4aa89406b 100644 --- a/services/companion/java/com/android/server/companion/PackageUtils.java +++ b/services/companion/java/com/android/server/companion/PackageUtils.java @@ -54,12 +54,19 @@ final class PackageUtils { private static final String PROPERTY_PRIMARY_TAG = "android.companion.PROPERTY_PRIMARY_COMPANION_DEVICE_SERVICE"; - static @Nullable PackageInfo getPackageInfo(@NonNull Context context, + @Nullable + static PackageInfo getPackageInfo(@NonNull Context context, @UserIdInt int userId, @NonNull String packageName) { final PackageManager pm = context.getPackageManager(); final PackageInfoFlags flags = PackageInfoFlags.of(GET_PERMISSIONS | GET_CONFIGURATIONS); - return Binder.withCleanCallingIdentity(() -> - pm.getPackageInfoAsUser(packageName, flags , userId)); + return Binder.withCleanCallingIdentity(() -> { + try { + return pm.getPackageInfoAsUser(packageName, flags, userId); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "Package [" + packageName + "] is not found."); + return null; + } + }); } static void enforceUsesCompanionDeviceFeature(@NonNull Context context, diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 4204162f3d98..5f27f598ee4e 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -95,6 +95,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final AssociationInfo mAssociationInfo; private final PendingTrampolineCallback mPendingTrampolineCallback; private final int mOwnerUid; + private final int mDeviceId; private final InputController mInputController; private VirtualAudioController mVirtualAudioController; @VisibleForTesting @@ -140,19 +141,40 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final SparseArray<GenericWindowPolicyController> mWindowPolicyControllers = new SparseArray<>(); - VirtualDeviceImpl(Context context, AssociationInfo associationInfo, - IBinder token, int ownerUid, OnDeviceCloseListener listener, + VirtualDeviceImpl( + Context context, + AssociationInfo associationInfo, + IBinder token, + int ownerUid, + int deviceId, + OnDeviceCloseListener listener, PendingTrampolineCallback pendingTrampolineCallback, IVirtualDeviceActivityListener activityListener, Consumer<ArraySet<Integer>> runningAppsChangedCallback, VirtualDeviceParams params) { - this(context, associationInfo, token, ownerUid, /* inputController= */ null, listener, - pendingTrampolineCallback, activityListener, runningAppsChangedCallback, params); + this( + context, + associationInfo, + token, + ownerUid, + deviceId, + /* inputController= */ null, + listener, + pendingTrampolineCallback, + activityListener, + runningAppsChangedCallback, + params); } @VisibleForTesting - VirtualDeviceImpl(Context context, AssociationInfo associationInfo, IBinder token, - int ownerUid, InputController inputController, OnDeviceCloseListener listener, + VirtualDeviceImpl( + Context context, + AssociationInfo associationInfo, + IBinder token, + int ownerUid, + int deviceId, + InputController inputController, + OnDeviceCloseListener listener, PendingTrampolineCallback pendingTrampolineCallback, IVirtualDeviceActivityListener activityListener, Consumer<ArraySet<Integer>> runningAppsChangedCallback, @@ -164,6 +186,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mActivityListener = activityListener; mRunningAppsChangedCallback = runningAppsChangedCallback; mOwnerUid = ownerUid; + mDeviceId = deviceId; mAppToken = token; mParams = params; if (inputController == null) { @@ -199,6 +222,12 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return mAssociationInfo.getDisplayName(); } + /** Returns the unique device ID of this device. */ + @Override // Binder call + public int getDeviceId() { + return mDeviceId; + } + @Override // Binder call public int getAssociationId() { return mAssociationInfo.getId(); diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 2b644fe8f7ba..06dfeabfe832 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -60,12 +60,12 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; @SuppressLint("LongLogTag") public class VirtualDeviceManagerService extends SystemService { - private static final boolean DEBUG = false; private static final String TAG = "VirtualDeviceManagerService"; private final Object mVirtualDeviceManagerLock = new Object(); @@ -73,6 +73,10 @@ public class VirtualDeviceManagerService extends SystemService { private final VirtualDeviceManagerInternal mLocalService; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler); + + private static AtomicInteger sNextUniqueIndex = new AtomicInteger( + VirtualDeviceManager.DEFAULT_DEVICE_ID + 1); + /** * Mapping from user IDs to CameraAccessControllers. */ @@ -260,8 +264,10 @@ public class VirtualDeviceManagerService extends SystemService { final int userId = UserHandle.getUserId(callingUid); final CameraAccessController cameraAccessController = mCameraAccessControllers.get(userId); + final int uniqueId = sNextUniqueIndex.getAndIncrement(); + VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(), - associationInfo, token, callingUid, + associationInfo, token, callingUid, uniqueId, new VirtualDeviceImpl.OnDeviceCloseListener() { @Override public void onClose(int associationId) { diff --git a/services/core/java/com/android/server/AlarmManagerInternal.java b/services/core/java/com/android/server/AlarmManagerInternal.java index b2abdbd144b8..67aa2b9a89bb 100644 --- a/services/core/java/com/android/server/AlarmManagerInternal.java +++ b/services/core/java/com/android/server/AlarmManagerInternal.java @@ -16,8 +16,10 @@ package com.android.server; +import android.annotation.CurrentTimeMillisLong; import android.app.PendingIntent; +import com.android.server.SystemClockTime.TimeConfidence; import com.android.server.SystemTimeZone.TimeZoneConfidence; public interface AlarmManagerInternal { @@ -59,4 +61,15 @@ public interface AlarmManagerInternal { * for details */ void setTimeZone(String tzId, @TimeZoneConfidence int confidence); + + /** + * Sets the device's current time and time confidence. + * + * @param unixEpochTimeMillis the time + * @param confidence the confidence that {@code unixEpochTimeMillis} is correct, see {@link + * TimeConfidence} for details + * @param logMsg the reason the time is being changed, for bug report logging + */ + void setTime(@CurrentTimeMillisLong long unixEpochTimeMillis, @TimeConfidence int confidence, + String logMsg); } diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 5b1f7409f439..83d527e1d68b 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -147,7 +147,6 @@ import com.android.internal.util.DumpUtils; import com.android.internal.util.HexDump; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; -import com.android.internal.widget.LockPatternUtils; import com.android.server.pm.Installer; import com.android.server.pm.UserManagerInternal; import com.android.server.storage.AppFuseBridge; @@ -557,7 +556,6 @@ class StorageManagerService extends IStorageManager.Stub private IAppOpsService mIAppOpsService; private final Callbacks mCallbacks; - private final LockPatternUtils mLockPatternUtils; private static final String ANR_DELAY_MILLIS_DEVICE_CONFIG_KEY = "anr_delay_millis"; @@ -1808,7 +1806,6 @@ class StorageManagerService extends IStorageManager.Stub ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false); mContext = context; mCallbacks = new Callbacks(FgThread.get().getLooper()); - mLockPatternUtils = new LockPatternUtils(mContext); HandlerThread hthread = new HandlerThread(TAG); hthread.start(); @@ -3069,96 +3066,21 @@ class StorageManagerService extends IStorageManager.Stub } } - private String encodeBytes(byte[] bytes) { - if (ArrayUtils.isEmpty(bytes)) { - return "!"; - } else { - return HexDump.toHexString(bytes); - } - } - + /* Only for use by LockSettingsService */ @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL) - /* - * Add this secret to the set of ways we can recover a user's disk - * encryption key. Changing the secret for a disk encryption key is done in - * two phases. First, this method is called to add the new secret binding. - * Second, fixateNewestUserKeyAuth is called to delete all other bindings. - * This allows other places where a credential is used, such as Gatekeeper, - * to be updated between the two calls. - */ @Override - public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) { - - try { - mVold.addUserKeyAuth(userId, serialNumber, encodeBytes(secret)); - } catch (Exception e) { - Slog.wtf(TAG, e); - } + public void setUserKeyProtection(@UserIdInt int userId, byte[] secret) throws RemoteException { + mVold.setUserKeyProtection(userId, HexDump.toHexString(secret)); } + /* Only for use by LockSettingsService */ @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL) - /* - * Store a user's disk encryption key without secret binding. Removing the - * secret for a disk encryption key is done in two phases. First, this - * method is called to retrieve the key using the provided secret and store - * it encrypted with a keystore key not bound to the user. Second, - * fixateNewestUserKeyAuth is called to delete the key's other bindings. - */ @Override - public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) { - - try { - mVold.clearUserKeyAuth(userId, serialNumber, encodeBytes(secret)); - } catch (Exception e) { - Slog.wtf(TAG, e); + public void unlockUserKey(@UserIdInt int userId, int serialNumber, byte[] secret) + throws RemoteException { + if (StorageManager.isFileEncrypted()) { + mVold.unlockUserKey(userId, serialNumber, HexDump.toHexString(secret)); } - } - - @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL) - /* - * Delete all bindings of a user's disk encryption key except the most - * recently added one. - */ - @Override - public void fixateNewestUserKeyAuth(int userId) { - - try { - mVold.fixateNewestUserKeyAuth(userId); - } catch (Exception e) { - Slog.wtf(TAG, e); - } - } - - @Override - public void unlockUserKey(int userId, int serialNumber, byte[] secret) { - boolean isFileEncrypted = StorageManager.isFileEncrypted(); - Slog.d(TAG, "unlockUserKey: " + userId - + " isFileEncrypted: " + isFileEncrypted - + " hasSecret: " + (secret != null)); - enforcePermission(android.Manifest.permission.STORAGE_INTERNAL); - - if (isUserKeyUnlocked(userId)) { - Slog.d(TAG, "User " + userId + "'s CE storage is already unlocked"); - return; - } - - if (isFileEncrypted) { - // When a user has a secure lock screen, a secret is required to - // unlock the key, so don't bother trying to unlock it without one. - // This prevents misleading error messages from being logged. - if (mLockPatternUtils.isSecure(userId) && ArrayUtils.isEmpty(secret)) { - Slog.d(TAG, "Not unlocking user " + userId - + "'s CE storage yet because a secret is needed"); - return; - } - try { - mVold.unlockUserKey(userId, serialNumber, encodeBytes(secret)); - } catch (Exception e) { - Slog.wtf(TAG, e); - return; - } - } - synchronized (mLock) { mLocalUnlockedUsers.append(userId); } diff --git a/services/core/java/com/android/server/SystemClockTime.java b/services/core/java/com/android/server/SystemClockTime.java new file mode 100644 index 000000000000..46fbbb290ed2 --- /dev/null +++ b/services/core/java/com/android/server/SystemClockTime.java @@ -0,0 +1,174 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server; + +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.CurrentTimeMillisLong; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.Build; +import android.os.Environment; +import android.os.SystemProperties; +import android.util.LocalLog; +import android.util.Slog; + +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A set of static methods that encapsulate knowledge of how the system clock time and associated + * metadata are stored on Android. + */ +public final class SystemClockTime { + + private static final String TAG = "SystemClockTime"; + + /** + * A log that records the decisions / decision metadata that affected the device's system clock + * time. This is logged in bug reports to assist with debugging issues with time. + */ + @NonNull + private static final LocalLog sTimeDebugLog = + new LocalLog(30, false /* useLocalTimestamps */); + + + /** + * An annotation that indicates a "time confidence" value is expected. + * + * <p>The confidence indicates whether the time is expected to be correct. The confidence can be + * upgraded or downgraded over time. It can be used to decide whether a user could / should be + * asked to confirm the time. For example, during device set up low confidence would describe a + * time that has been initialized by default. The user may then be asked to confirm the time, + * moving it to a high confidence. + */ + @Retention(SOURCE) + @Target(TYPE_USE) + @IntDef(prefix = "TIME_CONFIDENCE_", + value = { TIME_CONFIDENCE_LOW, TIME_CONFIDENCE_HIGH }) + public @interface TimeConfidence { + } + + /** Used when confidence is low and would (ideally) be confirmed by a user. */ + public static final @TimeConfidence int TIME_CONFIDENCE_LOW = 0; + + /** + * Used when confidence in the time is high and does not need to be confirmed by a user. + */ + public static final @TimeConfidence int TIME_CONFIDENCE_HIGH = 100; + + /** + * The confidence in the current time. Android's time confidence is held in memory because RTC + * hardware can forget / corrupt the time while the device is powered off. Therefore, on boot + * we can't assume the time is good, and so default it to "low" confidence until it is confirmed + * or explicitly set. + */ + private static @TimeConfidence int sTimeConfidence = TIME_CONFIDENCE_LOW; + + private static final long sNativeData = init(); + + private SystemClockTime() { + } + + /** + * Sets the system clock time to a reasonable lower bound. Used during boot-up to ensure the + * device has a time that is better than a default like 1970-01-01. + */ + public static void initializeIfRequired() { + // Use the most recent of Build.TIME, the root file system's timestamp, and the + // value of the ro.build.date.utc system property (which is in seconds). + final long systemBuildTime = Long.max( + 1000L * SystemProperties.getLong("ro.build.date.utc", -1L), + Long.max(Environment.getRootDirectory().lastModified(), Build.TIME)); + long currentTimeMillis = getCurrentTimeMillis(); + if (currentTimeMillis < systemBuildTime) { + String logMsg = "Current time only " + currentTimeMillis + + ", advancing to build time " + systemBuildTime; + Slog.i(TAG, logMsg); + setTimeAndConfidence(systemBuildTime, TIME_CONFIDENCE_LOW, logMsg); + } + } + + /** + * Sets the system clock time and confidence. See also {@link #setConfidence(int, String)} for + * an alternative that only sets the confidence. + * + * @param unixEpochMillis the time to set + * @param confidence the confidence in {@code unixEpochMillis}. See {@link TimeConfidence} for + * details. + * @param logMsg a log message that can be included in bug reports that explains the update + */ + public static void setTimeAndConfidence( + @CurrentTimeMillisLong long unixEpochMillis, int confidence, @NonNull String logMsg) { + synchronized (SystemClockTime.class) { + setTime(sNativeData, unixEpochMillis); + sTimeConfidence = confidence; + sTimeDebugLog.log(logMsg); + } + } + + /** + * Sets the system clock confidence. See also {@link #setTimeAndConfidence(long, int, String)} + * for an alternative that sets the time and confidence. + * + * @param confidence the confidence in the system clock time. See {@link TimeConfidence} for + * details. + * @param logMsg a log message that can be included in bug reports that explains the update + */ + public static void setConfidence(@TimeConfidence int confidence, @NonNull String logMsg) { + synchronized (SystemClockTime.class) { + sTimeConfidence = confidence; + sTimeDebugLog.log(logMsg); + } + } + + /** + * Returns the system clock time. The same as {@link System#currentTimeMillis()}. + */ + private static @CurrentTimeMillisLong long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } + + /** + * Returns the system clock confidence. See {@link TimeConfidence} for details. + */ + public static @TimeConfidence int getTimeConfidence() { + synchronized (SystemClockTime.class) { + return sTimeConfidence; + } + } + + /** + * Adds an entry to the system time debug log that is included in bug reports. This method is + * intended to be used to record event that may lead to a time change, e.g. config or mode + * changes. + */ + public static void addDebugLogEntry(@NonNull String logMsg) { + sTimeDebugLog.log(logMsg); + } + + /** + * Dumps information about recent time / confidence changes to the supplied writer. + */ + public static void dump(PrintWriter writer) { + sTimeDebugLog.dump(writer); + } + + private static native long init(); + private static native int setTime(long nativeData, @CurrentTimeMillisLong long millis); +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 481672e64d06..c4ebc9db6110 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -247,6 +247,7 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.PermissionInfo; import android.content.pm.PermissionMethod; +import android.content.pm.PermissionName; import android.content.pm.ProcessInfo; import android.content.pm.ProviderInfo; import android.content.pm.ProviderInfoList; @@ -5987,8 +5988,9 @@ public class ActivityManagerService extends IActivityManager.Stub * provided non-{@code null} {@code permission} before. Otherwise calls into * {@link ActivityManager#checkComponentPermission(String, int, int, boolean)}. */ + @PackageManager.PermissionResult @PermissionMethod - public static int checkComponentPermission(String permission, int pid, int uid, + public static int checkComponentPermission(@PermissionName String permission, int pid, int uid, int owningUid, boolean exported) { if (pid == MY_PID) { return PackageManager.PERMISSION_GRANTED; @@ -6034,8 +6036,9 @@ public class ActivityManagerService extends IActivityManager.Stub * This can be called with or without the global lock held. */ @Override + @PackageManager.PermissionResult @PermissionMethod - public int checkPermission(String permission, int pid, int uid) { + public int checkPermission(@PermissionName String permission, int pid, int uid) { if (permission == null) { return PackageManager.PERMISSION_DENIED; } @@ -6046,8 +6049,9 @@ public class ActivityManagerService extends IActivityManager.Stub * Binder IPC calls go through the public entry point. * This can be called with or without the global lock held. */ + @PackageManager.PermissionResult @PermissionMethod - int checkCallingPermission(String permission) { + int checkCallingPermission(@PermissionName String permission) { return checkPermission(permission, Binder.getCallingPid(), Binder.getCallingUid()); @@ -6057,7 +6061,7 @@ public class ActivityManagerService extends IActivityManager.Stub * This can be called with or without the global lock held. */ @PermissionMethod - void enforceCallingPermission(String permission, String func) { + void enforceCallingPermission(@PermissionName String permission, String func) { if (checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) { return; @@ -6074,7 +6078,6 @@ public class ActivityManagerService extends IActivityManager.Stub /** * This can be called with or without the global lock held. */ - @PermissionMethod private void enforceCallingHasAtLeastOnePermission(String func, String... permissions) { for (String permission : permissions) { if (checkCallingPermission(permission) == PackageManager.PERMISSION_GRANTED) { @@ -6093,7 +6096,8 @@ public class ActivityManagerService extends IActivityManager.Stub /** * This can be called with or without the global lock held. */ - void enforcePermission(String permission, int pid, int uid, String func) { + @PermissionMethod + void enforcePermission(@PermissionName String permission, int pid, int uid, String func) { if (checkPermission(permission, pid, uid) == PackageManager.PERMISSION_GRANTED) { return; } @@ -16399,23 +16403,17 @@ public class ActivityManagerService extends IActivityManager.Stub return mInjector.getSecondaryDisplayIdsForStartingBackgroundUsers(); } - /** - * Unlocks the given user. - * - * @param userId The ID of the user to unlock. - * @param token No longer used. (This parameter cannot be removed because - * this method is marked with UnsupportedAppUsage, so its - * signature might not be safe to change.) - * @param secret The secret needed to unlock the user's credential-encrypted - * storage, or null if no secret is needed. - * @param listener An optional progress listener. - * - * @return true if the user was successfully unlocked, otherwise false. - */ + /** @deprecated see the AIDL documentation {@inheritDoc} */ + @Override + @Deprecated + public boolean unlockUser(@UserIdInt int userId, @Nullable byte[] token, + @Nullable byte[] secret, @Nullable IProgressListener listener) { + return mUserController.unlockUser(userId, listener); + } + @Override - public boolean unlockUser(int userId, @Nullable byte[] token, @Nullable byte[] secret, - @Nullable IProgressListener listener) { - return mUserController.unlockUser(userId, secret, listener); + public boolean unlockUser2(@UserIdInt int userId, @Nullable IProgressListener listener) { + return mUserController.unlockUser(userId, listener); } @Override diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 10e2aae9a8d1..e4f947da868e 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -2119,7 +2119,7 @@ final class ActivityManagerShellCommand extends ShellCommand { return -1; } - boolean success = mInterface.unlockUser(userId, null, null, null); + boolean success = mInterface.unlockUser2(userId, null); if (success) { pw.println("Success: user unlocked"); } else { diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 82fb1e816888..226c63862226 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -654,7 +654,7 @@ class UserController implements Handler.Callback { EventLog.writeEvent(EventLogTags.UC_FINISH_USER_UNLOCKING, userId); logUserLifecycleEvent(userId, USER_LIFECYCLE_EVENT_UNLOCKING_USER, USER_LIFECYCLE_EVENT_STATE_BEGIN); - // Only keep marching forward if user is actually unlocked + // If the user key hasn't been unlocked yet, we cannot proceed. if (!StorageManager.isUserKeyUnlocked(userId)) return false; synchronized (mLock) { // Do not proceed if unexpected state or a stale user @@ -1776,28 +1776,19 @@ class UserController implements Handler.Callback { } } - boolean unlockUser(final @UserIdInt int userId, byte[] secret, IProgressListener listener) { + boolean unlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) { checkCallingPermission(INTERACT_ACROSS_USERS_FULL, "unlockUser"); EventLog.writeEvent(EventLogTags.UC_UNLOCK_USER, userId); final long binderToken = Binder.clearCallingIdentity(); try { - return unlockUserCleared(userId, secret, listener); + return maybeUnlockUser(userId, listener); } finally { Binder.restoreCallingIdentity(binderToken); } } - /** - * Attempt to unlock user without a secret. This typically succeeds when the - * device doesn't have credential-encrypted storage, or when the - * credential-encrypted storage isn't tied to a user-provided PIN or - * pattern. - */ - private boolean maybeUnlockUser(final @UserIdInt int userId) { - return unlockUserCleared(userId, null, null); - } - - private static void notifyFinished(@UserIdInt int userId, IProgressListener listener) { + private static void notifyFinished(@UserIdInt int userId, + @Nullable IProgressListener listener) { if (listener == null) return; try { listener.onFinished(userId, null); @@ -1805,8 +1796,18 @@ class UserController implements Handler.Callback { } } - private boolean unlockUserCleared(final @UserIdInt int userId, byte[] secret, - IProgressListener listener) { + private boolean maybeUnlockUser(@UserIdInt int userId) { + return maybeUnlockUser(userId, null); + } + + /** + * Tries to unlock the given user. + * <p> + * This will succeed only if the user's CE storage key is already unlocked or if the user + * doesn't have a lockscreen credential set. + */ + private boolean maybeUnlockUser(@UserIdInt int userId, @Nullable IProgressListener listener) { + // Delay user unlocking for headless system user mode until the system boot // completes. When the system boot completes, the {@link #onBootCompleted()} // method unlocks all started users for headless system user mode. This is done @@ -1825,14 +1826,8 @@ class UserController implements Handler.Callback { UserState uss; if (!StorageManager.isUserKeyUnlocked(userId)) { - final UserInfo userInfo = getUserInfo(userId); - final IStorageManager storageManager = mInjector.getStorageManager(); - try { - // We always want to unlock user storage, even user is not started yet - storageManager.unlockUserKey(userId, userInfo.serialNumber, secret); - } catch (RemoteException | RuntimeException e) { - Slogf.w(TAG, "Failed to unlock: " + e.getMessage()); - } + // We always want to try to unlock the user key, even if the user is not started yet. + mLockPatternUtils.unlockUserKeyIfUnsecured(userId); } synchronized (mLock) { // Register the given listener to watch for unlock progress diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthResult.java b/services/core/java/com/android/server/biometrics/sensors/AuthResult.java new file mode 100644 index 000000000000..c0ebf6b8b634 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/AuthResult.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import android.hardware.biometrics.BiometricManager; + +class AuthResult { + static final int FAILED = 0; + static final int LOCKED_OUT = 1; + static final int AUTHENTICATED = 2; + private final int mStatus; + private final int mBiometricStrength; + + AuthResult(int status, @BiometricManager.Authenticators.Types int strength) { + mStatus = status; + mBiometricStrength = strength; + } + + int getStatus() { + return mStatus; + } + + int getBiometricStrength() { + return mBiometricStrength; + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java new file mode 100644 index 000000000000..6d00c3fc15ea --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/AuthResultCoordinator.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import android.hardware.biometrics.BiometricManager.Authenticators; + +import java.util.ArrayList; +import java.util.List; + +/** + * A class that takes in a series of authentication attempts (successes, failures, lockouts) + * across different biometric strengths (convenience, weak, strong) and returns a single AuthResult. + * + * The AuthResult will be the strongest biometric operation that occurred amongst all reported + * operations, and if multiple such operations exist, it will favor a successful authentication. + */ +class AuthResultCoordinator { + + private static final String TAG = "AuthResultCoordinator"; + private final List<AuthResult> mOperations; + + AuthResultCoordinator() { + mOperations = new ArrayList<>(); + } + + /** + * Adds auth success for a given strength to the current operation list. + */ + void authenticatedFor(@Authenticators.Types int strength) { + mOperations.add(new AuthResult(AuthResult.AUTHENTICATED, strength)); + } + + /** + * Adds auth ended for a given strength to the current operation list. + */ + void authEndedFor(@Authenticators.Types int strength) { + mOperations.add(new AuthResult(AuthResult.FAILED, strength)); + } + + /** + * Adds a lock out of a given strength to the current operation list. + */ + void lockedOutFor(@Authenticators.Types int strength) { + mOperations.add(new AuthResult(AuthResult.LOCKED_OUT, strength)); + } + + /** + * Obtains an auth result & strength from a current set of biometric operations. + */ + AuthResult getResult() { + AuthResult result = new AuthResult(AuthResult.FAILED, Authenticators.BIOMETRIC_CONVENIENCE); + return mOperations.stream().filter( + (element) -> element.getStatus() != AuthResult.FAILED).reduce(result, + ((curr, next) -> { + int strengthCompare = curr.getBiometricStrength() - next.getBiometricStrength(); + if (strengthCompare < 0) { + return curr; + } else if (strengthCompare == 0) { + // Equal level of strength, favor authentication. + if (curr.getStatus() == AuthResult.AUTHENTICATED) { + return curr; + } else { + // Either next is Authenticated, or it is not, either way return this + // one. + return next; + } + } else { + // curr is a weaker biometric + return next; + } + })); + } + + void resetState() { + mOperations.clear(); + } +} + + diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java new file mode 100644 index 000000000000..13840ff389d9 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionCoordinator.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import android.hardware.biometrics.BiometricManager.Authenticators; +import android.util.Slog; + +import java.util.HashSet; +import java.util.Set; + +/** + * Coordinates lockout counter enforcement for all types of biometric strengths across all users. + * + * This class is not thread-safe. In general, all calls to this class should be made on the same + * handler to ensure no collisions. + */ +class AuthSessionCoordinator implements AuthSessionListener { + private static final String TAG = "AuthSessionCoordinator"; + + private final Set<Integer> mAuthOperations; + + private int mUserId; + private boolean mIsAuthenticating; + private AuthResultCoordinator mAuthResultCoordinator; + private MultiBiometricLockoutState mMultiBiometricLockoutState; + + AuthSessionCoordinator() { + mAuthOperations = new HashSet<>(); + mAuthResultCoordinator = new AuthResultCoordinator(); + mMultiBiometricLockoutState = new MultiBiometricLockoutState(); + } + + /** + * A Call indicating that an auth session has started + */ + void onAuthSessionStarted(int userId) { + mAuthOperations.clear(); + mUserId = userId; + mIsAuthenticating = true; + mAuthResultCoordinator.resetState(); + } + + /** + * Ends the current auth session and updates the lockout state. + * + * This can happen two ways. + * 1. Manually calling this API + * 2. If authStartedFor() was called, and all authentication attempts finish. + */ + void endAuthSession() { + if (mIsAuthenticating) { + mAuthOperations.clear(); + AuthResult res = + mAuthResultCoordinator.getResult(); + if (res.getStatus() == AuthResult.AUTHENTICATED) { + mMultiBiometricLockoutState.onUserUnlocked(mUserId, res.getBiometricStrength()); + } else if (res.getStatus() == AuthResult.LOCKED_OUT) { + mMultiBiometricLockoutState.onUserLocked(mUserId, res.getBiometricStrength()); + } + mAuthResultCoordinator.resetState(); + mIsAuthenticating = false; + } + } + + /** + * @return true if a user can authenticate with a given strength. + */ + boolean getCanAuthFor(int userId, @Authenticators.Types int strength) { + return mMultiBiometricLockoutState.canUserAuthenticate(userId, strength); + } + + @Override + public void authStartedFor(int userId, int sensorId) { + if (!mIsAuthenticating) { + onAuthSessionStarted(userId); + } + + if (mAuthOperations.contains(sensorId)) { + Slog.e(TAG, "Error, authStartedFor(" + sensorId + ") without being finished"); + return; + } + + if (mUserId != userId) { + Slog.e(TAG, "Error authStartedFor(" + userId + ") Incorrect userId, expected" + mUserId + + ", ignoring..."); + return; + } + + mAuthOperations.add(sensorId); + } + + @Override + public void authenticatedFor(int userId, @Authenticators.Types int biometricStrength, + int sensorId) { + mAuthResultCoordinator.authenticatedFor(biometricStrength); + attemptToFinish(userId, sensorId, + "authenticatedFor(userId=" + userId + ", biometricStrength=" + biometricStrength + + ", sensorId=" + sensorId + ""); + } + + @Override + public void lockedOutFor(int userId, @Authenticators.Types int biometricStrength, + int sensorId) { + mAuthResultCoordinator.lockedOutFor(biometricStrength); + attemptToFinish(userId, sensorId, + "lockOutFor(userId=" + userId + ", biometricStrength=" + biometricStrength + + ", sensorId=" + sensorId + ""); + } + + @Override + public void authEndedFor(int userId, @Authenticators.Types int biometricStrength, + int sensorId) { + mAuthResultCoordinator.authEndedFor(biometricStrength); + attemptToFinish(userId, sensorId, + "authEndedFor(userId=" + userId + " ,biometricStrength=" + biometricStrength + + ", sensorId=" + sensorId); + } + + @Override + public void resetLockoutFor(int userId, @Authenticators.Types int biometricStrength) { + mMultiBiometricLockoutState.onUserUnlocked(userId, biometricStrength); + } + + private void attemptToFinish(int userId, int sensorId, String description) { + boolean didFail = false; + if (!mAuthOperations.contains(sensorId)) { + Slog.e(TAG, "Error unable to find auth operation : " + description); + didFail = true; + } + if (userId != mUserId) { + Slog.e(TAG, "Error mismatched userId, expected=" + mUserId + " for " + description); + didFail = true; + } + if (didFail) { + return; + } + mAuthOperations.remove(sensorId); + if (mIsAuthenticating && mAuthOperations.isEmpty()) { + endAuthSession(); + } + } + +} diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java new file mode 100644 index 000000000000..8b1f90af0234 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/AuthSessionListener.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import android.hardware.biometrics.BiometricManager.Authenticators; + +/** + * An interface that listens to authentication events. + */ +interface AuthSessionListener { + /** + * Indicates an auth operation has started for a given user and sensor. + */ + void authStartedFor(int userId, int sensorId); + + /** + * Indicates a successful authentication occurred for a sensor of a given strength. + */ + void authenticatedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId); + + /** + * Indicates authentication ended for a sensor of a given strength. + */ + void authEndedFor(int userId, @Authenticators.Types int biometricStrength, int sensorId); + + /** + * Indicates a lockout occurred for a sensor of a given strength. + */ + void lockedOutFor(int userId, @Authenticators.Types int biometricStrength, int sensorId); + + /** + * Indicates that a reset lockout has happened for a given strength. + */ + void resetLockoutFor(int uerId, @Authenticators.Types int biometricStrength); +} diff --git a/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java new file mode 100644 index 000000000000..49dc817c7fd5 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/sensors/MultiBiometricLockoutState.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import static android.hardware.biometrics.BiometricManager.Authenticators; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; + +import android.util.ArrayMap; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class is used as a system to store the state of each + * {@link Authenticators.Types} status for every user. + */ +class MultiBiometricLockoutState { + + private static final String TAG = "MultiBiometricLockoutState"; + private static final Map<Integer, List<Integer>> PRECEDENCE; + + static { + Map<Integer, List<Integer>> precedence = new ArrayMap<>(); + precedence.put(Authenticators.BIOMETRIC_STRONG, + Arrays.asList(BIOMETRIC_STRONG, BIOMETRIC_WEAK, BIOMETRIC_CONVENIENCE)); + precedence.put(BIOMETRIC_WEAK, Arrays.asList(BIOMETRIC_WEAK, BIOMETRIC_CONVENIENCE)); + precedence.put(BIOMETRIC_CONVENIENCE, Arrays.asList(BIOMETRIC_CONVENIENCE)); + PRECEDENCE = Collections.unmodifiableMap(precedence); + } + + private final Map<Integer, Map<Integer, Boolean>> mCanUserAuthenticate; + + @VisibleForTesting + MultiBiometricLockoutState() { + mCanUserAuthenticate = new HashMap<>(); + } + + private static Map<Integer, Boolean> createLockedOutMap() { + Map<Integer, Boolean> lockOutMap = new HashMap<>(); + lockOutMap.put(BIOMETRIC_STRONG, false); + lockOutMap.put(BIOMETRIC_WEAK, false); + lockOutMap.put(BIOMETRIC_CONVENIENCE, false); + return lockOutMap; + } + + private Map<Integer, Boolean> getAuthMapForUser(int userId) { + if (!mCanUserAuthenticate.containsKey(userId)) { + mCanUserAuthenticate.put(userId, createLockedOutMap()); + } + return mCanUserAuthenticate.get(userId); + } + + /** + * Indicates a {@link Authenticators} has been locked for userId. + * + * @param userId The user. + * @param strength The strength of biometric that is requested to be locked. + */ + void onUserLocked(int userId, @Authenticators.Types int strength) { + Slog.d(TAG, "onUserLocked(userId=" + userId + ", strength=" + strength + ")"); + Map<Integer, Boolean> canUserAuthState = getAuthMapForUser(userId); + for (int strengthToLockout : PRECEDENCE.get(strength)) { + canUserAuthState.put(strengthToLockout, false); + } + } + + /** + * Indicates that a user has unlocked a {@link Authenticators} + * + * @param userId The user. + * @param strength The strength of biometric that is unlocked. + */ + void onUserUnlocked(int userId, @Authenticators.Types int strength) { + Slog.d(TAG, "onUserUnlocked(userId=" + userId + ", strength=" + strength + ")"); + Map<Integer, Boolean> canUserAuthState = getAuthMapForUser(userId); + for (int strengthToLockout : PRECEDENCE.get(strength)) { + canUserAuthState.put(strengthToLockout, true); + } + } + + /** + * Indicates if a user can perform an authentication operation with a given + * {@link Authenticators.Types} + * + * @param userId The user. + * @param strength The strength of biometric that is requested to authenticate. + * @return If a user can authenticate with a given biometric of this strength. + */ + boolean canUserAuthenticate(int userId, @Authenticators.Types int strength) { + final boolean canAuthenticate = getAuthMapForUser(userId).get(strength); + Slog.d(TAG, "canUserAuthenticate(userId=" + userId + ", strength=" + strength + ") =" + + canAuthenticate); + return canAuthenticate; + } +} diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java index 3e397468aa87..82436cc0890a 100644 --- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java +++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java @@ -221,14 +221,12 @@ final class IInputMethodInvoker { } @AnyThread - boolean updateEditorToolType(int toolType) { + void updateEditorToolType(@MotionEvent.ToolType int toolType) { try { mTarget.updateEditorToolType(toolType); } catch (RemoteException e) { logRemoteException(e); - return false; } - return true; } @AnyThread diff --git a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java index 5a0069ac8e06..789222eb08f1 100644 --- a/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java +++ b/services/core/java/com/android/server/inputmethod/InputContentUriTokenHandler.java @@ -1,18 +1,18 @@ /* -** Copyright 2016, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.android.server.inputmethod; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java index 2160b652317a..dc2799e8e434 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodDeviceConfigs.java @@ -29,7 +29,7 @@ final class InputMethodDeviceConfigs { private boolean mHideImeWhenNoEditorFocus; private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigChangedListener; - public InputMethodDeviceConfigs() { + InputMethodDeviceConfigs() { mDeviceConfigChangedListener = properties -> { if (!DeviceConfig.NAMESPACE_INPUT_METHOD_MANAGER.equals(properties.getNamespace())) { return; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 8a56afaa0d74..520a471224b2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -69,7 +69,6 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AppGlobals; -import android.app.AppOpsManager; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; @@ -307,7 +306,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean mHasFeature; private final ArrayMap<String, List<InputMethodSubtype>> mAdditionalSubtypeMap = new ArrayMap<>(); - private final AppOpsManager mAppOpsManager; private final UserManager mUserManager; private final UserManagerInternal mUserManagerInternal; private final InputMethodMenuController mMenuController; @@ -1734,7 +1732,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mInputMethodDeviceConfigs = new InputMethodDeviceConfigs(); mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy; mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); - mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mUserManager = mContext.getSystemService(UserManager.class); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); mAccessibilityManager = AccessibilityManager.getInstance(context); @@ -2520,7 +2517,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub null, null, null, selectedMethodId, getSequenceNumberLocked(), null, false); } - if (!InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, cs.mUid, + if (!InputMethodUtils.checkIfPackageBelongsToUid(mPackageManagerInternal, cs.mUid, editorInfo.packageName)) { Slog.e(TAG, "Rejecting this client as it reported an invalid package name." + " uid=" + cs.mUid + " package=" + editorInfo.packageName); @@ -3957,7 +3954,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid( - mAppOpsManager, + mPackageManagerInternal, uid, getCurIntentLocked().getComponent().getPackageName())) { return true; @@ -4156,6 +4153,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (UserHandle.getCallingUserId() != userId) { mContext.enforceCallingPermission(Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } + final int callingUid = Binder.getCallingUid(); // By this IPC call, only a process which shares the same uid with the IME can add // additional input method subtypes to the IME. @@ -4176,7 +4174,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (mSettings.getCurrentUserId() == userId) { if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, - mAdditionalSubtypeMap, mIPackageManager)) { + mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) { return; } final long ident = Binder.clearCallingIdentity(); @@ -4188,14 +4186,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(mContext, methodMap, - userId, false); + 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(mContext, methodMap, + userId, false); settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap, - mIPackageManager); + mPackageManagerInternal, callingUid); } } @@ -4208,8 +4209,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final int callingUid = Binder.getCallingUid(); final ComponentName imeComponentName = imeId != null ? ComponentName.unflattenFromString(imeId) : null; - if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid(mAppOpsManager, - callingUid, imeComponentName.getPackageName())) { + if (imeComponentName == null || !InputMethodUtils.checkIfPackageBelongsToUid( + mPackageManagerInternal, callingUid, imeComponentName.getPackageName())) { throw new SecurityException("Calling UID=" + callingUid + " does not belong to imeId=" + imeId); } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java index 11e6923aa75a..a25630ffefa1 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java @@ -71,7 +71,7 @@ final class InputMethodMenuController { @Nullable private InputMethodDialogWindowContext mDialogWindowContext; - public InputMethodMenuController(InputMethodManagerService service) { + InputMethodMenuController(InputMethodManagerService service) { mService = service; mSettings = mService.mSettings; mSwitchingController = mService.mSwitchingController; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index c57fe332867a..69b06613b298 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -20,16 +20,13 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserHandleAware; import android.annotation.UserIdInt; -import android.app.AppOpsManager; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageManager; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.res.Resources; -import android.os.Binder; import android.os.Build; -import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; @@ -45,7 +42,6 @@ import android.view.textservice.SpellCheckerInfo; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.StartInputFlags; -import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.textservices.TextServicesManagerInternal; @@ -80,29 +76,6 @@ final class InputMethodUtils { } // ---------------------------------------------------------------------- - // Utilities for debug - static String getApiCallStack() { - String apiCallStack = ""; - try { - throw new RuntimeException(); - } catch (RuntimeException e) { - final StackTraceElement[] frames = e.getStackTrace(); - for (int j = 1; j < frames.length; ++j) { - final String tempCallStack = frames[j].toString(); - if (TextUtils.isEmpty(apiCallStack)) { - // Overwrite apiCallStack if it's empty - apiCallStack = tempCallStack; - } else if (tempCallStack.indexOf("Transact(") < 0) { - // Overwrite apiCallStack if it's not a binder call - apiCallStack = tempCallStack; - } else { - break; - } - } - } - return apiCallStack; - } - // ---------------------------------------------------------------------- static boolean canAddToLastInputMethod(InputMethodSubtype subtype) { if (subtype == null) return true; @@ -210,28 +183,27 @@ final class InputMethodUtils { return subtype != null ? TextUtils.concat(subtype.getDisplayName(context, imi.getPackageName(), imi.getServiceInfo().applicationInfo), - (TextUtils.isEmpty(imiLabel) ? - "" : " - " + imiLabel)) + (TextUtils.isEmpty(imiLabel) ? "" : " - " + imiLabel)) : imiLabel; } /** * Returns true if a package name belongs to a UID. * - * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p> - * @param appOpsManager the {@link AppOpsManager} object to be used for the validation. + * <p>This is a simple wrapper of + * {@link PackageManagerInternal#getPackageUid(String, long, int)}.</p> + * @param packageManagerInternal the {@link PackageManagerInternal} object to be used for the + * validation. * @param uid the UID to be validated. * @param packageName the package name. * @return {@code true} if the package name belongs to the UID. */ - static boolean checkIfPackageBelongsToUid(AppOpsManager appOpsManager, + static boolean checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal, int uid, String packageName) { - try { - appOpsManager.checkPackage(uid, packageName); - return true; - } catch (SecurityException e) { - return false; - } + // PackageManagerInternal#getPackageUid() doesn't check MATCH_INSTANT/MATCH_APEX as of + // writing. So setting 0 should be fine. + return packageManagerInternal.getPackageUid(packageName, 0 /* flags */, + UserHandle.getUserId(uid)) == uid; } /** @@ -674,8 +646,8 @@ final class InputMethodUtils { List<InputMethodSubtype> implicitlyEnabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi); if (implicitlyEnabledSubtypes != null) { - final int N = implicitlyEnabledSubtypes.size(); - for (int i = 0; i < N; ++i) { + final int numSubtypes = implicitlyEnabledSubtypes.size(); + for (int i = 0; i < numSubtypes; ++i) { final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i); if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) { return subtypeHashCode; @@ -877,20 +849,13 @@ final class InputMethodUtils { boolean setAdditionalInputMethodSubtypes(@NonNull String imeId, @NonNull ArrayList<InputMethodSubtype> subtypes, @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - @NonNull IPackageManager packageManager) { + @NonNull PackageManagerInternal packageManagerInternal, int callingUid) { final InputMethodInfo imi = mMethodMap.get(imeId); if (imi == null) { return false; } - final String[] packageInfos; - try { - packageInfos = packageManager.getPackagesForUid(Binder.getCallingUid()); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to get package infos"); - return false; - } - if (ArrayUtils.find(packageInfos, - packageInfo -> TextUtils.equals(packageInfo, imi.getPackageName())) == null) { + if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid, + imi.getPackageName())) { return false; } diff --git a/services/core/java/com/android/server/inputmethod/LocaleUtils.java b/services/core/java/com/android/server/inputmethod/LocaleUtils.java index 3d02b3af6bc1..f865e6010580 100644 --- a/services/core/java/com/android/server/inputmethod/LocaleUtils.java +++ b/services/core/java/com/android/server/inputmethod/LocaleUtils.java @@ -46,7 +46,7 @@ final class LocaleUtils { * @param desired The locale preferred by user. * @return A score based on the locale matching for the default subtype enabling. */ - @IntRange(from=1, to=3) + @IntRange(from = 1, to = 3) private static byte calculateMatchingSubScore(@NonNull final ULocale supported, @NonNull final ULocale desired) { // Assuming supported/desired is fully expanded. @@ -111,7 +111,7 @@ final class LocaleUtils { * @return 1 if {@code left} is larger than {@code right}. -1 if {@code left} is less than * {@code right}. 0 if {@code left} and {@code right} is equal. */ - @IntRange(from=-1, to=1) + @IntRange(from = -1, to = 1) private static int compare(@NonNull byte[] left, @NonNull byte[] right) { for (int i = 0; i < left.length; ++i) { if (left[i] > right[i]) { diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java index 7085868e4f61..f07539fa5eb2 100644 --- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java @@ -68,14 +68,14 @@ final class SubtypeUtils { if (locale == null) { return false; } - final int N = imi.getSubtypeCount(); - for (int i = 0; i < N; ++i) { + final int numSubtypes = imi.getSubtypeCount(); + for (int i = 0; i < numSubtypes; ++i) { final InputMethodSubtype subtype = imi.getSubtypeAt(i); if (checkCountry) { final Locale subtypeLocale = subtype.getLocaleObject(); - if (subtypeLocale == null || - !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) || - !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) { + if (subtypeLocale == null + || !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) + || !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) { continue; } } else { @@ -260,8 +260,8 @@ final class SubtypeUtils { boolean partialMatchFound = false; InputMethodSubtype applicableSubtype = null; InputMethodSubtype firstMatchedModeSubtype = null; - final int N = subtypes.size(); - for (int i = 0; i < N; ++i) { + final int numSubtypes = subtypes.size(); + for (int i = 0; i < numSubtypes; ++i) { InputMethodSubtype subtype = subtypes.get(i); final String subtypeLocale = subtype.getLocale(); final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 533d1b0c7034..58d677c31606 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -113,6 +113,7 @@ import android.util.EventLog; import android.util.LongSparseArray; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseIntArray; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -141,6 +142,7 @@ import com.android.server.locksettings.SyntheticPasswordManager.SyntheticPasswor import com.android.server.locksettings.SyntheticPasswordManager.TokenType; import com.android.server.locksettings.recoverablekeystore.RecoverableKeyStoreManager; import com.android.server.pm.UserManagerInternal; +import com.android.server.utils.Slogf; import com.android.server.wm.WindowManagerInternal; import libcore.util.HexEncoding; @@ -243,6 +245,17 @@ public class LockSettingsService extends ILockSettings.Stub { private final RebootEscrowManager mRebootEscrowManager; + // Locking order is mUserCreationAndRemovalLock -> mSpManager. + private final Object mUserCreationAndRemovalLock = new Object(); + // These two arrays are only used at boot time. To save memory, they are set to null when + // PHASE_BOOT_COMPLETED is reached. + @GuardedBy("mUserCreationAndRemovalLock") + private SparseIntArray mEarlyCreatedUsers = new SparseIntArray(); + @GuardedBy("mUserCreationAndRemovalLock") + private SparseIntArray mEarlyRemovedUsers = new SparseIntArray(); + @GuardedBy("mUserCreationAndRemovalLock") + private boolean mBootComplete; + // Current password metric for all users on the device. Updated when user unlocks // the device or changes password. Removed when user is stopped. @GuardedBy("this") @@ -283,9 +296,16 @@ public class LockSettingsService extends ILockSettings.Stub { @Override public void onBootPhase(int phase) { super.onBootPhase(phase); - if (phase == PHASE_ACTIVITY_MANAGER_READY) { - mLockSettingsService.migrateOldDataAfterSystemReady(); - mLockSettingsService.loadEscrowData(); + switch (phase) { + case PHASE_ACTIVITY_MANAGER_READY: + mLockSettingsService.migrateOldDataAfterSystemReady(); + mLockSettingsService.loadEscrowData(); + break; + case PHASE_BOOT_COMPLETED: + mLockSettingsService.bootCompleted(); + break; + default: + break; } } @@ -577,7 +597,6 @@ public class LockSettingsService extends ILockSettings.Stub { IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_USER_ADDED); filter.addAction(Intent.ACTION_USER_STARTING); - filter.addAction(Intent.ACTION_USER_REMOVED); injector.getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter, null, null); @@ -720,28 +739,32 @@ public class LockSettingsService extends ILockSettings.Stub { } /** - * Clean up states associated with the given user, in case the userId is reused but LSS didn't - * get a chance to do cleanup previously during ACTION_USER_REMOVED. - * - * Internally, LSS stores serial number for each user and check it against the current user's - * serial number to determine if the userId is reused and invoke cleanup code. + * Removes the LSS state for the given userId if the userId was reused without its LSS state + * being fully removed. + * <p> + * This is primarily needed for users that were removed by Android 13 or earlier, which didn't + * guarantee removal of LSS state as it relied on the {@code ACTION_USER_REMOVED} intent. It is + * also needed because {@link #removeUser()} delays requests to remove LSS state until the + * {@code PHASE_BOOT_COMPLETED} boot phase, so they can be lost. + * <p> + * Stale state is detected by checking whether the user serial number changed. This works + * because user serial numbers are never reused. */ - private void cleanupDataForReusedUserIdIfNecessary(int userId) { + private void removeStateForReusedUserIdIfNecessary(@UserIdInt int userId, int serialNumber) { if (userId == UserHandle.USER_SYSTEM) { // Short circuit as we never clean up user 0. return; } - // Serial number is never reusued, so we can use it as a distinguisher for user Id reuse. - int serialNumber = mUserManager.getUserSerialNumber(userId); - int storedSerialNumber = mStorage.getInt(USER_SERIAL_NUMBER_KEY, -1, userId); if (storedSerialNumber != serialNumber) { // If LockSettingsStorage does not have a copy of the serial number, it could be either // this is a user created before the serial number recording logic is introduced, or // the user does not exist or was removed and cleaned up properly. In either case, don't - // invoke removeUser(). + // invoke removeUserState(). if (storedSerialNumber != -1) { - removeUser(userId, /* unknownUser */ true); + Slogf.i(TAG, "Removing stale state for reused userId %d (serial %d => %d)", userId, + storedSerialNumber, serialNumber); + removeUserState(userId); } mStorage.setInt(USER_SERIAL_NUMBER_KEY, serialNumber, userId); } @@ -771,7 +794,6 @@ public class LockSettingsService extends ILockSettings.Stub { mHandler.post(new Runnable() { @Override public void run() { - cleanupDataForReusedUserIdIfNecessary(userId); ensureProfileKeystoreUnlocked(userId); // Hide notification first, as tie managed profile lock takes time hideEncryptionNotification(new UserHandle(userId)); @@ -779,38 +801,10 @@ public class LockSettingsService extends ILockSettings.Stub { if (isCredentialSharableWithParent(userId)) { tieProfileLockIfNecessary(userId, LockscreenCredential.createNone()); } - - // If the user doesn't have a credential, try and derive their secret for the - // AuthSecret HAL. The secret will have been enrolled if the user previously set a - // credential and still needs to be passed to the HAL once that credential is - // removed. - if (mUserManager.getUserInfo(userId).isPrimary() && !isUserSecure(userId)) { - tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(userId); - } } }); } - private void tryDeriveVendorAuthSecretForUnsecuredPrimaryUser(@UserIdInt int userId) { - synchronized (mSpManager) { - // If there is no SP, then there is no vendor auth secret. - if (!isSyntheticPasswordBasedCredentialLocked(userId)) { - return; - } - - final long protectorId = getCurrentLskfBasedProtectorId(userId); - AuthenticationResult result = - mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId, - LockscreenCredential.createNone(), userId, null); - if (result.syntheticPassword != null) { - Slog.i(TAG, "Unwrapped SP for unsecured primary user " + userId); - onSyntheticPasswordKnown(userId, result.syntheticPassword); - } else { - Slog.e(TAG, "Failed to unwrap SP for unsecured primary user " + userId); - } - } - } - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -821,11 +815,6 @@ public class LockSettingsService extends ILockSettings.Stub { } else if (Intent.ACTION_USER_STARTING.equals(intent.getAction())) { final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); mStorage.prefetchUser(userHandle); - } else if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) { - final int userHandle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); - if (userHandle > 0) { - removeUser(userHandle, /* unknownUser= */ false); - } } } }; @@ -937,6 +926,79 @@ public class LockSettingsService extends ILockSettings.Stub { return success; } + private void bootCompleted() { + synchronized (mUserCreationAndRemovalLock) { + // Handle delayed calls to LSS.removeUser() and LSS.createNewUser(). + for (int i = 0; i < mEarlyRemovedUsers.size(); i++) { + int userId = mEarlyRemovedUsers.keyAt(i); + Slogf.i(TAG, "Removing locksettings state for removed user %d now that boot " + + "is complete", userId); + removeUserState(userId); + } + mEarlyRemovedUsers = null; // no longer needed + for (int i = 0; i < mEarlyCreatedUsers.size(); i++) { + int userId = mEarlyCreatedUsers.keyAt(i); + int serialNumber = mEarlyCreatedUsers.valueAt(i); + + removeStateForReusedUserIdIfNecessary(userId, serialNumber); + synchronized (mSpManager) { + if (!isSyntheticPasswordBasedCredentialLocked(userId)) { + Slogf.i(TAG, "Creating locksettings state for user %d now that boot " + + "is complete", userId); + initializeSyntheticPasswordLocked(userId); + } + } + } + mEarlyCreatedUsers = null; // no longer needed + + // Also do a one-time migration of all users to SP-based credentials with the CE key + // encrypted by the SP. This is needed for the system user on the first boot of a + // device, as the system user is special and never goes through the user creation flow + // that other users do. It is also needed for existing users on a device upgraded from + // Android 13 or earlier, where users with no LSKF didn't necessarily have an SP, and if + // they did have an SP then their CE key wasn't encrypted by it. + // + // If this gets interrupted (e.g. by the device powering off), there shouldn't be a + // problem since this will run again on the next boot, and setUserKeyProtection() is + // okay with the key being already protected by the given secret. + if (getString("migrated_all_users_to_sp_and_bound_ce", null, 0) == null) { + for (UserInfo user : mUserManager.getAliveUsers()) { + removeStateForReusedUserIdIfNecessary(user.id, user.serialNumber); + synchronized (mSpManager) { + migrateUserToSpWithBoundCeKeyLocked(user.id); + } + } + setString("migrated_all_users_to_sp_and_bound_ce", "true", 0); + } + + mBootComplete = true; + } + } + + @GuardedBy("mSpManager") + private void migrateUserToSpWithBoundCeKeyLocked(@UserIdInt int userId) { + if (isUserSecure(userId)) { + Slogf.d(TAG, "User %d is secured; no migration needed", userId); + return; + } + long protectorId = getCurrentLskfBasedProtectorId(userId); + if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) { + Slogf.i(TAG, "Migrating unsecured user %d to SP-based credential", userId); + initializeSyntheticPasswordLocked(userId); + } else { + Slogf.i(TAG, "Existing unsecured user %d has a synthetic password; re-encrypting CE " + + "key with it", userId); + AuthenticationResult result = mSpManager.unlockLskfBasedProtector( + getGateKeeperService(), protectorId, LockscreenCredential.createNone(), userId, + null); + if (result.syntheticPassword == null) { + Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId); + return; + } + setUserKeyProtection(userId, result.syntheticPassword.deriveFileBasedEncryptionKey()); + } + } + /** * Returns the lowest password quality that still presents the same UI for entering it. * @@ -1269,9 +1331,8 @@ public class LockSettingsService extends ILockSettings.Stub { * can end up calling into other system services to process user unlock request (via * {@link com.android.server.SystemServiceManager#unlockUser} </em> */ - private void unlockUser(int userId, byte[] secret) { - Slog.i(TAG, "Unlocking user " + userId + " with secret only, length " - + (secret != null ? secret.length : 0)); + private void unlockUser(@UserIdInt int userId) { + Slogf.i(TAG, "Unlocking user %d", userId); // TODO: make this method fully async so we can update UI with progress strings final boolean alreadyUnlocked = mUserManager.isUserUnlockingOrUnlocked(userId); final CountDownLatch latch = new CountDownLatch(1); @@ -1294,7 +1355,7 @@ public class LockSettingsService extends ILockSettings.Stub { }; try { - mActivityManager.unlockUser(userId, null, secret, listener); + mActivityManager.unlockUser2(userId, listener); } catch (RemoteException e) { throw e.rethrowAsRuntimeException(); } @@ -1587,6 +1648,7 @@ public class LockSettingsService extends ILockSettings.Stub { if (!savedCredential.isNone()) { throw new IllegalStateException("Saved credential given, but user has no SP"); } + // TODO(b/232452368): this case is only needed by unit tests now; remove it. initializeSyntheticPasswordLocked(userId); } else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) { // get credential from keystore when profile has unified lock @@ -1897,19 +1959,12 @@ public class LockSettingsService extends ILockSettings.Stub { mStorage.writeChildProfileLock(userId, ArrayUtils.concat(iv, ciphertext)); } - private void setUserKeyProtection(int userId, byte[] key) { - if (DEBUG) Slog.d(TAG, "setUserKeyProtection: user=" + userId); - addUserKeyAuth(userId, key); - } - - private void clearUserKeyProtection(int userId, byte[] secret) { - if (DEBUG) Slog.d(TAG, "clearUserKeyProtection user=" + userId); - final UserInfo userInfo = mUserManager.getUserInfo(userId); + private void setUserKeyProtection(@UserIdInt int userId, byte[] secret) { final long callingId = Binder.clearCallingIdentity(); try { - mStorageManager.clearUserKeyAuth(userId, userInfo.serialNumber, secret); + mStorageManager.setUserKeyProtection(userId, secret); } catch (RemoteException e) { - throw new IllegalStateException("clearUserKeyAuth failed user=" + userId); + throw new IllegalStateException("Failed to protect CE key for user " + userId, e); } finally { Binder.restoreCallingIdentity(callingId); } @@ -1924,40 +1979,51 @@ public class LockSettingsService extends ILockSettings.Stub { } } - /** Unlock file-based encryption */ - private void unlockUserKey(int userId, byte[] secret) { - final UserInfo userInfo = mUserManager.getUserInfo(userId); - try { - mStorageManager.unlockUserKey(userId, userInfo.serialNumber, secret); - } catch (RemoteException e) { - throw new IllegalStateException("Failed to unlock user key " + userId, e); - + /** + * Unlocks the user's CE (credential-encrypted) storage if it's not already unlocked. + * <p> + * This method doesn't throw exceptions because it is called opportunistically whenever a user + * is started. Whether it worked or not can be detected by whether the key got unlocked or not. + */ + private void unlockUserKey(@UserIdInt int userId, SyntheticPassword sp) { + if (isUserKeyUnlocked(userId)) { + Slogf.d(TAG, "CE storage for user %d is already unlocked", userId); + return; } - } - - private void addUserKeyAuth(int userId, byte[] secret) { final UserInfo userInfo = mUserManager.getUserInfo(userId); - final long callingId = Binder.clearCallingIdentity(); + final String userType = isUserSecure(userId) ? "secured" : "unsecured"; + final byte[] secret = sp.deriveFileBasedEncryptionKey(); try { - mStorageManager.addUserKeyAuth(userId, userInfo.serialNumber, secret); + mStorageManager.unlockUserKey(userId, userInfo.serialNumber, secret); + Slogf.i(TAG, "Unlocked CE storage for %s user %d", userType, userId); } catch (RemoteException e) { - throw new IllegalStateException("Failed to add new key to vold " + userId, e); + Slogf.wtf(TAG, e, "Failed to unlock CE storage for %s user %d", userType, userId); } finally { - Binder.restoreCallingIdentity(callingId); + Arrays.fill(secret, (byte) 0); } } - private void fixateNewestUserKeyAuth(int userId) { - if (DEBUG) Slog.d(TAG, "fixateNewestUserKeyAuth: user=" + userId); - final long callingId = Binder.clearCallingIdentity(); - try { - mStorageManager.fixateNewestUserKeyAuth(userId); - } catch (RemoteException e) { - // OK to ignore the exception as vold would just accept both old and new - // keys if this call fails, and will fix itself during the next boot - Slog.w(TAG, "fixateNewestUserKeyAuth failed", e); - } finally { - Binder.restoreCallingIdentity(callingId); + private void unlockUserKeyIfUnsecured(@UserIdInt int userId) { + synchronized (mSpManager) { + if (isUserKeyUnlocked(userId)) { + Slogf.d(TAG, "CE storage for user %d is already unlocked", userId); + return; + } + if (isUserSecure(userId)) { + Slogf.d(TAG, "Not unlocking CE storage for user %d yet because user is secured", + userId); + return; + } + Slogf.i(TAG, "Unwrapping synthetic password for unsecured user %d", userId); + AuthenticationResult result = mSpManager.unlockLskfBasedProtector( + getGateKeeperService(), getCurrentLskfBasedProtectorId(userId), + LockscreenCredential.createNone(), userId, null); + if (result.syntheticPassword == null) { + Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId); + return; + } + onSyntheticPasswordKnown(userId, result.syntheticPassword); + unlockUserKey(userId, result.syntheticPassword); } } @@ -2228,8 +2294,50 @@ public class LockSettingsService extends ILockSettings.Stub { }); } - private void removeUser(int userId, boolean unknownUser) { - Slog.i(TAG, "RemoveUser: " + userId); + private void createNewUser(@UserIdInt int userId, int userSerialNumber) { + synchronized (mUserCreationAndRemovalLock) { + // Before PHASE_BOOT_COMPLETED, don't actually create the synthetic password yet, but + // rather automatically delay it to later. We do this because protecting the synthetic + // password requires the Weaver HAL if the device supports it, and some devices don't + // make Weaver available until fairly late in the boot process. This logic ensures a + // consistent flow across all devices, regardless of their Weaver implementation. + if (!mBootComplete) { + Slogf.i(TAG, "Delaying locksettings state creation for user %d until boot complete", + userId); + mEarlyCreatedUsers.put(userId, userSerialNumber); + mEarlyRemovedUsers.delete(userId); + return; + } + removeStateForReusedUserIdIfNecessary(userId, userSerialNumber); + synchronized (mSpManager) { + initializeSyntheticPasswordLocked(userId); + } + } + } + + private void removeUser(@UserIdInt int userId) { + synchronized (mUserCreationAndRemovalLock) { + // Before PHASE_BOOT_COMPLETED, don't actually remove the LSS state yet, but rather + // automatically delay it to later. We do this because deleting synthetic password + // protectors requires the Weaver HAL if the device supports it, and some devices don't + // make Weaver available until fairly late in the boot process. This logic ensures a + // consistent flow across all devices, regardless of their Weaver implementation. + if (!mBootComplete) { + Slogf.i(TAG, "Delaying locksettings state removal for user %d until boot complete", + userId); + if (mEarlyCreatedUsers.indexOfKey(userId) >= 0) { + mEarlyCreatedUsers.delete(userId); + } else { + mEarlyRemovedUsers.put(userId, -1 /* unused */); + } + return; + } + Slogf.i(TAG, "Removing state for user %d", userId); + removeUserState(userId); + } + } + + private void removeUserState(@UserIdInt int userId) { removeBiometricsForUser(userId); mSpManager.removeUser(getGateKeeperService(), userId); mStrongAuth.removeUser(userId); @@ -2238,11 +2346,9 @@ public class LockSettingsService extends ILockSettings.Stub { mManagedProfilePasswordCache.removePassword(userId); gateKeeperClearSecureUserId(userId); - if (unknownUser || isCredentialSharableWithParent(userId)) { - removeKeystoreProfileKey(userId); - } - // Clean up storage last, this is to ensure that cleanupDataForReusedUserIdIfNecessary() - // can make the assumption that no USER_SERIAL_NUMBER_KEY means user is fully removed. + removeKeystoreProfileKey(userId); + // Clean up storage last, so that removeStateForReusedUserIdIfNecessary() can assume that no + // USER_SERIAL_NUMBER_KEY means user is fully removed. mStorage.removeUser(userId); } @@ -2497,8 +2603,11 @@ public class LockSettingsService extends ILockSettings.Stub { } private void callToAuthSecretIfNeeded(@UserIdInt int userId, SyntheticPassword sp) { - // Pass the primary user's auth secret to the HAL - if (mAuthSecretService != null && mUserManager.getUserInfo(userId).isPrimary()) { + // If the given user is the primary user, pass the auth secret to the HAL. Only the system + // user can be primary. Check for the system user ID before calling getUserInfo(), as other + // users may still be under construction. + if (mAuthSecretService != null && userId == UserHandle.USER_SYSTEM && + mUserManager.getUserInfo(userId).isPrimary()) { try { final byte[] rawSecret = sp.deriveVendorAuthSecret(); final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length); @@ -2513,9 +2622,13 @@ public class LockSettingsService extends ILockSettings.Stub { } /** - * Creates the synthetic password (SP) for the given user and protects it with an empty LSKF. - * This is called just once in the lifetime of the user: the first time a nonempty LSKF is set, - * or when an escrow token is activated on a device with an empty LSKF. + * Creates the synthetic password (SP) for the given user, protects it with an empty LSKF, and + * protects the user's CE key with a key derived from the SP. + * <p> + * This is called just once in the lifetime of the user: at user creation time (possibly delayed + * until {@code PHASE_BOOT_COMPLETED} to ensure that the Weaver HAL is available if the device + * supports it), or when upgrading from Android 13 or earlier where users with no LSKF didn't + * necessarily have an SP. */ @GuardedBy("mSpManager") @VisibleForTesting @@ -2529,6 +2642,7 @@ public class LockSettingsService extends ILockSettings.Stub { final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(), LockscreenCredential.createNone(), sp, userId); setCurrentLskfBasedProtectorId(protectorId, userId); + setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey()); onSyntheticPasswordKnown(userId, sp); return sp; } @@ -2601,11 +2715,10 @@ public class LockSettingsService extends ILockSettings.Stub { unlockKeystore(sp.deriveKeyStorePassword(), userId); - { - final byte[] secret = sp.deriveFileBasedEncryptionKey(); - unlockUser(userId, secret); - Arrays.fill(secret, (byte) 0); - } + unlockUserKey(userId, sp); + + unlockUser(userId); + activateEscrowTokens(sp, userId); if (isProfileWithSeparatedLock(userId)) { @@ -2626,9 +2739,9 @@ public class LockSettingsService extends ILockSettings.Stub { * be empty) and replacing the old LSKF-based protector with it. The SP itself is not changed. * * Also maintains the invariants described in {@link SyntheticPasswordManager} by - * setting/clearing the protection (by the SP) on the user's file-based encryption key and - * auth-bound Keystore keys when the LSKF is added/removed, respectively. If the new LSKF is - * nonempty, then the Gatekeeper auth token is also refreshed. + * setting/clearing the protection (by the SP) on the user's auth-bound Keystore keys when the + * LSKF is added/removed, respectively. If the new LSKF is nonempty, then the Gatekeeper auth + * token is also refreshed. */ @GuardedBy("mSpManager") private long setLockCredentialWithSpLocked(LockscreenCredential credential, @@ -2648,8 +2761,6 @@ public class LockSettingsService extends ILockSettings.Stub { } else { mSpManager.newSidForUser(getGateKeeperService(), sp, userId); mSpManager.verifyChallenge(getGateKeeperService(), sp, 0L, userId); - setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey()); - fixateNewestUserKeyAuth(userId); setKeystorePassword(sp.deriveKeyStorePassword(), userId); } } else { @@ -2659,9 +2770,7 @@ public class LockSettingsService extends ILockSettings.Stub { mSpManager.clearSidForUser(userId); gateKeeperClearSecureUserId(userId); - unlockUserKey(userId, sp.deriveFileBasedEncryptionKey()); - clearUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey()); - fixateNewestUserKeyAuth(userId); + unlockUserKey(userId, sp); unlockKeystore(sp.deriveKeyStorePassword(), userId); setKeystorePassword(null, userId); removeBiometricsForUser(userId); @@ -2804,6 +2913,7 @@ public class LockSettingsService extends ILockSettings.Stub { if (!isUserSecure(userId)) { long protectorId = getCurrentLskfBasedProtectorId(userId); if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) { + // TODO(b/232452368): this case is only needed by unit tests now; remove it. sp = initializeSyntheticPasswordLocked(userId); } else { sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId, @@ -2888,7 +2998,7 @@ public class LockSettingsService extends ILockSettings.Stub { // If clearing credential, unlock the user manually in order to progress user start // Call unlockUser() on a handler thread so no lock is held (either by LSS or by // the caller like DPMS), otherwise it can lead to deadlock. - mHandler.post(() -> unlockUser(userId, null)); + mHandler.post(() -> unlockUser(userId)); } notifyPasswordChanged(credential, userId); notifySeparateProfileChallengeChanged(userId); @@ -3041,6 +3151,9 @@ public class LockSettingsService extends ILockSettings.Stub { pw.decreaseIndent(); pw.println("PasswordHandleCount: " + mGatekeeperPasswords.size()); + synchronized (mUserCreationAndRemovalLock) { + pw.println("BootComplete: " + mBootComplete); + } } private void dumpKeystoreKeys(IndentingPrintWriter pw) { @@ -3197,6 +3310,21 @@ public class LockSettingsService extends ILockSettings.Stub { private final class LocalService extends LockSettingsInternal { @Override + public void unlockUserKeyIfUnsecured(@UserIdInt int userId) { + LockSettingsService.this.unlockUserKeyIfUnsecured(userId); + } + + @Override + public void createNewUser(@UserIdInt int userId, int userSerialNumber) { + LockSettingsService.this.createNewUser(userId, userSerialNumber); + } + + @Override + public void removeUser(@UserIdInt int userId) { + LockSettingsService.this.removeUser(userId); + } + + @Override public long addEscrowToken(byte[] token, int userId, EscrowTokenStateChangeCallback callback) { return LockSettingsService.this.addEscrowToken(token, TOKEN_TYPE_STRONG, userId, diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index f1afb96866eb..66bdadb185c1 100644 --- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java @@ -49,6 +49,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.VerifyCredentialResponse; import com.android.server.locksettings.LockSettingsStorage.PersistentData; +import com.android.server.utils.Slogf; import libcore.util.HexEncoding; @@ -79,11 +80,12 @@ import java.util.Set; * LockscreenCredential. The LSKF may be empty (none). There may be escrow token-based * protectors as well, only for specific use cases such as enterprise-managed users. * - * - While the user's LSKF is nonempty, the SP protects the user's CE (credential encrypted) - * storage and auth-bound Keystore keys: the user's CE key is encrypted by an SP-derived secret, - * and the user's Keystore and Gatekeeper passwords are other SP-derived secrets. However, while - * the user's LSKF is empty, these protections are cleared; this is needed to invalidate the - * auth-bound keys and make UserController.unlockUser() work with an empty secret. + * - The user's credential-encrypted storage is always protected by the SP. + * + * - The user's auth-bound Keystore keys are protected by the SP, but only while an LSKF is set. + * This works by setting the user's Keystore and Gatekeeper passwords to SP-derived secrets, but + * only while an LSKF is set. When the LSKF is removed, these passwords are cleared, + * invalidating the user's auth-bound keys. * * Files stored on disk for each user: * For the SP itself, stored under NULL_PROTECTOR_ID: @@ -1026,6 +1028,14 @@ public class SyntheticPasswordManager { long protectorId, @NonNull LockscreenCredential credential, int userId, ICheckCredentialProgressCallback progressCallback) { AuthenticationResult result = new AuthenticationResult(); + + if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) { + // This should never happen, due to the migration done in LSS.bootCompleted(). + Slogf.wtf(TAG, "Synthetic password not found for user %d", userId); + result.gkResponse = VerifyCredentialResponse.ERROR; + return result; + } + PasswordData pwd = PasswordData.fromBytes(loadState(PASSWORD_DATA_NAME, protectorId, userId)); diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index 0c601bfde05a..890c89152a7c 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -1962,10 +1962,15 @@ class ShortcutPackage extends ShortcutPackageItem { continue; case TAG_SHORTCUT: - final ShortcutInfo si = parseShortcut(parser, packageName, - shortcutUser.getUserId(), fromBackup); - // Don't use addShortcut(), we don't need to save the icon. - ret.mShortcuts.put(si.getId(), si); + try { + final ShortcutInfo si = parseShortcut(parser, packageName, + shortcutUser.getUserId(), fromBackup); + // Don't use addShortcut(), we don't need to save the icon. + ret.mShortcuts.put(si.getId(), si); + } catch (Exception e) { + // b/246540168 malformed shortcuts should be ignored + Slog.e(TAG, "Failed parsing shortcut.", e); + } continue; case TAG_SHARE_TARGET: ret.mShareTargets.add(ShareTargetInfo.loadFromXml(parser)); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c39cbae0ebf2..0a89d131eda2 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -88,8 +88,6 @@ import android.os.UserManager.QuietModeFlag; import android.os.storage.StorageManager; import android.os.storage.StorageManagerInternal; import android.provider.Settings; -import android.security.GateKeeper; -import android.service.gatekeeper.IGateKeeperService; import android.service.voice.VoiceInteractionManagerInternal; import android.stats.devicepolicy.DevicePolicyEnums; import android.text.TextUtils; @@ -4664,6 +4662,10 @@ public class UserManagerService extends IUserManager.Stub { StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE); t.traceEnd(); + t.traceBegin("LSS.createNewUser"); + mLockPatternUtils.createNewUser(userId, userInfo.serialNumber); + t.traceEnd(); + final Set<String> userTypeInstallablePackages = mSystemPackageInstaller.getInstallablePackagesForUserType(userType); t.traceBegin("PM.createNewUser"); @@ -5500,15 +5502,8 @@ public class UserManagerService extends IUserManager.Stub { Slog.i(LOG_TAG, "Destroying key for user " + userId + " failed, continuing anyway", e); } - // Cleanup gatekeeper secure user id - try { - final IGateKeeperService gk = GateKeeper.getService(); - if (gk != null) { - gk.clearSecureUserId(userId); - } - } catch (Exception ex) { - Slog.w(LOG_TAG, "unable to clear GK secure user id"); - } + // Cleanup lock settings + mLockPatternUtils.removeUser(userId); // Cleanup package manager settings mPm.cleanUpUser(this, userId); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 2d22b8fc3272..f9352cb2b30f 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -5316,7 +5316,7 @@ public final class PowerManagerService extends SystemService private final class SuspendBlockerImpl implements SuspendBlocker { private static final String UNKNOWN_ID = "unknown"; private final String mName; - private final String mTraceName; + private final int mNameHash; private int mReferenceCount; // Maps suspend blocker IDs to a list (LongArray) of open acquisitions for the suspend @@ -5325,7 +5325,7 @@ public final class PowerManagerService extends SystemService public SuspendBlockerImpl(String name) { mName = name; - mTraceName = "SuspendBlocker (" + name + ")"; + mNameHash = mName.hashCode(); } @Override @@ -5336,7 +5336,8 @@ public final class PowerManagerService extends SystemService + "\" was finalized without being released!"); mReferenceCount = 0; mNativeWrapper.nativeReleaseSuspendBlocker(mName); - Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0); + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER, + "SuspendBlockers", mNameHash); } } finally { super.finalize(); @@ -5357,7 +5358,8 @@ public final class PowerManagerService extends SystemService if (DEBUG_SPEW) { Slog.d(TAG, "Acquiring suspend blocker \"" + mName + "\"."); } - Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, mTraceName, 0); + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_POWER, + "SuspendBlockers", mName, mNameHash); mNativeWrapper.nativeAcquireSuspendBlocker(mName); } } @@ -5378,7 +5380,10 @@ public final class PowerManagerService extends SystemService Slog.d(TAG, "Releasing suspend blocker \"" + mName + "\"."); } mNativeWrapper.nativeReleaseSuspendBlocker(mName); - Trace.asyncTraceEnd(Trace.TRACE_TAG_POWER, mTraceName, 0); + if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_POWER, + "SuspendBlockers", mNameHash); + } } else if (mReferenceCount < 0) { Slog.wtf(TAG, "Suspend blocker \"" + mName + "\" was released without being acquired!", new Throwable()); diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index c6128f9f6cef..8a8486038558 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -6813,20 +6813,31 @@ public class BatteryStatsImpl extends BatteryStats { synchronized (mModemNetworkLock) { if (displayTransport == TRANSPORT_CELLULAR) { mModemIfaces = includeInStringArray(mModemIfaces, iface); - if (DEBUG) Slog.d(TAG, "Note mobile iface " + iface + ": " + mModemIfaces); + if (DEBUG) { + Slog.d(TAG, "Note mobile iface " + iface + ": " + + Arrays.toString(mModemIfaces)); + } } else { mModemIfaces = excludeFromStringArray(mModemIfaces, iface); - if (DEBUG) Slog.d(TAG, "Note non-mobile iface " + iface + ": " + mModemIfaces); + if (DEBUG) { + Slog.d(TAG, "Note non-mobile iface " + iface + ": " + + Arrays.toString(mModemIfaces)); + } } } synchronized (mWifiNetworkLock) { if (displayTransport == TRANSPORT_WIFI) { mWifiIfaces = includeInStringArray(mWifiIfaces, iface); - if (DEBUG) Slog.d(TAG, "Note wifi iface " + iface + ": " + mWifiIfaces); + if (DEBUG) { + Slog.d(TAG, "Note wifi iface " + iface + ": " + Arrays.toString(mWifiIfaces)); + } } else { mWifiIfaces = excludeFromStringArray(mWifiIfaces, iface); - if (DEBUG) Slog.d(TAG, "Note non-wifi iface " + iface + ": " + mWifiIfaces); + if (DEBUG) { + Slog.d(TAG, "Note non-wifi iface " + iface + ": " + + Arrays.toString(mWifiIfaces)); + } } } } @@ -12622,8 +12633,7 @@ public class BatteryStatsImpl extends BatteryStats { private void updateCpuMeasuredEnergyStatsLocked(@NonNull long[] clusterChargeUC, @NonNull CpuDeltaPowerAccumulator accumulator) { if (DEBUG_ENERGY) { - Slog.d(TAG, - "Updating cpu cluster stats: " + clusterChargeUC.toString()); + Slog.d(TAG, "Updating cpu cluster stats: " + Arrays.toString(clusterChargeUC)); } if (mGlobalMeasuredEnergyStats == null) { return; diff --git a/services/core/java/com/android/server/timedetector/ConfigurationInternal.java b/services/core/java/com/android/server/timedetector/ConfigurationInternal.java index 372bcc6b07ca..46f335ef9fa2 100644 --- a/services/core/java/com/android/server/timedetector/ConfigurationInternal.java +++ b/services/core/java/com/android/server/timedetector/ConfigurationInternal.java @@ -46,6 +46,7 @@ public final class ConfigurationInternal { private final boolean mAutoDetectionSupported; private final int mSystemClockUpdateThresholdMillis; + private final int mSystemClockConfidenceUpgradeThresholdMillis; private final Instant mAutoSuggestionLowerBound; private final Instant mManualSuggestionLowerBound; private final Instant mSuggestionUpperBound; @@ -57,6 +58,8 @@ public final class ConfigurationInternal { private ConfigurationInternal(Builder builder) { mAutoDetectionSupported = builder.mAutoDetectionSupported; mSystemClockUpdateThresholdMillis = builder.mSystemClockUpdateThresholdMillis; + mSystemClockConfidenceUpgradeThresholdMillis = + builder.mSystemClockConfidenceUpgradeThresholdMillis; mAutoSuggestionLowerBound = Objects.requireNonNull(builder.mAutoSuggestionLowerBound); mManualSuggestionLowerBound = Objects.requireNonNull(builder.mManualSuggestionLowerBound); mSuggestionUpperBound = Objects.requireNonNull(builder.mSuggestionUpperBound); @@ -82,6 +85,17 @@ public final class ConfigurationInternal { } /** + * Return the absolute threshold at/below which the system clock confidence can be upgraded. + * i.e. if the detector receives a high-confidence time and the current system clock is +/- this + * value from that time and the confidence in the time is low, then the device's confidence in + * the current system clock time can be upgraded. This needs to be an amount users would + * consider "close enough". + */ + public int getSystemClockConfidenceUpgradeThresholdMillis() { + return mSystemClockConfidenceUpgradeThresholdMillis; + } + + /** * Returns the lower bound for valid automatic time suggestions. It is guaranteed to be in the * past, i.e. it is unrelated to the current system clock time. * It holds no other meaning; it could be related to when the device system image was built, @@ -242,6 +256,8 @@ public final class ConfigurationInternal { return "ConfigurationInternal{" + "mAutoDetectionSupported=" + mAutoDetectionSupported + ", mSystemClockUpdateThresholdMillis=" + mSystemClockUpdateThresholdMillis + + ", mSystemClockConfidenceUpgradeThresholdMillis=" + + mSystemClockConfidenceUpgradeThresholdMillis + ", mAutoSuggestionLowerBound=" + mAutoSuggestionLowerBound + "(" + mAutoSuggestionLowerBound.toEpochMilli() + ")" + ", mManualSuggestionLowerBound=" + mManualSuggestionLowerBound @@ -258,6 +274,7 @@ public final class ConfigurationInternal { static final class Builder { private boolean mAutoDetectionSupported; private int mSystemClockUpdateThresholdMillis; + private int mSystemClockConfidenceUpgradeThresholdMillis; @NonNull private Instant mAutoSuggestionLowerBound; @NonNull private Instant mManualSuggestionLowerBound; @NonNull private Instant mSuggestionUpperBound; @@ -286,68 +303,55 @@ public final class ConfigurationInternal { this.mAutoDetectionEnabledSetting = toCopy.mAutoDetectionEnabledSetting; } - /** - * Sets whether the user is allowed to configure time settings on this device. - */ + /** See {@link ConfigurationInternal#isUserConfigAllowed()}. */ Builder setUserConfigAllowed(boolean userConfigAllowed) { mUserConfigAllowed = userConfigAllowed; return this; } - /** - * Sets whether automatic time detection is supported on this device. - */ + /** See {@link ConfigurationInternal#isAutoDetectionSupported()}. */ public Builder setAutoDetectionSupported(boolean supported) { mAutoDetectionSupported = supported; return this; } - /** - * Sets the absolute threshold below which the system clock need not be updated. i.e. if - * setting the system clock would adjust it by less than this (either backwards or forwards) - * then it need not be set. - */ + /** See {@link ConfigurationInternal#getSystemClockUpdateThresholdMillis()}. */ public Builder setSystemClockUpdateThresholdMillis(int systemClockUpdateThresholdMillis) { mSystemClockUpdateThresholdMillis = systemClockUpdateThresholdMillis; return this; } - /** - * Sets the lower bound for valid automatic time suggestions. - */ + /** See {@link ConfigurationInternal#getSystemClockConfidenceUpgradeThresholdMillis()}. */ + public Builder setSystemClockConfidenceUpgradeThresholdMillis(int thresholdMillis) { + mSystemClockConfidenceUpgradeThresholdMillis = thresholdMillis; + return this; + } + + /** See {@link ConfigurationInternal#getAutoSuggestionLowerBound()}. */ public Builder setAutoSuggestionLowerBound(@NonNull Instant autoSuggestionLowerBound) { mAutoSuggestionLowerBound = Objects.requireNonNull(autoSuggestionLowerBound); return this; } - /** - * Sets the lower bound for valid manual time suggestions. - */ + /** See {@link ConfigurationInternal#getManualSuggestionLowerBound()}. */ public Builder setManualSuggestionLowerBound(@NonNull Instant manualSuggestionLowerBound) { mManualSuggestionLowerBound = Objects.requireNonNull(manualSuggestionLowerBound); return this; } - /** - * Sets the upper bound for valid time suggestions (manual and automatic). - */ + /** See {@link ConfigurationInternal#getSuggestionUpperBound()}. */ public Builder setSuggestionUpperBound(@NonNull Instant suggestionUpperBound) { mSuggestionUpperBound = Objects.requireNonNull(suggestionUpperBound); return this; } - /** - * Sets the order to look at time suggestions when automatically detecting time. - * See {@code #ORIGIN_} constants - */ + /** See {@link ConfigurationInternal#getAutoOriginPriorities()}. */ public Builder setOriginPriorities(@NonNull @Origin int... originPriorities) { mOriginPriorities = Objects.requireNonNull(originPriorities); return this; } - /** - * Sets the value of the automatic time detection enabled setting for this device. - */ + /** See {@link ConfigurationInternal#getAutoDetectionEnabledSetting()}. */ Builder setAutoDetectionEnabledSetting(boolean autoDetectionEnabledSetting) { mAutoDetectionEnabledSetting = autoDetectionEnabledSetting; return this; diff --git a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java index 3e02b463284d..4972412472f1 100644 --- a/services/core/java/com/android/server/timedetector/EnvironmentImpl.java +++ b/services/core/java/com/android/server/timedetector/EnvironmentImpl.java @@ -16,16 +16,21 @@ package com.android.server.timedetector; +import android.annotation.CurrentTimeMillisLong; import android.annotation.NonNull; -import android.app.AlarmManager; import android.content.Context; import android.os.Handler; import android.os.PowerManager; import android.os.SystemClock; import android.util.Slog; +import com.android.server.AlarmManagerInternal; +import com.android.server.LocalServices; +import com.android.server.SystemClockTime; +import com.android.server.SystemClockTime.TimeConfidence; import com.android.server.timezonedetector.ConfigurationChangeListener; +import java.io.PrintWriter; import java.util.Objects; /** @@ -38,7 +43,7 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment { @NonNull private final Handler mHandler; @NonNull private final ServiceConfigAccessor mServiceConfigAccessor; @NonNull private final PowerManager.WakeLock mWakeLock; - @NonNull private final AlarmManager mAlarmManager; + @NonNull private final AlarmManagerInternal mAlarmManagerInternal; EnvironmentImpl(@NonNull Context context, @NonNull Handler handler, @NonNull ServiceConfigAccessor serviceConfigAccessor) { @@ -49,7 +54,8 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment { mWakeLock = Objects.requireNonNull( powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG)); - mAlarmManager = Objects.requireNonNull(context.getSystemService(AlarmManager.class)); + mAlarmManagerInternal = Objects.requireNonNull( + LocalServices.getService(AlarmManagerInternal.class)); } @Override @@ -84,9 +90,22 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment { } @Override - public void setSystemClock(long newTimeMillis) { + public @TimeConfidence int systemClockConfidence() { + return SystemClockTime.getTimeConfidence(); + } + + @Override + public void setSystemClock( + @CurrentTimeMillisLong long newTimeMillis, @TimeConfidence int confidence, + @NonNull String logMsg) { + checkWakeLockHeld(); + mAlarmManagerInternal.setTime(newTimeMillis, confidence, logMsg); + } + + @Override + public void setSystemClockConfidence(@TimeConfidence int confidence, @NonNull String logMsg) { checkWakeLockHeld(); - mAlarmManager.setTime(newTimeMillis); + SystemClockTime.setConfidence(confidence, logMsg); } @Override @@ -100,4 +119,13 @@ final class EnvironmentImpl implements TimeDetectorStrategyImpl.Environment { Slog.wtf(LOG_TAG, "WakeLock " + mWakeLock + " not held"); } } -} + + @Override + public void addDebugLogEntry(@NonNull String logMsg) { + SystemClockTime.addDebugLogEntry(logMsg); + } + + @Override + public void dumpDebugLog(@NonNull PrintWriter printWriter) { + SystemClockTime.dump(printWriter); + }} diff --git a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java index 888304aa199b..0ea5f7a105d1 100644 --- a/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java +++ b/services/core/java/com/android/server/timedetector/ServiceConfigAccessorImpl.java @@ -68,6 +68,15 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { private static final int SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS_DEFAULT = 2 * 1000; /** + * An absolute threshold at/below which the system clock confidence can be upgraded. i.e. if the + * detector receives a high-confidence time and the current system clock is +/- this value from + * that time and the confidence in the time is low, then the device's confidence in the current + * system clock time can be upgraded. This needs to be an amount users would consider + * "close enough". + */ + private static final int SYSTEM_CLOCK_CONFIRMATION_THRESHOLD_MILLIS = 1000; + + /** * By default telephony and network only suggestions are accepted and telephony takes * precedence over network. */ @@ -236,6 +245,8 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { .setAutoDetectionSupported(isAutoDetectionSupported()) .setAutoDetectionEnabledSetting(getAutoDetectionEnabledSetting()) .setSystemClockUpdateThresholdMillis(getSystemClockUpdateThresholdMillis()) + .setSystemClockConfidenceUpgradeThresholdMillis( + getSystemClockConfidenceUpgradeThresholdMillis()) .setAutoSuggestionLowerBound(getAutoSuggestionLowerBound()) .setManualSuggestionLowerBound(timeDetectorHelper.getManualSuggestionLowerBound()) .setSuggestionUpperBound(timeDetectorHelper.getSuggestionUpperBound()) @@ -285,6 +296,10 @@ final class ServiceConfigAccessorImpl implements ServiceConfigAccessor { return mSystemClockUpdateThresholdMillis; } + private int getSystemClockConfidenceUpgradeThresholdMillis() { + return SYSTEM_CLOCK_CONFIRMATION_THRESHOLD_MILLIS; + } + @NonNull private Instant getAutoSuggestionLowerBound() { return mServerFlags.getOptionalInstant(KEY_TIME_DETECTOR_LOWER_BOUND_MILLIS_OVERRIDE) diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java index 547cf9d32aa1..fe2760e9ce10 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java @@ -16,6 +16,7 @@ package com.android.server.timedetector; +import static com.android.server.SystemClockTime.TIME_CONFIDENCE_HIGH; import static com.android.server.timedetector.TimeDetectorStrategy.originToString; import android.annotation.CurrentTimeMillisLong; @@ -23,7 +24,6 @@ import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.app.AlarmManager; import android.app.time.ExternalTimeSuggestion; import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.TelephonyTimeSuggestion; @@ -31,24 +31,24 @@ import android.content.Context; import android.os.Handler; import android.os.TimestampedValue; import android.util.IndentingPrintWriter; -import android.util.LocalLog; import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.SystemClockTime; +import com.android.server.SystemClockTime.TimeConfidence; import com.android.server.timezonedetector.ArrayMapWithHistory; import com.android.server.timezonedetector.ConfigurationChangeListener; import com.android.server.timezonedetector.ReferenceWithHistory; +import java.io.PrintWriter; import java.time.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Objects; /** - * An implementation of {@link TimeDetectorStrategy} that passes telephony and manual suggestions to - * {@link AlarmManager}. When there are multiple telephony sources, the one with the lowest ID is - * used unless the data becomes too stale. + * The real implementation of {@link TimeDetectorStrategy}. * * <p>Most public methods are marked synchronized to ensure thread safety around internal state. */ @@ -84,13 +84,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { */ private static final int KEEP_SUGGESTION_HISTORY_SIZE = 10; - /** - * A log that records the decisions / decision metadata that affected the device's system clock - * time. This is logged in bug reports to assist with debugging issues with detection. - */ - @NonNull - private final LocalLog mTimeChangesLog = new LocalLog(30, false /* useLocalTimestamps */); - @NonNull private final Environment mEnvironment; @@ -157,11 +150,30 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { @CurrentTimeMillisLong long systemClockMillis(); - /** Sets the device system clock. The WakeLock must be held. */ - void setSystemClock(@CurrentTimeMillisLong long newTimeMillis); + /** Returns the system clock confidence value. */ + @TimeConfidence int systemClockConfidence(); + + /** Sets the device system clock and confidence. The WakeLock must be held. */ + void setSystemClock( + @CurrentTimeMillisLong long newTimeMillis, @TimeConfidence int confidence, + @NonNull String logMsg); + + /** Sets the device system clock confidence. The WakeLock must be held. */ + void setSystemClockConfidence(@TimeConfidence int confidence, @NonNull String logMsg); /** Release the wake lock acquired by a call to {@link #acquireWakeLock()}. */ void releaseWakeLock(); + + + /** + * Adds a standalone entry to the time debug log. + */ + void addDebugLogEntry(@NonNull String logMsg); + + /** + * Dumps the time debug log to the supplied {@link PrintWriter}. + */ + void dumpDebugLog(PrintWriter printWriter); } static TimeDetectorStrategy create( @@ -250,7 +262,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } String cause = "Manual time suggestion received: suggestion=" + suggestion; - return setSystemClockIfRequired(ORIGIN_MANUAL, newUnixEpochTime, cause); + return setSystemClockAndConfidenceIfRequired(ORIGIN_MANUAL, newUnixEpochTime, cause); } @Override @@ -318,7 +330,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { String logMsg = "handleConfigurationInternalChanged:" + " oldConfiguration=" + mCurrentConfigurationInternal + ", newConfiguration=" + currentUserConfig; - logTimeDetectorChange(logMsg); + addDebugLogEntry(logMsg); mCurrentConfigurationInternal = currentUserConfig; boolean autoDetectionEnabled = @@ -335,11 +347,11 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } } - private void logTimeDetectorChange(@NonNull String logMsg) { + private void addDebugLogEntry(@NonNull String logMsg) { if (DBG) { Slog.d(LOG_TAG, logMsg); } - mTimeChangesLog.log(logMsg); + mEnvironment.addDebugLogEntry(logMsg); } @Override @@ -356,10 +368,11 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { long systemClockMillis = mEnvironment.systemClockMillis(); ipw.printf("mEnvironment.systemClockMillis()=%s (%s)\n", Instant.ofEpochMilli(systemClockMillis), systemClockMillis); + ipw.println("mEnvironment.systemClockConfidence()=" + mEnvironment.systemClockConfidence()); ipw.println("Time change log:"); ipw.increaseIndent(); // level 2 - mTimeChangesLog.dump(ipw); + SystemClockTime.dump(ipw); ipw.decreaseIndent(); // level 2 ipw.println("Telephony suggestion history:"); @@ -494,11 +507,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { @GuardedBy("this") private void doAutoTimeDetection(@NonNull String detectionReason) { - if (!mCurrentConfigurationInternal.getAutoDetectionEnabledBehavior()) { - // Avoid doing unnecessary work with this (race-prone) check. - return; - } - // Try the different origins one at a time. int[] originPriorities = mCurrentConfigurationInternal.getAutoOriginPriorities(); for (int origin : originPriorities) { @@ -544,7 +552,14 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { // Update the system clock if a good suggestion has been found. if (newUnixEpochTime != null) { - setSystemClockIfRequired(origin, newUnixEpochTime, cause); + if (mCurrentConfigurationInternal.getAutoDetectionEnabledBehavior()) { + setSystemClockAndConfidenceIfRequired(origin, newUnixEpochTime, cause); + } else { + // An automatically detected time can be used to raise the confidence in the + // current time even if the device is set to only allow user input for the time + // itself. + upgradeSystemClockConfidenceIfRequired(newUnixEpochTime, cause); + } return; } } @@ -718,14 +733,18 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } @GuardedBy("this") - private boolean setSystemClockIfRequired( + private boolean setSystemClockAndConfidenceIfRequired( @Origin int origin, @NonNull TimestampedValue<Long> time, @NonNull String cause) { + // Any time set through this class is inherently high confidence. Either it came directly + // from a user, or it was detected automatically. + @TimeConfidence final int newTimeConfidence = TIME_CONFIDENCE_HIGH; boolean isOriginAutomatic = isOriginAutomatic(origin); if (isOriginAutomatic) { if (!mCurrentConfigurationInternal.getAutoDetectionEnabledBehavior()) { if (DBG) { - Slog.d(LOG_TAG, "Auto time detection is not enabled." + Slog.d(LOG_TAG, + "Auto time detection is not enabled / no confidence update is needed." + " origin=" + originToString(origin) + ", time=" + time + ", cause=" + cause); @@ -746,7 +765,57 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { mEnvironment.acquireWakeLock(); try { - return setSystemClockUnderWakeLock(origin, time, cause); + return setSystemClockAndConfidenceUnderWakeLock(origin, time, newTimeConfidence, cause); + } finally { + mEnvironment.releaseWakeLock(); + } + } + + /** + * Upgrades the system clock confidence if the current time matches the supplied auto-detected + * time. The method never changes the system clock and it never lowers the confidence. It only + * raises the confidence if the supplied time is within the configured threshold of the current + * system clock time. + */ + @GuardedBy("this") + private void upgradeSystemClockConfidenceIfRequired( + @NonNull TimestampedValue<Long> autoDetectedUnixEpochTime, @NonNull String cause) { + @TimeConfidence int newTimeConfidence = TIME_CONFIDENCE_HIGH; + @TimeConfidence int currentTimeConfidence = mEnvironment.systemClockConfidence(); + boolean timeNeedsConfirmation = currentTimeConfidence < newTimeConfidence; + if (!timeNeedsConfirmation) { + return; + } + + // All system clock calculation take place under a wake lock. + mEnvironment.acquireWakeLock(); + try { + // Check if the specified time matches the current system clock time (closely + // enough) to raise the confidence. + long elapsedRealtimeMillis = mEnvironment.elapsedRealtimeMillis(); + long actualSystemClockMillis = mEnvironment.systemClockMillis(); + long adjustedAutoDetectedUnixEpochMillis = TimeDetectorStrategy.getTimeAt( + autoDetectedUnixEpochTime, elapsedRealtimeMillis); + long absTimeDifferenceMillis = + Math.abs(adjustedAutoDetectedUnixEpochMillis - actualSystemClockMillis); + int confidenceUpgradeThresholdMillis = + mCurrentConfigurationInternal.getSystemClockConfidenceUpgradeThresholdMillis(); + boolean updateConfidenceRequired = + absTimeDifferenceMillis <= confidenceUpgradeThresholdMillis; + if (updateConfidenceRequired) { + String logMsg = "Upgrade system clock confidence." + + " autoDetectedUnixEpochTime=" + autoDetectedUnixEpochTime + + " newTimeConfidence=" + newTimeConfidence + + " cause=" + cause + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + " (old) actualSystemClockMillis=" + actualSystemClockMillis + + " currentTimeConfidence=" + currentTimeConfidence; + if (DBG) { + Slog.d(LOG_TAG, logMsg); + } + + mEnvironment.setSystemClockConfidence(newTimeConfidence, logMsg); + } } finally { mEnvironment.releaseWakeLock(); } @@ -757,8 +826,9 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } @GuardedBy("this") - private boolean setSystemClockUnderWakeLock( - @Origin int origin, @NonNull TimestampedValue<Long> newTime, @NonNull String cause) { + private boolean setSystemClockAndConfidenceUnderWakeLock( + @Origin int origin, @NonNull TimestampedValue<Long> newTime, + @TimeConfidence int newTimeConfidence, @NonNull String cause) { long elapsedRealtimeMillis = mEnvironment.elapsedRealtimeMillis(); boolean isOriginAutomatic = isOriginAutomatic(origin); @@ -776,6 +846,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { "System clock has not tracked elapsed real time clock. A clock may" + " be inaccurate or something unexpectedly set the system" + " clock." + + " origin=" + originToString(origin) + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + " expectedTimeMillis=" + expectedTimeMillis + " actualTimeMillis=" + actualSystemClockMillis @@ -784,44 +855,72 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } } + // If the new signal would make sufficient difference to the system clock or mean a change + // in confidence then system state must be updated. + // Adjust for the time that has elapsed since the signal was received. long newSystemClockMillis = TimeDetectorStrategy.getTimeAt(newTime, elapsedRealtimeMillis); - - // Check if the new signal would make sufficient difference to the system clock. If it's - // below the threshold then ignore it. long absTimeDifference = Math.abs(newSystemClockMillis - actualSystemClockMillis); long systemClockUpdateThreshold = mCurrentConfigurationInternal.getSystemClockUpdateThresholdMillis(); - if (absTimeDifference < systemClockUpdateThreshold) { + boolean updateSystemClockRequired = absTimeDifference >= systemClockUpdateThreshold; + + @TimeConfidence int currentTimeConfidence = mEnvironment.systemClockConfidence(); + boolean updateConfidenceRequired = newTimeConfidence > currentTimeConfidence; + + if (updateSystemClockRequired) { + String logMsg = "Set system clock & confidence." + + " origin=" + originToString(origin) + + " newTime=" + newTime + + " newTimeConfidence=" + newTimeConfidence + + " cause=" + cause + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + " (old) actualSystemClockMillis=" + actualSystemClockMillis + + " newSystemClockMillis=" + newSystemClockMillis + + " currentTimeConfidence=" + currentTimeConfidence; + mEnvironment.setSystemClock(newSystemClockMillis, newTimeConfidence, logMsg); if (DBG) { - Slog.d(LOG_TAG, "Not setting system clock. New time and" - + " system clock are close enough." - + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + Slog.d(LOG_TAG, logMsg); + } + + // CLOCK_PARANOIA : Record the last time this class set the system clock due to an + // auto-time signal, or clear the record it is being done manually. + if (isOriginAutomatic(origin)) { + mLastAutoSystemClockTimeSet = newTime; + } else { + mLastAutoSystemClockTimeSet = null; + } + } else if (updateConfidenceRequired) { + // Only the confidence needs updating. This path is separate from a system clock update + // to deliberately avoid touching the system clock's value when it's not needed. Doing + // so could introduce inaccuracies or cause unnecessary wear in RTC hardware or + // associated storage. + String logMsg = "Set system clock confidence." + + " origin=" + originToString(origin) + + " newTime=" + newTime + + " newTimeConfidence=" + newTimeConfidence + + " cause=" + cause + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + + " (old) actualSystemClockMillis=" + actualSystemClockMillis + + " newSystemClockMillis=" + newSystemClockMillis + + " currentTimeConfidence=" + currentTimeConfidence; + if (DBG) { + Slog.d(LOG_TAG, logMsg); + } + mEnvironment.setSystemClockConfidence(newTimeConfidence, logMsg); + } else { + // Neither clock nor confidence need updating. + if (DBG) { + Slog.d(LOG_TAG, "Not setting system clock or confidence." + + " origin=" + originToString(origin) + " newTime=" + newTime + + " newTimeConfidence=" + newTimeConfidence + " cause=" + cause + + " elapsedRealtimeMillis=" + elapsedRealtimeMillis + " systemClockUpdateThreshold=" + systemClockUpdateThreshold - + " absTimeDifference=" + absTimeDifference); + + " absTimeDifference=" + absTimeDifference + + " currentTimeConfidence=" + currentTimeConfidence); } - return true; - } - - mEnvironment.setSystemClock(newSystemClockMillis); - String logMsg = "Set system clock using time=" + newTime - + " cause=" + cause - + " elapsedRealtimeMillis=" + elapsedRealtimeMillis - + " (old) actualSystemClockMillis=" + actualSystemClockMillis - + " newSystemClockMillis=" + newSystemClockMillis; - if (DBG) { - Slog.d(LOG_TAG, logMsg); - } - mTimeChangesLog.log(logMsg); - - // CLOCK_PARANOIA : Record the last time this class set the system clock due to an auto-time - // signal, or clear the record it is being done manually. - if (isOriginAutomatic(origin)) { - mLastAutoSystemClockTimeSet = newTime; - } else { - mLastAutoSystemClockTimeSet = null; } return true; } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 5d6ffd8b8ead..7e93d623f3e0 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -53,6 +53,7 @@ cc_library_static { "com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker.cpp", "com_android_server_stats_pull_StatsPullAtomService.cpp", "com_android_server_storage_AppFuseBridge.cpp", + "com_android_server_SystemClockTime.cpp", "com_android_server_SystemServer.cpp", "com_android_server_tv_TvUinputBridge.cpp", "com_android_server_tv_TvInputHal.cpp", diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS index 9abf107c780a..2584b86f53db 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -12,6 +12,7 @@ per-file com_android_server_power_PowerManagerService.* = michaelwr@google.com, per-file com_android_server_am_BatteryStatsService.cpp = file:/BATTERY_STATS_OWNERS per-file Android.bp = file:platform/build/soong:/OWNERS #{LAST_RESORT_SUGGESTION} +per-file com_android_server_SystemClock* = file:/services/core/java/com/android/server/timedetector/OWNERS per-file com_android_server_Usb* = file:/services/usb/OWNERS per-file com_android_server_Vibrator* = file:/services/core/java/com/android/server/vibrator/OWNERS per-file com_android_server_hdmi_* = file:/core/java/android/hardware/hdmi/OWNERS diff --git a/services/core/jni/com_android_server_SystemClockTime.cpp b/services/core/jni/com_android_server_SystemClockTime.cpp new file mode 100644 index 000000000000..9db4c429f0c7 --- /dev/null +++ b/services/core/jni/com_android_server_SystemClockTime.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "SystemClockTime" + +#include <android-base/file.h> +#include <android-base/unique_fd.h> +#include <linux/rtc.h> +#include <nativehelper/JNIHelp.h> +#include <utils/Log.h> +#include <utils/String8.h> + +#include "jni.h" + +namespace android { + +class SystemClockImpl { +public: + SystemClockImpl(const std::string &rtc_dev) : rtc_dev{rtc_dev} {} + + int setTime(struct timeval *tv); + +private: + std::string rtc_dev; +}; + +int SystemClockImpl::setTime(struct timeval *tv) { + if (settimeofday(tv, NULL) == -1) { + ALOGV("settimeofday() failed: %s", strerror(errno)); + return -1; + } + + android::base::unique_fd fd{open(rtc_dev.c_str(), O_RDWR)}; + if (!fd.ok()) { + ALOGE("Unable to open %s: %s", rtc_dev.c_str(), strerror(errno)); + return -1; + } + + struct tm tm; + if (!gmtime_r(&tv->tv_sec, &tm)) { + ALOGV("gmtime_r() failed: %s", strerror(errno)); + return -1; + } + + struct rtc_time rtc = {}; + rtc.tm_sec = tm.tm_sec; + rtc.tm_min = tm.tm_min; + rtc.tm_hour = tm.tm_hour; + rtc.tm_mday = tm.tm_mday; + rtc.tm_mon = tm.tm_mon; + rtc.tm_year = tm.tm_year; + rtc.tm_wday = tm.tm_wday; + rtc.tm_yday = tm.tm_yday; + rtc.tm_isdst = tm.tm_isdst; + if (ioctl(fd, RTC_SET_TIME, &rtc) == -1) { + ALOGV("RTC_SET_TIME ioctl failed: %s", strerror(errno)); + return -1; + } + + return 0; +} + +static jlong com_android_server_SystemClockTime_init(JNIEnv *, jobject) { + // Find the wall clock RTC. We expect this always to be /dev/rtc0, but + // check the /dev/rtc symlink first so that legacy devices that don't use + // rtc0 can add a symlink rather than need to carry a local patch to this + // code. + // + // TODO: if you're reading this in a world where all devices are using the + // GKI, you can remove the readlink and just assume /dev/rtc0. + std::string dev_rtc; + if (!android::base::Readlink("/dev/rtc", &dev_rtc)) { + dev_rtc = "/dev/rtc0"; + } + + std::unique_ptr<SystemClockImpl> system_clock{new SystemClockImpl(dev_rtc)}; + return reinterpret_cast<jlong>(system_clock.release()); +} + +static jint com_android_server_SystemClockTime_setTime(JNIEnv *, jobject, jlong nativeData, + jlong millis) { + SystemClockImpl *impl = reinterpret_cast<SystemClockImpl *>(nativeData); + + if (millis <= 0 || millis / 1000LL >= std::numeric_limits<time_t>::max()) { + return -1; + } + + struct timeval tv; + tv.tv_sec = (millis / 1000LL); + tv.tv_usec = ((millis % 1000LL) * 1000LL); + + ALOGD("Setting time of day to sec=%ld", tv.tv_sec); + + int ret = impl->setTime(&tv); + if (ret < 0) { + ALOGW("Unable to set rtc to %ld: %s", tv.tv_sec, strerror(errno)); + ret = -1; + } + return ret; +} + +static const JNINativeMethod sMethods[] = { + /* name, signature, funcPtr */ + {"init", "()J", (void *)com_android_server_SystemClockTime_init}, + {"setTime", "(JJ)I", (void *)com_android_server_SystemClockTime_setTime}, +}; + +int register_com_android_server_SystemClockTime(JNIEnv *env) { + return jniRegisterNativeMethods(env, "com/android/server/SystemClockTime", sMethods, + NELEM(sMethods)); +} + +} /* namespace android */ diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 00bef0935308..00f851f9f4ff 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -65,6 +65,7 @@ int register_android_server_companion_virtual_InputController(JNIEnv* env); int register_android_server_app_GameManagerService(JNIEnv* env); int register_com_android_server_wm_TaskFpsCallbackController(JNIEnv* env); int register_com_android_server_display_DisplayControl(JNIEnv* env); +int register_com_android_server_SystemClockTime(JNIEnv* env); }; using namespace android; @@ -122,5 +123,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_app_GameManagerService(env); register_com_android_server_wm_TaskFpsCallbackController(env); register_com_android_server_display_DisplayControl(env); + register_com_android_server_SystemClockTime(env); return JNI_VERSION_1_4; } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index d8f282aee5a8..9e449aedf0d9 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -331,6 +331,8 @@ public final class SystemServer implements Dumpable { "com.android.server.wallpaper.WallpaperManagerService$Lifecycle"; private static final String AUTO_FILL_MANAGER_SERVICE_CLASS = "com.android.server.autofill.AutofillManagerService"; + private static final String CREDENTIAL_MANAGER_SERVICE_CLASS = + "com.android.server.credentials.CredentialManagerService"; private static final String CONTENT_CAPTURE_MANAGER_SERVICE_CLASS = "com.android.server.contentcapture.ContentCaptureManagerService"; private static final String TRANSLATION_MANAGER_SERVICE_CLASS = @@ -2571,6 +2573,12 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_CREDENTIALS)) { + t.traceBegin("StartCredentialManagerService"); + mSystemServiceManager.startService(CREDENTIAL_MANAGER_SERVICE_CLASS); + t.traceEnd(); + } + // Translation manager service if (deviceHasConfigString(context, R.string.config_defaultTranslationService)) { t.traceBegin("StartTranslationManagerService"); diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java index 1b9282d426d0..830e2ac029c2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java @@ -168,6 +168,7 @@ import com.android.server.AppStateTracker; import com.android.server.AppStateTrackerImpl; import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; +import com.android.server.SystemClockTime.TimeConfidence; import com.android.server.SystemService; import com.android.server.pm.permission.PermissionManagerService; import com.android.server.pm.permission.PermissionManagerServiceInternal; @@ -346,10 +347,6 @@ public class AlarmManagerServiceTest { } @Override - void setKernelTime(long millis) { - } - - @Override int getSystemUiUid(PackageManagerInternal unused) { return SYSTEM_UI_UID; } @@ -361,7 +358,18 @@ public class AlarmManagerServiceTest { } @Override - long getElapsedRealtime() { + void initializeTimeIfRequired() { + // No-op + } + + @Override + void setCurrentTimeMillis(long unixEpochMillis, + @TimeConfidence int confidence, String logMsg) { + mNowRtcTest = unixEpochMillis; + } + + @Override + long getElapsedRealtimeMillis() { return mNowElapsedTest; } diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 96c3823f8349..2d2c76c40b10 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -94,6 +94,7 @@ import android.view.Display; import androidx.test.filters.SmallTest; +import com.android.internal.widget.LockPatternUtils; import com.android.server.FgThread; import com.android.server.SystemService; import com.android.server.am.UserState.KeyEvictedCallback; @@ -834,8 +835,7 @@ public class UserControllerTest { private void setUpAndStartUserInBackground(int userId) throws Exception { setUpUser(userId, 0); mUserController.startUser(userId, /* foreground= */ false); - verify(mInjector.mStorageManagerMock, times(1)) - .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null); + verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId); mUserStates.put(userId, mUserController.getStartedUserState(userId)); } @@ -843,8 +843,7 @@ public class UserControllerTest { setUpUser(userId, UserInfo.FLAG_PROFILE, false, UserManager.USER_TYPE_PROFILE_MANAGED); assertThat(mUserController.startProfile(userId)).isTrue(); - verify(mInjector.mStorageManagerMock, times(1)) - .unlockUserKey(userId, /* serialNumber= */ 0, /* secret= */ null); + verify(mInjector.mLockPatternUtilsMock, times(1)).unlockUserKeyIfUnsecured(userId); mUserStates.put(userId, mUserController.getStartedUserState(userId)); } @@ -966,6 +965,7 @@ public class UserControllerTest { private final UserManagerInternal mUserManagerInternalMock; private final WindowManagerService mWindowManagerMock; private final KeyguardManager mKeyguardManagerMock; + private final LockPatternUtils mLockPatternUtilsMock; private final Context mCtx; @@ -982,6 +982,7 @@ public class UserControllerTest { mStorageManagerMock = mock(IStorageManager.class); mKeyguardManagerMock = mock(KeyguardManager.class); when(mKeyguardManagerMock.isDeviceSecure(anyInt())).thenReturn(true); + mLockPatternUtilsMock = mock(LockPatternUtils.class); } @Override @@ -1081,6 +1082,11 @@ public class UserControllerTest { protected void dismissKeyguard(Runnable runnable, String reason) { runnable.run(); } + + @Override + protected LockPatternUtils getLockPatternUtils() { + return mLockPatternUtilsMock; + } } private static class TestHandler extends Handler { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java new file mode 100644 index 000000000000..47b4bf547ff8 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthResultCoordinatorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import static com.google.common.truth.Truth.assertThat; + +import android.hardware.biometrics.BiometricManager; + +import org.junit.Before; +import org.junit.Test; + +public class AuthResultCoordinatorTest { + private AuthResultCoordinator mAuthResultCoordinator; + + @Before + public void setUp() throws Exception { + mAuthResultCoordinator = new AuthResultCoordinator(); + } + + @Test + public void testDefaultMessage() { + checkResult(mAuthResultCoordinator.getResult(), + AuthResult.FAILED, + BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE); + } + + @Test + public void testSingleMessageCoordinator() { + mAuthResultCoordinator.authenticatedFor( + BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE); + checkResult(mAuthResultCoordinator.getResult(), + AuthResult.AUTHENTICATED, + BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE); + } + + @Test + public void testLockout() { + mAuthResultCoordinator.lockedOutFor( + BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE); + checkResult(mAuthResultCoordinator.getResult(), + AuthResult.LOCKED_OUT, + BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE); + } + + @Test + public void testHigherStrengthPrecedence() { + mAuthResultCoordinator.authenticatedFor( + BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE); + mAuthResultCoordinator.authenticatedFor( + BiometricManager.Authenticators.BIOMETRIC_WEAK); + checkResult(mAuthResultCoordinator.getResult(), + AuthResult.AUTHENTICATED, + BiometricManager.Authenticators.BIOMETRIC_WEAK); + + mAuthResultCoordinator.authenticatedFor( + BiometricManager.Authenticators.BIOMETRIC_STRONG); + checkResult(mAuthResultCoordinator.getResult(), + AuthResult.AUTHENTICATED, + BiometricManager.Authenticators.BIOMETRIC_STRONG); + } + + @Test + public void testAuthPrecedence() { + mAuthResultCoordinator.authenticatedFor( + BiometricManager.Authenticators.BIOMETRIC_WEAK); + mAuthResultCoordinator.lockedOutFor( + BiometricManager.Authenticators.BIOMETRIC_WEAK); + checkResult(mAuthResultCoordinator.getResult(), + AuthResult.AUTHENTICATED, + BiometricManager.Authenticators.BIOMETRIC_WEAK); + + } + + void checkResult(AuthResult res, int status, + @BiometricManager.Authenticators.Types int strength) { + assertThat(res.getStatus()).isEqualTo(status); + assertThat(res.getBiometricStrength()).isEqualTo(strength); + } + +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java new file mode 100644 index 000000000000..9bb0f58db520 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AuthSessionCoordinatorTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +@Presubmit +@SmallTest +public class AuthSessionCoordinatorTest { + private static final int PRIMARY_USER = 0; + private static final int SECONDARY_USER = 10; + + private AuthSessionCoordinator mCoordinator; + + @Before + public void setUp() throws Exception { + mCoordinator = new AuthSessionCoordinator(); + } + + @Test + public void testUserUnlocked() { + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse(); + + mCoordinator.authStartedFor(PRIMARY_USER, 1); + mCoordinator.authenticatedFor(PRIMARY_USER, BIOMETRIC_WEAK, 1); + + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse(); + } + + @Test + public void testUserCanAuthDuringLockoutOfSameSession() { + mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG); + + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue(); + + mCoordinator.authStartedFor(PRIMARY_USER, 1); + mCoordinator.authStartedFor(PRIMARY_USER, 2); + mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2); + + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + } + + @Test + public void testMultiUserAuth() { + mCoordinator.resetLockoutFor(PRIMARY_USER, BIOMETRIC_STRONG); + + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue(); + + assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse(); + assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse(); + assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse(); + + mCoordinator.authStartedFor(PRIMARY_USER, 1); + mCoordinator.authStartedFor(PRIMARY_USER, 2); + mCoordinator.lockedOutFor(PRIMARY_USER, BIOMETRIC_WEAK, 2); + + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.getCanAuthFor(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + + assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_CONVENIENCE)).isFalse(); + assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_WEAK)).isFalse(); + assertThat(mCoordinator.getCanAuthFor(SECONDARY_USER, BIOMETRIC_STRONG)).isFalse(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java new file mode 100644 index 000000000000..8baa1ce3f6c6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/MultiBiometricLockoutStateTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics.sensors; + +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_WEAK; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@Presubmit +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class MultiBiometricLockoutStateTest { + private static final int PRIMARY_USER = 0; + private MultiBiometricLockoutState mCoordinator; + + private void unlockAllBiometrics() { + unlockAllBiometrics(mCoordinator, PRIMARY_USER); + } + + private void lockoutAllBiometrics() { + lockoutAllBiometrics(mCoordinator, PRIMARY_USER); + } + + private static void unlockAllBiometrics(MultiBiometricLockoutState coordinator, int userId) { + coordinator.onUserUnlocked(userId, BIOMETRIC_STRONG); + assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isTrue(); + assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isTrue(); + assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isTrue(); + } + + private static void lockoutAllBiometrics(MultiBiometricLockoutState coordinator, int userId) { + coordinator.onUserLocked(userId, BIOMETRIC_STRONG); + assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_STRONG)).isFalse(); + assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_WEAK)).isFalse(); + assertThat(coordinator.canUserAuthenticate(userId, BIOMETRIC_CONVENIENCE)).isFalse(); + } + + @Before + public void setUp() throws Exception { + mCoordinator = new MultiBiometricLockoutState(); + } + + @Test + public void testInitialStateLockedOut() { + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse(); + } + + @Test + public void testConvenienceLockout() { + unlockAllBiometrics(); + mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_CONVENIENCE); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse(); + } + + @Test + public void testWeakLockout() { + unlockAllBiometrics(); + mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_WEAK); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse(); + } + + @Test + public void testStrongLockout() { + unlockAllBiometrics(); + mCoordinator.onUserLocked(PRIMARY_USER, BIOMETRIC_STRONG); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isFalse(); + } + + @Test + public void testConvenienceUnlock() { + lockoutAllBiometrics(); + mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_CONVENIENCE); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + } + + @Test + public void testWeakUnlock() { + lockoutAllBiometrics(); + mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_WEAK); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isFalse(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + } + + @Test + public void testStrongUnlock() { + lockoutAllBiometrics(); + mCoordinator.onUserUnlocked(PRIMARY_USER, BIOMETRIC_STRONG); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_STRONG)).isTrue(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_WEAK)).isTrue(); + assertThat(mCoordinator.canUserAuthenticate(PRIMARY_USER, BIOMETRIC_CONVENIENCE)).isTrue(); + } + + @Test + public void multiUser_userOneDoesNotAffectUserTwo() { + final int userOne = 1; + final int userTwo = 2; + MultiBiometricLockoutState coordinator = new MultiBiometricLockoutState(); + lockoutAllBiometrics(coordinator, userOne); + lockoutAllBiometrics(coordinator, userTwo); + + coordinator.onUserUnlocked(userOne, BIOMETRIC_WEAK); + assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_STRONG)).isFalse(); + assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_WEAK)).isTrue(); + assertThat(coordinator.canUserAuthenticate(userOne, BIOMETRIC_CONVENIENCE)).isTrue(); + + assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_STRONG)).isFalse(); + assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_WEAK)).isFalse(); + assertThat(coordinator.canUserAuthenticate(userTwo, BIOMETRIC_CONVENIENCE)).isFalse(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 57ded9905273..6388c7d52f10 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -236,10 +236,9 @@ public class VirtualDeviceManagerServiceTest { .setBlockedActivities(getBlockedActivities()) .build(); mDeviceImpl = new VirtualDeviceImpl(mContext, - mAssociationInfo, new Binder(), /* uid */ 0, mInputController, - (int associationId) -> { - }, mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, - params); + mAssociationInfo, new Binder(), /* ownerUid */ 0, /* uniqueId */ 1, + mInputController, (int associationId) -> {}, mPendingTrampolineCallback, + mActivityListener, mRunningAppsChangedCallback, params); } @Test diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java index e220841a3816..c934e653564e 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java @@ -168,16 +168,15 @@ public abstract class BaseLockSettingsServiceTests { allUsers.add(SECONDARY_USER_INFO); when(mUserManager.getUsers()).thenReturn(allUsers); - when(mActivityManager.unlockUser(anyInt(), any(), any(), any())).thenAnswer( - new Answer<Boolean>() { - @Override - public Boolean answer(InvocationOnMock invocation) throws Throwable { + when(mActivityManager.unlockUser2(anyInt(), any())).thenAnswer( + invocation -> { Object[] args = invocation.getArguments(); - mStorageManager.unlockUser((int)args[0], (byte[])args[2], - (IProgressListener) args[3]); + int userId = (int) args[0]; + IProgressListener listener = (IProgressListener) args[1]; + listener.onStarted(userId, null); + listener.onFinished(userId, null); return true; - } - }); + }); // Adding a fake Device Owner app which will enable escrow token support in LSS. when(mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser()).thenReturn( @@ -215,37 +214,20 @@ public abstract class BaseLockSettingsServiceTests { private IStorageManager setUpStorageManagerMock() throws RemoteException { final IStorageManager sm = mock(IStorageManager.class); - doAnswer(new Answer<Void>() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - mStorageManager.addUserKeyAuth((int) args[0] /* userId */, - (int) args[1] /* serialNumber */, - (byte[]) args[2] /* secret */); - return null; - } - }).when(sm).addUserKeyAuth(anyInt(), anyInt(), any()); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + mStorageManager.unlockUserKey(/* userId= */ (int) args[0], + /* secret= */ (byte[]) args[2]); + return null; + }).when(sm).unlockUserKey(anyInt(), anyInt(), any()); - doAnswer(new Answer<Void>() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - mStorageManager.clearUserKeyAuth((int) args[0] /* userId */, - (int) args[1] /* serialNumber */, - (byte[]) args[2] /* secret */); - return null; - } - }).when(sm).clearUserKeyAuth(anyInt(), anyInt(), any()); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + mStorageManager.setUserKeyProtection(/* userId= */ (int) args[0], + /* secret= */ (byte[]) args[1]); + return null; + }).when(sm).setUserKeyProtection(anyInt(), any()); - doAnswer( - new Answer<Void>() { - @Override - public Void answer(InvocationOnMock invocation) throws Throwable { - Object[] args = invocation.getArguments(); - mStorageManager.fixateNewestUserKeyAuth((int) args[0] /* userId */); - return null; - } - }).when(sm).fixateNewestUserKeyAuth(anyInt()); return sm; } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java index 619ef7078c24..91f3fed01267 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/FakeStorageManager.java @@ -16,75 +16,26 @@ package com.android.server.locksettings; -import android.os.IProgressListener; -import android.os.RemoteException; -import android.util.ArrayMap; - - -import junit.framework.AssertionFailedError; +import static com.google.common.truth.Truth.assertThat; -import java.util.ArrayList; -import java.util.Arrays; +import android.util.ArrayMap; public class FakeStorageManager { - private ArrayMap<Integer, ArrayList<byte[]>> mAuth = new ArrayMap<>(); - private boolean mIgnoreBadUnlock; - - public void addUserKeyAuth(int userId, int serialNumber, byte[] secret) { - getUserAuth(userId).add(secret); - } - - public void clearUserKeyAuth(int userId, int serialNumber, byte[] secret) { - ArrayList<byte[]> auths = getUserAuth(userId); - if (secret == null) { - return; - } - auths.remove(secret); - auths.add(null); - } - - public void fixateNewestUserKeyAuth(int userId) { - ArrayList<byte[]> auths = mAuth.get(userId); - byte[] latest = auths.get(auths.size() - 1); - auths.clear(); - auths.add(latest); - } + private final ArrayMap<Integer, byte[]> mUserSecrets = new ArrayMap<>(); - private ArrayList<byte[]> getUserAuth(int userId) { - if (!mAuth.containsKey(userId)) { - ArrayList<byte[]> auths = new ArrayList<>(); - auths.add(null); - mAuth.put(userId, auths); - } - return mAuth.get(userId); + public void setUserKeyProtection(int userId, byte[] secret) { + assertThat(mUserSecrets).doesNotContainKey(userId); + mUserSecrets.put(userId, secret); } public byte[] getUserUnlockToken(int userId) { - ArrayList<byte[]> auths = getUserAuth(userId); - if (auths.size() != 1) { - throw new AssertionFailedError("More than one secret exists"); - } - return auths.get(0); - } - - public void unlockUser(int userId, byte[] secret, IProgressListener listener) - throws RemoteException { - listener.onStarted(userId, null); - listener.onFinished(userId, null); - ArrayList<byte[]> auths = getUserAuth(userId); - if (auths.size() > 1) { - throw new AssertionFailedError("More than one secret exists"); - } - byte[] auth = auths.get(0); - if (!Arrays.equals(secret, auth)) { - if (!mIgnoreBadUnlock) { - throw new AssertionFailedError("Invalid secret to unlock user " + userId); - } - } + byte[] secret = mUserSecrets.get(userId); + assertThat(secret).isNotNull(); + return secret; } - public void setIgnoreBadUnlock(boolean ignore) { - mIgnoreBadUnlock = ignore; + public void unlockUserKey(int userId, byte[] secret) { + assertThat(mUserSecrets.get(userId)).isEqualTo(secret); } } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java index 3477288f4cff..3f259e343ee6 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java @@ -145,15 +145,9 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { // Verify that profile which aren't running (e.g. turn off work) don't get unlocked assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID)); - /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new - * credential as part of verifyCredential() before the new credential is committed in - * StorageManager. So we relax the check in our mock StorageManager to allow that. - */ - mStorageManager.setIgnoreBadUnlock(true); // Change primary password and verify that profile SID remains assertTrue(mService.setLockCredential( secondUnifiedPassword, firstUnifiedPassword, PRIMARY_USER_ID)); - mStorageManager.setIgnoreBadUnlock(false); assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID)); @@ -172,15 +166,9 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertTrue(mService.setLockCredential(primaryPassword, nonePassword(), PRIMARY_USER_ID)); - /* Currently in LockSettingsService.setLockCredential, unlockUser() is called with the new - * credential as part of verifyCredential() before the new credential is committed in - * StorageManager. So we relax the check in our mock StorageManager to allow that. - */ - mStorageManager.setIgnoreBadUnlock(true); assertTrue(mService.setLockCredential(profilePassword, nonePassword(), MANAGED_PROFILE_USER_ID)); - mStorageManager.setIgnoreBadUnlock(false); final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID); @@ -203,11 +191,8 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertNotNull(mGateKeeperService.getAuthToken(MANAGED_PROFILE_USER_ID)); assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); - // Change primary credential and make sure we don't affect profile - mStorageManager.setIgnoreBadUnlock(true); assertTrue(mService.setLockCredential( newPassword("pwd"), primaryPassword, PRIMARY_USER_ID)); - mStorageManager.setIgnoreBadUnlock(false); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( profilePassword, MANAGED_PROFILE_USER_ID, 0 /* flags */) .getResponseCode()); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java index 87beece5b414..5e6cccc07983 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java @@ -27,6 +27,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.never; @@ -126,6 +127,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { initializeCredential(password, PRIMARY_USER_ID); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode()); + verify(mActivityManager).unlockUser2(eq(PRIMARY_USER_ID), any()); assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, mService.verifyCredential( badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode()); @@ -187,32 +189,13 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { } @Test - public void testNoSyntheticPasswordOrCredentialDoesNotPassAuthSecret() throws RemoteException { - mService.onUnlockUser(PRIMARY_USER_ID); - flushHandlerTasks(); - verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class)); - } - - @Test - public void testCredentialDoesNotPassAuthSecret() throws RemoteException { - LockscreenCredential password = newPassword("password"); - initializeCredential(password, PRIMARY_USER_ID); - - reset(mAuthSecretService); - mService.onUnlockUser(PRIMARY_USER_ID); - flushHandlerTasks(); - verify(mAuthSecretService, never()).primaryUserCredential(any(ArrayList.class)); - } - - @Test - public void testSyntheticPasswordButNoCredentialPassesAuthSecret() throws RemoteException { + public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException { LockscreenCredential password = newPassword("password"); initializeCredential(password, PRIMARY_USER_ID); mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID); reset(mAuthSecretService); - mService.onUnlockUser(PRIMARY_USER_ID); - flushHandlerTasks(); + mLocalService.unlockUserKeyIfUnsecured(PRIMARY_USER_ID); verify(mAuthSecretService).primaryUserCredential(any(ArrayList.class)); } diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java index 1aea6727d3a1..060c31f2985e 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java @@ -16,6 +16,8 @@ package com.android.server.timedetector; +import static com.android.server.SystemClockTime.TIME_CONFIDENCE_HIGH; +import static com.android.server.SystemClockTime.TIME_CONFIDENCE_LOW; import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_EXTERNAL; import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_GNSS; import static com.android.server.timedetector.TimeDetectorStrategy.ORIGIN_NETWORK; @@ -35,6 +37,7 @@ import android.os.TimestampedValue; import androidx.test.runner.AndroidJUnit4; +import com.android.server.SystemClockTime.TimeConfidence; import com.android.server.timedetector.TimeDetectorStrategy.Origin; import com.android.server.timezonedetector.ConfigurationChangeListener; @@ -42,6 +45,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.PrintWriter; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; @@ -86,6 +90,7 @@ public class TimeDetectorStrategyImplTest { .setAutoDetectionSupported(true) .setSystemClockUpdateThresholdMillis( ARBITRARY_SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS) + .setSystemClockUpdateThresholdMillis(TIME_CONFIDENCE_HIGH) .setAutoSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND) .setManualSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND) .setSuggestionUpperBound(DEFAULT_SUGGESTION_UPPER_BOUND) @@ -99,6 +104,7 @@ public class TimeDetectorStrategyImplTest { .setAutoDetectionSupported(true) .setSystemClockUpdateThresholdMillis( ARBITRARY_SYSTEM_CLOCK_UPDATE_THRESHOLD_MILLIS) + .setSystemClockUpdateThresholdMillis(TIME_CONFIDENCE_HIGH) .setAutoSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND) .setManualSuggestionLowerBound(DEFAULT_SUGGESTION_LOWER_BOUND) .setSuggestionUpperBound(DEFAULT_SUGGESTION_UPPER_BOUND) @@ -112,12 +118,14 @@ public class TimeDetectorStrategyImplTest { public void setUp() { mFakeEnvironment = new FakeEnvironment(); mFakeEnvironment.initializeConfig(CONFIG_AUTO_DISABLED); - mFakeEnvironment.initializeFakeClocks(ARBITRARY_CLOCK_INITIALIZATION_INFO); + mFakeEnvironment.initializeFakeClocks( + ARBITRARY_CLOCK_INITIALIZATION_INFO, TIME_CONFIDENCE_LOW); } @Test public void testSuggestTelephonyTime_autoTimeEnabled() { - Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED); + Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); int slotIndex = ARBITRARY_SLOT_INDEX; Instant testTime = ARBITRARY_TEST_TIME; @@ -129,18 +137,21 @@ public class TimeDetectorStrategyImplTest { long expectedSystemClockMillis = script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime()); - script.verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis) + script.verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis) .assertLatestTelephonySuggestion(slotIndex, timeSuggestion); } @Test public void testSuggestTelephonyTime_emptySuggestionIgnored() { - Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED); + Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); int slotIndex = ARBITRARY_SLOT_INDEX; TelephonyTimeSuggestion timeSuggestion = script.generateTelephonyTimeSuggestion(slotIndex, null); script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, null); } @@ -278,17 +289,115 @@ public class TimeDetectorStrategyImplTest { } } + /** + * If an auto suggested time matches the current system clock, the confidence in the current + * system clock is raised even when auto time is disabled. The system clock itself must not be + * changed. + */ @Test - public void testSuggestTelephonyTime_autoTimeDisabled() { - Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED); + public void testSuggestTelephonyTime_autoTimeDisabled_suggestionMatchesSystemClock() { + TimestampedValue<Instant> initialClockTime = ARBITRARY_CLOCK_INITIALIZATION_INFO; + final int confidenceUpgradeThresholdMillis = 1000; + ConfigurationInternal configInternal = + new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) + .setSystemClockConfidenceUpgradeThresholdMillis( + confidenceUpgradeThresholdMillis) + .build(); + Script script = new Script() + .pokeFakeClocks(initialClockTime, TIME_CONFIDENCE_LOW) + .simulateConfigurationInternalChange(configInternal); int slotIndex = ARBITRARY_SLOT_INDEX; - TelephonyTimeSuggestion timeSuggestion = - script.generateTelephonyTimeSuggestion(slotIndex, ARBITRARY_TEST_TIME); - script.simulateTimePassing() - .simulateTelephonyTimeSuggestion(timeSuggestion) - .verifySystemClockWasNotSetAndResetCallTracking() - .assertLatestTelephonySuggestion(slotIndex, timeSuggestion); + + script.simulateTimePassing(); + long timeElapsedMillis = + script.peekElapsedRealtimeMillis() - initialClockTime.getReferenceTimeMillis(); + + // Create a suggestion time that approximately matches the current system clock. + Instant suggestionInstant = initialClockTime.getValue() + .plusMillis(timeElapsedMillis) + .plusMillis(confidenceUpgradeThresholdMillis); + TimestampedValue<Long> matchingClockTime = new TimestampedValue<>( + script.peekElapsedRealtimeMillis(), + suggestionInstant.toEpochMilli()); + TelephonyTimeSuggestion timeSuggestion = new TelephonyTimeSuggestion.Builder(slotIndex) + .setUnixEpochTime(matchingClockTime) + .build(); + script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) + .verifySystemClockWasNotSetAndResetCallTracking(); + } + + /** + * If an auto suggested time doesn't match the current system clock, the confidence in the + * current system clock will stay where it is. The system clock itself must not be changed. + */ + @Test + public void testSuggestTelephonyTime_autoTimeDisabled_suggestionMismatchesSystemClock() { + TimestampedValue<Instant> initialClockTime = ARBITRARY_CLOCK_INITIALIZATION_INFO; + final int confidenceUpgradeThresholdMillis = 1000; + ConfigurationInternal configInternal = + new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) + .setSystemClockConfidenceUpgradeThresholdMillis( + confidenceUpgradeThresholdMillis) + .build(); + Script script = new Script().pokeFakeClocks(initialClockTime, TIME_CONFIDENCE_LOW) + .simulateConfigurationInternalChange(configInternal); + + int slotIndex = ARBITRARY_SLOT_INDEX; + + script.simulateTimePassing(); + long timeElapsedMillis = + script.peekElapsedRealtimeMillis() - initialClockTime.getReferenceTimeMillis(); + + // Create a suggestion time that doesn't match the current system clock closely enough. + Instant suggestionInstant = initialClockTime.getValue() + .plusMillis(timeElapsedMillis) + .plusMillis(confidenceUpgradeThresholdMillis + 1); + TimestampedValue<Long> mismatchingClockTime = new TimestampedValue<>( + script.peekElapsedRealtimeMillis(), + suggestionInstant.toEpochMilli()); + TelephonyTimeSuggestion timeSuggestion = new TelephonyTimeSuggestion.Builder(slotIndex) + .setUnixEpochTime(mismatchingClockTime) + .build(); + script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) + .verifySystemClockWasNotSetAndResetCallTracking(); + } + + /** + * If a suggested time doesn't match the current system clock, the confidence in the current + * system clock will not drop. + */ + @Test + public void testSuggestTelephonyTime_autoTimeDisabled_suggestionMismatchesSystemClock2() { + TimestampedValue<Instant> initialClockTime = ARBITRARY_CLOCK_INITIALIZATION_INFO; + final int confidenceUpgradeThresholdMillis = 1000; + ConfigurationInternal configInternal = + new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) + .setSystemClockConfidenceUpgradeThresholdMillis( + confidenceUpgradeThresholdMillis) + .build(); + Script script = new Script().pokeFakeClocks(initialClockTime, TIME_CONFIDENCE_HIGH) + .simulateConfigurationInternalChange(configInternal); + + int slotIndex = ARBITRARY_SLOT_INDEX; + + script.simulateTimePassing(); + long timeElapsedMillis = + script.peekElapsedRealtimeMillis() - initialClockTime.getReferenceTimeMillis(); + + // Create a suggestion time that doesn't closely match the current system clock. + Instant initialClockInstant = initialClockTime.getValue(); + TimestampedValue<Long> mismatchingClockTime = new TimestampedValue<>( + script.peekElapsedRealtimeMillis(), + initialClockInstant.plusMillis(timeElapsedMillis + 1_000_000).toEpochMilli()); + TelephonyTimeSuggestion timeSuggestion = new TelephonyTimeSuggestion.Builder(slotIndex) + .setUnixEpochTime(mismatchingClockTime) + .build(); + script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) + .verifySystemClockWasNotSetAndResetCallTracking(); } @Test @@ -298,7 +407,8 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setSystemClockUpdateThresholdMillis(systemClockUpdateThresholdMillis) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant testTime = ARBITRARY_TEST_TIME; int slotIndex = ARBITRARY_SLOT_INDEX; @@ -311,6 +421,7 @@ public class TimeDetectorStrategyImplTest { script.simulateTimePassing(); long expectedSystemClockMillis1 = script.calculateTimeInMillisForNow(unixEpochTime1); script.simulateTelephonyTimeSuggestion(timeSuggestion1) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1) .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1); @@ -327,6 +438,7 @@ public class TimeDetectorStrategyImplTest { TelephonyTimeSuggestion timeSuggestion2 = createTelephonyTimeSuggestion(slotIndex, unixEpochTime2); script.simulateTelephonyTimeSuggestion(timeSuggestion2) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1); @@ -339,6 +451,7 @@ public class TimeDetectorStrategyImplTest { TelephonyTimeSuggestion timeSuggestion3 = createTelephonyTimeSuggestion(slotIndex, unixEpochTime3); script.simulateTelephonyTimeSuggestion(timeSuggestion3) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1); @@ -350,6 +463,7 @@ public class TimeDetectorStrategyImplTest { TelephonyTimeSuggestion timeSuggestion4 = createTelephonyTimeSuggestion(slotIndex, unixEpochTime4); script.simulateTelephonyTimeSuggestion(timeSuggestion4) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis4) .assertLatestTelephonySuggestion(slotIndex, timeSuggestion4); } @@ -362,7 +476,8 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) .setSystemClockUpdateThresholdMillis(systemClockUpdateThresholdMillis) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); int slotIndex = ARBITRARY_SLOT_INDEX; Instant testTime = ARBITRARY_TEST_TIME; @@ -376,6 +491,7 @@ public class TimeDetectorStrategyImplTest { // Simulate the time signal being received. It should not be used because auto time // detection is off but it should be recorded. script.simulateTelephonyTimeSuggestion(timeSuggestion1) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1); @@ -386,11 +502,13 @@ public class TimeDetectorStrategyImplTest { // Turn on auto time detection. script.simulateAutoTimeDetectionToggle() + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis1) .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1); // Turn off auto time detection. script.simulateAutoTimeDetectionToggle() + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, timeSuggestion1); @@ -408,18 +526,21 @@ public class TimeDetectorStrategyImplTest { // The new time, though valid, should not be set in the system clock because auto time is // disabled. script.simulateTelephonyTimeSuggestion(timeSuggestion2) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, timeSuggestion2); // Turn on auto time detection. script.simulateAutoTimeDetectionToggle() + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis2) .assertLatestTelephonySuggestion(slotIndex, timeSuggestion2); } @Test public void testSuggestTelephonyTime_maxSuggestionAge() { - Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED); + Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); int slotIndex = ARBITRARY_SLOT_INDEX; Instant testTime = ARBITRARY_TEST_TIME; @@ -431,6 +552,7 @@ public class TimeDetectorStrategyImplTest { long expectedSystemClockMillis = script.calculateTimeInMillisForNow(telephonySuggestion.getUnixEpochTime()); script.simulateTelephonyTimeSuggestion(telephonySuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking( expectedSystemClockMillis /* expectedNetworkBroadcast */) .assertLatestTelephonySuggestion(slotIndex, telephonySuggestion); @@ -442,7 +564,7 @@ public class TimeDetectorStrategyImplTest { script.simulateTimePassing(TimeDetectorStrategyImpl.MAX_SUGGESTION_TIME_AGE_MILLIS); // Look inside and check what the strategy considers the current best telephony suggestion. - // It should still be the, it's just no longer used. + // It should still be there, it's just no longer used. assertNull(script.peekBestTelephonySuggestion()); script.assertLatestTelephonySuggestion(slotIndex, telephonySuggestion); } @@ -454,12 +576,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_TELEPHONY) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1); TelephonyTimeSuggestion timeSuggestion = script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, belowLowerBound); script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -470,12 +594,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_TELEPHONY) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1); TelephonyTimeSuggestion timeSuggestion = script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, aboveLowerBound); script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli()); } @@ -486,12 +612,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_TELEPHONY) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1); TelephonyTimeSuggestion timeSuggestion = script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, aboveUpperBound); script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -502,18 +630,21 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_TELEPHONY) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1); TelephonyTimeSuggestion timeSuggestion = script.generateTelephonyTimeSuggestion(ARBITRARY_SLOT_INDEX, belowUpperBound); script.simulateTelephonyTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli()); } @Test public void testSuggestManualTime_autoTimeDisabled() { - Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED); + Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_DISABLED) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(ARBITRARY_TEST_TIME); @@ -524,12 +655,14 @@ public class TimeDetectorStrategyImplTest { script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime()); script.simulateManualTimeSuggestion( ARBITRARY_USER_ID, timeSuggestion, true /* expectedResult */) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); } @Test public void testSuggestManualTime_retainsAutoSignal() { - Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED); + Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); int slotIndex = ARBITRARY_SLOT_INDEX; @@ -544,6 +677,7 @@ public class TimeDetectorStrategyImplTest { long expectedAutoClockMillis = script.calculateTimeInMillisForNow(telephonyTimeSuggestion.getUnixEpochTime()); script.simulateTelephonyTimeSuggestion(telephonyTimeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedAutoClockMillis) .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion); @@ -552,6 +686,7 @@ public class TimeDetectorStrategyImplTest { // Switch to manual. script.simulateAutoTimeDetectionToggle() + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion); @@ -568,6 +703,7 @@ public class TimeDetectorStrategyImplTest { script.calculateTimeInMillisForNow(manualTimeSuggestion.getUnixEpochTime()); script.simulateManualTimeSuggestion( ARBITRARY_USER_ID, manualTimeSuggestion, true /* expectedResult */) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedManualClockMillis) .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion); @@ -580,17 +716,20 @@ public class TimeDetectorStrategyImplTest { expectedAutoClockMillis = script.calculateTimeInMillisForNow(telephonyTimeSuggestion.getUnixEpochTime()); script.verifySystemClockWasSetAndResetCallTracking(expectedAutoClockMillis) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion); // Switch back to manual - nothing should happen to the clock. script.simulateAutoTimeDetectionToggle() + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasNotSetAndResetCallTracking() .assertLatestTelephonySuggestion(slotIndex, telephonyTimeSuggestion); } @Test public void testSuggestManualTime_isIgnored_whenAutoTimeEnabled() { - Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED); + Script script = new Script().simulateConfigurationInternalChange(CONFIG_AUTO_ENABLED) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(ARBITRARY_TEST_TIME); @@ -598,6 +737,7 @@ public class TimeDetectorStrategyImplTest { script.simulateTimePassing() .simulateManualTimeSuggestion( ARBITRARY_USER_ID, timeSuggestion, false /* expectedResult */) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -607,12 +747,14 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1); ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(aboveUpperBound); script.simulateManualTimeSuggestion( ARBITRARY_USER_ID, timeSuggestion, false /* expectedResult */) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -622,12 +764,14 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1); ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(belowUpperBound); script.simulateManualTimeSuggestion( ARBITRARY_USER_ID, timeSuggestion, true /* expectedResult */) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli()); } @@ -637,12 +781,14 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) .setManualSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1); ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(belowLowerBound); script.simulateManualTimeSuggestion( ARBITRARY_USER_ID, timeSuggestion, false /* expectedResult */) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -652,12 +798,14 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_DISABLED) .setManualSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1); ManualTimeSuggestion timeSuggestion = script.generateManualTimeSuggestion(aboveLowerBound); script.simulateManualTimeSuggestion( ARBITRARY_USER_ID, timeSuggestion, true /* expectedResult */) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli()); } @@ -667,7 +815,8 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_NETWORK) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME); @@ -677,6 +826,7 @@ public class TimeDetectorStrategyImplTest { long expectedSystemClockMillis = script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime()); script.simulateNetworkTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); } @@ -703,12 +853,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_NETWORK) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1); NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(belowLowerBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -719,12 +871,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_NETWORK) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1); NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(aboveLowerBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli()); } @@ -735,12 +889,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_NETWORK) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1); NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(aboveUpperBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -751,12 +907,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_NETWORK) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1); NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(belowUpperBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli()); } @@ -766,7 +924,8 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_GNSS) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); GnssTimeSuggestion timeSuggestion = script.generateGnssTimeSuggestion(ARBITRARY_TEST_TIME); @@ -776,6 +935,7 @@ public class TimeDetectorStrategyImplTest { long expectedSystemClockMillis = script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime()); script.simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); } @@ -802,12 +962,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_GNSS) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1); GnssTimeSuggestion timeSuggestion = script.generateGnssTimeSuggestion(belowLowerBound); script.simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -818,12 +980,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_GNSS) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1); GnssTimeSuggestion timeSuggestion = script.generateGnssTimeSuggestion(aboveLowerBound); script.simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli()); } @@ -834,12 +998,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_GNSS) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1); GnssTimeSuggestion timeSuggestion = script.generateGnssTimeSuggestion(aboveUpperBound); script.simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -850,12 +1016,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_GNSS) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1); GnssTimeSuggestion timeSuggestion = script.generateGnssTimeSuggestion(belowUpperBound); script.simulateGnssTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli()); } @@ -865,7 +1033,8 @@ public class TimeDetectorStrategyImplTest { new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_EXTERNAL) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); ExternalTimeSuggestion timeSuggestion = script.generateExternalTimeSuggestion(ARBITRARY_TEST_TIME); @@ -875,6 +1044,7 @@ public class TimeDetectorStrategyImplTest { long expectedSystemClockMillis = script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime()); script.simulateExternalTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); } @@ -901,12 +1071,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_EXTERNAL) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowLowerBound = TEST_SUGGESTION_LOWER_BOUND.minusSeconds(1); ExternalTimeSuggestion timeSuggestion = script.generateExternalTimeSuggestion(belowLowerBound); script.simulateExternalTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -917,12 +1089,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_EXTERNAL) .setAutoSuggestionLowerBound(TEST_SUGGESTION_LOWER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveLowerBound = TEST_SUGGESTION_LOWER_BOUND.plusSeconds(1); ExternalTimeSuggestion timeSuggestion = script.generateExternalTimeSuggestion(aboveLowerBound); script.simulateExternalTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli()); } @@ -933,12 +1107,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_EXTERNAL) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant aboveUpperBound = TEST_SUGGESTION_UPPER_BOUND.plusSeconds(1); ExternalTimeSuggestion timeSuggestion = script.generateExternalTimeSuggestion(aboveUpperBound); script.simulateExternalTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -949,12 +1125,14 @@ public class TimeDetectorStrategyImplTest { .setOriginPriorities(ORIGIN_EXTERNAL) .setSuggestionUpperBound(TEST_SUGGESTION_UPPER_BOUND) .build(); - Script script = new Script().simulateConfigurationInternalChange(configInternal); + Script script = new Script().simulateConfigurationInternalChange(configInternal) + .verifySystemClockConfidence(TIME_CONFIDENCE_LOW); Instant belowUpperBound = TEST_SUGGESTION_UPPER_BOUND.minusSeconds(1); ExternalTimeSuggestion timeSuggestion = script.generateExternalTimeSuggestion(belowUpperBound); script.simulateExternalTimeSuggestion(timeSuggestion) + .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli()); } @@ -1472,6 +1650,7 @@ public class TimeDetectorStrategyImplTest { private boolean mWakeLockAcquired; private long mElapsedRealtimeMillis; private long mSystemClockMillis; + private int mSystemClockConfidence = TIME_CONFIDENCE_LOW; private ConfigurationChangeListener mConfigurationInternalChangeListener; // Tracking operations. @@ -1481,9 +1660,10 @@ public class TimeDetectorStrategyImplTest { mConfigurationInternal = configurationInternal; } - public void initializeFakeClocks(TimestampedValue<Instant> timeInfo) { + public void initializeFakeClocks( + TimestampedValue<Instant> timeInfo, @TimeConfidence int timeConfidence) { pokeElapsedRealtimeMillis(timeInfo.getReferenceTimeMillis()); - pokeSystemClockMillis(timeInfo.getValue().toEpochMilli()); + pokeSystemClockMillis(timeInfo.getValue().toEpochMilli(), timeConfidence); } @Override @@ -1515,10 +1695,23 @@ public class TimeDetectorStrategyImplTest { } @Override - public void setSystemClock(long newTimeMillis) { + public @TimeConfidence int systemClockConfidence() { + return mSystemClockConfidence; + } + + @Override + public void setSystemClock( + long newTimeMillis, @TimeConfidence int confidence, String logMsg) { assertWakeLockAcquired(); mSystemClockWasSet = true; mSystemClockMillis = newTimeMillis; + mSystemClockConfidence = confidence; + } + + @Override + public void setSystemClockConfidence(@TimeConfidence int confidence, String logMsg) { + assertWakeLockAcquired(); + mSystemClockConfidence = confidence; } @Override @@ -1527,6 +1720,16 @@ public class TimeDetectorStrategyImplTest { mWakeLockAcquired = false; } + @Override + public void addDebugLogEntry(String logMsg) { + // No-op for tests + } + + @Override + public void dumpDebugLog(PrintWriter printWriter) { + // No-op for tests + } + // Methods below are for managing the fake's behavior. void simulateConfigurationInternalChange(ConfigurationInternal configurationInternal) { @@ -1538,8 +1741,9 @@ public class TimeDetectorStrategyImplTest { mElapsedRealtimeMillis = elapsedRealtimeMillis; } - void pokeSystemClockMillis(long systemClockMillis) { + void pokeSystemClockMillis(long systemClockMillis, @TimeConfidence int timeConfidence) { mSystemClockMillis = systemClockMillis; + mSystemClockConfidence = timeConfidence; } long peekElapsedRealtimeMillis() { @@ -1567,6 +1771,10 @@ public class TimeDetectorStrategyImplTest { assertEquals(expectedSystemClockMillis, mSystemClockMillis); } + public void verifySystemClockConfidence(@TimeConfidence int expectedConfidence) { + assertEquals(expectedConfidence, mSystemClockConfidence); + } + void resetCallTracking() { mSystemClockWasSet = false; } @@ -1589,6 +1797,14 @@ public class TimeDetectorStrategyImplTest { mTimeDetectorStrategy = new TimeDetectorStrategyImpl(mFakeEnvironment); } + Script pokeFakeClocks(TimestampedValue<Instant> initialClockTime, + @TimeConfidence int timeConfidence) { + mFakeEnvironment.pokeElapsedRealtimeMillis(initialClockTime.getReferenceTimeMillis()); + mFakeEnvironment.pokeSystemClockMillis( + initialClockTime.getValue().toEpochMilli(), timeConfidence); + return this; + } + long peekElapsedRealtimeMillis() { return mFakeEnvironment.peekElapsedRealtimeMillis(); } @@ -1675,6 +1891,11 @@ public class TimeDetectorStrategyImplTest { return this; } + Script verifySystemClockConfidence(@TimeConfidence int expectedConfidence) { + mFakeEnvironment.verifySystemClockConfidence(expectedConfidence); + return this; + } + /** * White box test info: Asserts the latest suggestion for the slotIndex is as expected. */ diff --git a/telephony/common/com/android/internal/telephony/SmsApplication.java b/telephony/common/com/android/internal/telephony/SmsApplication.java index 7b7c83f39c92..f848c4013fa9 100644 --- a/telephony/common/com/android/internal/telephony/SmsApplication.java +++ b/telephony/common/com/android/internal/telephony/SmsApplication.java @@ -190,12 +190,11 @@ public final class SmsApplication { } /** - * Returns the userId of the Context object, if called from a system app, + * Returns the userId of the current process, if called from a system app, * otherwise it returns the caller's userId - * @param context The context object passed in by the caller. - * @return + * @return userId of the caller. */ - private static int getIncomingUserId(Context context) { + private static int getIncomingUserId() { int contextUserId = UserHandle.myUserId(); final int callingUid = Binder.getCallingUid(); if (DEBUG_MULTIUSER) { @@ -231,7 +230,7 @@ public final class SmsApplication { */ @UnsupportedAppUsage public static Collection<SmsApplicationData> getApplicationCollection(Context context) { - return getApplicationCollectionAsUser(context, getIncomingUserId(context)); + return getApplicationCollectionAsUser(context, getIncomingUserId()); } /** @@ -590,7 +589,7 @@ public final class SmsApplication { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static void setDefaultApplication(String packageName, Context context) { - setDefaultApplicationAsUser(packageName, context, getIncomingUserId(context)); + setDefaultApplicationAsUser(packageName, context, getIncomingUserId()); } /** @@ -952,7 +951,7 @@ public final class SmsApplication { */ @UnsupportedAppUsage public static ComponentName getDefaultSmsApplication(Context context, boolean updateIfNeeded) { - return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId(context)); + return getDefaultSmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId()); } /** @@ -988,7 +987,18 @@ public final class SmsApplication { */ @UnsupportedAppUsage public static ComponentName getDefaultMmsApplication(Context context, boolean updateIfNeeded) { - int userId = getIncomingUserId(context); + return getDefaultMmsApplicationAsUser(context, updateIfNeeded, getIncomingUserId()); + } + + /** + * Gets the default MMS application on a given user + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured. + * @param userId target user ID. + * @return component name of the app and class to deliver MMS messages to. + */ + public static ComponentName getDefaultMmsApplicationAsUser(Context context, + boolean updateIfNeeded, int userId) { final long token = Binder.clearCallingIdentity(); try { ComponentName component = null; @@ -1013,7 +1023,19 @@ public final class SmsApplication { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static ComponentName getDefaultRespondViaMessageApplication(Context context, boolean updateIfNeeded) { - int userId = getIncomingUserId(context); + return getDefaultRespondViaMessageApplicationAsUser(context, updateIfNeeded, + getIncomingUserId()); + } + + /** + * Gets the default Respond Via Message application on a given user + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured + * @param userId target user ID. + * @return component name of the app and class to direct Respond Via Message intent to + */ + public static ComponentName getDefaultRespondViaMessageApplicationAsUser(Context context, + boolean updateIfNeeded, int userId) { final long token = Binder.clearCallingIdentity(); try { ComponentName component = null; @@ -1039,7 +1061,7 @@ public final class SmsApplication { */ public static ComponentName getDefaultSendToApplication(Context context, boolean updateIfNeeded) { - int userId = getIncomingUserId(context); + int userId = getIncomingUserId(); final long token = Binder.clearCallingIdentity(); try { ComponentName component = null; @@ -1064,7 +1086,20 @@ public final class SmsApplication { */ public static ComponentName getDefaultExternalTelephonyProviderChangedApplication( Context context, boolean updateIfNeeded) { - int userId = getIncomingUserId(context); + return getDefaultExternalTelephonyProviderChangedApplicationAsUser(context, updateIfNeeded, + getIncomingUserId()); + } + + /** + * Gets the default application that handles external changes to the SmsProvider and + * MmsProvider on a given user. + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured + * @param userId target user ID. + * @return component name of the app and class to deliver change intents to. + */ + public static ComponentName getDefaultExternalTelephonyProviderChangedApplicationAsUser( + Context context, boolean updateIfNeeded, int userId) { final long token = Binder.clearCallingIdentity(); try { ComponentName component = null; @@ -1089,7 +1124,18 @@ public final class SmsApplication { */ public static ComponentName getDefaultSimFullApplication( Context context, boolean updateIfNeeded) { - int userId = getIncomingUserId(context); + return getDefaultSimFullApplicationAsUser(context, updateIfNeeded, getIncomingUserId()); + } + + /** + * Gets the default application that handles sim full event on a given user. + * @param context context from the calling app + * @param updateIfNeeded update the default app if there is no valid default app configured + * @param userId target user ID. + * @return component name of the app and class to deliver change intents to + */ + public static ComponentName getDefaultSimFullApplicationAsUser(Context context, + boolean updateIfNeeded, int userId) { final long token = Binder.clearCallingIdentity(); try { ComponentName component = null; @@ -1114,7 +1160,12 @@ public final class SmsApplication { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public static boolean shouldWriteMessageForPackage(String packageName, Context context) { - return !isDefaultSmsApplication(context, packageName); + return !shouldWriteMessageForPackageAsUser(packageName, context, getIncomingUserId()); + } + + public static boolean shouldWriteMessageForPackageAsUser(String packageName, Context context, + int userId) { + return !isDefaultSmsApplicationAsUser(context, packageName, userId); } /** @@ -1126,28 +1177,42 @@ public final class SmsApplication { */ @UnsupportedAppUsage public static boolean isDefaultSmsApplication(Context context, String packageName) { + return isDefaultSmsApplicationAsUser(context, packageName, getIncomingUserId()); + } + + /** + * Check if a package is default sms app (or equivalent, like bluetooth) on a given user. + * + * @param context context from the calling app + * @param packageName the name of the package to be checked + * @param userId target user ID. + * @return true if the package is default sms app or bluetooth + */ + public static boolean isDefaultSmsApplicationAsUser(Context context, String packageName, + int userId) { if (packageName == null) { return false; } - final String defaultSmsPackage = getDefaultSmsApplicationPackageName(context); - final String bluetoothPackageName = context.getResources() + ComponentName component = getDefaultSmsApplicationAsUser(context, false, + userId); + if (component == null) { + return false; + } + + String defaultSmsPackage = component.getPackageName(); + if (defaultSmsPackage == null) { + return false; + } + + String bluetoothPackageName = context.getResources() .getString(com.android.internal.R.string.config_systemBluetoothStack); - if ((defaultSmsPackage != null && defaultSmsPackage.equals(packageName)) - || bluetoothPackageName.equals(packageName)) { + if (defaultSmsPackage.equals(packageName) || bluetoothPackageName.equals(packageName)) { return true; } return false; } - private static String getDefaultSmsApplicationPackageName(Context context) { - final ComponentName component = getDefaultSmsApplication(context, false); - if (component != null) { - return component.getPackageName(); - } - return null; - } - /** * Check if a package is default mms app (or equivalent, like bluetooth) * @@ -1157,25 +1222,40 @@ public final class SmsApplication { */ @UnsupportedAppUsage public static boolean isDefaultMmsApplication(Context context, String packageName) { + return isDefaultMmsApplicationAsUser(context, packageName, getIncomingUserId()); + } + + /** + * Check if a package is default mms app (or equivalent, like bluetooth) on a given user. + * + * @param context context from the calling app + * @param packageName the name of the package to be checked + * @param userId target user ID. + * @return true if the package is default mms app or bluetooth + */ + public static boolean isDefaultMmsApplicationAsUser(Context context, String packageName, + int userId) { if (packageName == null) { return false; } - String defaultMmsPackage = getDefaultMmsApplicationPackageName(context); + + ComponentName component = getDefaultMmsApplicationAsUser(context, false, + userId); + if (component == null) { + return false; + } + + String defaultMmsPackage = component.getPackageName(); + if (defaultMmsPackage == null) { + return false; + } + String bluetoothPackageName = context.getResources() .getString(com.android.internal.R.string.config_systemBluetoothStack); - if ((defaultMmsPackage != null && defaultMmsPackage.equals(packageName)) - || bluetoothPackageName.equals(packageName)) { + if (defaultMmsPackage.equals(packageName)|| bluetoothPackageName.equals(packageName)) { return true; } return false; } - - private static String getDefaultMmsApplicationPackageName(Context context) { - ComponentName component = getDefaultMmsApplication(context, false); - if (component != null) { - return component.getPackageName(); - } - return null; - } -} +}
\ No newline at end of file diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java new file mode 100644 index 000000000000..858cd7672fe3 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/GestureHelper.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.helpers; + +import android.annotation.NonNull; +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.os.SystemClock; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.MotionEvent; +import android.view.MotionEvent.PointerCoords; +import android.view.MotionEvent.PointerProperties; + +/** + * Injects gestures given an {@link Instrumentation} object. + */ +public class GestureHelper { + // Inserted after each motion event injection. + private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5; + + private final UiAutomation mUiAutomation; + + /** + * A pair of floating point values. + */ + public static class Tuple { + public float x; + public float y; + + public Tuple(float x, float y) { + this.x = x; + this.y = y; + } + } + + public GestureHelper(Instrumentation instrumentation) { + mUiAutomation = instrumentation.getUiAutomation(); + } + + /** + * Injects a series of {@link MotionEvent} objects to simulate a pinch gesture. + * + * @param startPoint1 initial coordinates of the first pointer + * @param startPoint2 initial coordinates of the second pointer + * @param endPoint1 final coordinates of the first pointer + * @param endPoint2 final coordinates of the second pointer + * @param steps number of steps to take to animate pinching + * @return true if gesture is injected successfully + */ + public boolean pinch(@NonNull Tuple startPoint1, @NonNull Tuple startPoint2, + @NonNull Tuple endPoint1, @NonNull Tuple endPoint2, int steps) { + PointerProperties ptrProp1 = getPointerProp(0, MotionEvent.TOOL_TYPE_FINGER); + PointerProperties ptrProp2 = getPointerProp(1, MotionEvent.TOOL_TYPE_FINGER); + + PointerCoords ptrCoord1 = getPointerCoord(startPoint1.x, startPoint1.y, 1, 1); + PointerCoords ptrCoord2 = getPointerCoord(startPoint2.x, startPoint2.y, 1, 1); + + PointerProperties[] ptrProps = new PointerProperties[] { + ptrProp1, ptrProp2 + }; + + PointerCoords[] ptrCoords = new PointerCoords[] { + ptrCoord1, ptrCoord2 + }; + + long downTime = SystemClock.uptimeMillis(); + + if (!primaryPointerDown(ptrProp1, ptrCoord1, downTime)) { + return false; + } + + if (!nonPrimaryPointerDown(ptrProps, ptrCoords, downTime, 1)) { + return false; + } + + if (!movePointers(ptrProps, ptrCoords, new Tuple[] { endPoint1, endPoint2 }, + downTime, steps)) { + return false; + } + + if (!nonPrimaryPointerUp(ptrProps, ptrCoords, downTime, 1)) { + return false; + } + + return primaryPointerUp(ptrProp1, ptrCoord1, downTime); + } + + private boolean primaryPointerDown(@NonNull PointerProperties prop, + @NonNull PointerCoords coord, long downTime) { + MotionEvent event = getMotionEvent(downTime, downTime, MotionEvent.ACTION_DOWN, 1, + new PointerProperties[]{ prop }, new PointerCoords[]{ coord }); + + return injectEventSync(event); + } + + private boolean nonPrimaryPointerDown(@NonNull PointerProperties[] props, + @NonNull PointerCoords[] coords, long downTime, int index) { + // at least 2 pointers are needed + if (props.length != coords.length || coords.length < 2) { + return false; + } + + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_DOWN + + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords); + + return injectEventSync(event); + } + + private boolean movePointers(@NonNull PointerProperties[] props, + @NonNull PointerCoords[] coords, @NonNull Tuple[] endPoints, long downTime, int steps) { + // the number of endpoints should be the same as the number of pointers + if (props.length != coords.length || coords.length != endPoints.length) { + return false; + } + + // prevent division by 0 and negative number of steps + if (steps < 1) { + steps = 1; + } + + // save the starting points before updating any pointers + Tuple[] startPoints = new Tuple[coords.length]; + + for (int i = 0; i < coords.length; i++) { + startPoints[i] = new Tuple(coords[i].x, coords[i].y); + } + + MotionEvent event; + long eventTime; + + for (int i = 0; i < steps; i++) { + // inject a delay between movements + SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS); + + // update the coordinates + for (int j = 0; j < coords.length; j++) { + coords[j].x += (endPoints[j].x - startPoints[j].x) / steps; + coords[j].y += (endPoints[j].y - startPoints[j].y) / steps; + } + + eventTime = SystemClock.uptimeMillis(); + + event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_MOVE, + coords.length, props, coords); + + boolean didInject = injectEventSync(event); + + if (!didInject) { + return false; + } + } + + return true; + } + + private boolean primaryPointerUp(@NonNull PointerProperties prop, + @NonNull PointerCoords coord, long downTime) { + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_UP, 1, + new PointerProperties[]{ prop }, new PointerCoords[]{ coord }); + + return injectEventSync(event); + } + + private boolean nonPrimaryPointerUp(@NonNull PointerProperties[] props, + @NonNull PointerCoords[] coords, long downTime, int index) { + // at least 2 pointers are needed + if (props.length != coords.length || coords.length < 2) { + return false; + } + + long eventTime = SystemClock.uptimeMillis(); + + MotionEvent event = getMotionEvent(downTime, eventTime, MotionEvent.ACTION_POINTER_UP + + (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT), coords.length, props, coords); + + return injectEventSync(event); + } + + private PointerCoords getPointerCoord(float x, float y, float pressure, float size) { + PointerCoords ptrCoord = new PointerCoords(); + ptrCoord.x = x; + ptrCoord.y = y; + ptrCoord.pressure = pressure; + ptrCoord.size = size; + return ptrCoord; + } + + private PointerProperties getPointerProp(int id, int toolType) { + PointerProperties ptrProp = new PointerProperties(); + ptrProp.id = id; + ptrProp.toolType = toolType; + return ptrProp; + } + + private static MotionEvent getMotionEvent(long downTime, long eventTime, int action, + int pointerCount, PointerProperties[] ptrProps, PointerCoords[] ptrCoords) { + return MotionEvent.obtain(downTime, eventTime, action, pointerCount, + ptrProps, ptrCoords, 0, 0, 1.0f, 1.0f, + 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0); + } + + private boolean injectEventSync(InputEvent event) { + return mUiAutomation.injectInputEvent(event, true); + } +} diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index e730f315392b..19ee09a81ec5 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -22,6 +22,7 @@ import android.media.session.MediaSessionManager import android.util.Log import androidx.test.uiautomator.By import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.GestureHelper.Tuple import com.android.server.wm.flicker.testapp.ActivityOptions import com.android.server.wm.traces.common.Rect import com.android.server.wm.traces.common.WindowManagerConditionsFactory @@ -44,6 +45,8 @@ open class PipAppHelper(instrumentation: Instrumentation) : get() = mediaSessionManager.getActiveSessions(null).firstOrNull { it.packageName == `package` } + private val gestureHelper: GestureHelper = GestureHelper(mInstrumentation) + open fun clickObject(resId: String) { val selector = By.res(`package`, resId) val obj = uiDevice.findObject(selector) ?: error("Could not find `$resId` object") @@ -52,6 +55,50 @@ open class PipAppHelper(instrumentation: Instrumentation) : } /** + * Expands the PIP window my using the pinch out gesture. + * + * @param percent The percentage by which to increase the pip window size. + * @throws IllegalArgumentException if percentage isn't between 0.0f and 1.0f + */ + fun pinchOpenPipWindow(wmHelper: WindowManagerStateHelper, percent: Float, steps: Int) { + // the percentage must be between 0.0f and 1.0f + if (percent <= 0.0f || percent > 1.0f) { + throw IllegalArgumentException("Percent must be between 0.0f and 1.0f") + } + + val windowRect = getWindowRect(wmHelper) + + // first pointer's initial x coordinate is halfway between the left edge and the center + val initLeftX = (windowRect.centerX() - windowRect.width / 4).toFloat() + // second pointer's initial x coordinate is halfway between the right edge and the center + val initRightX = (windowRect.centerX() + windowRect.width / 4).toFloat() + + // horizontal distance the window should increase by + val distIncrease = windowRect.width * percent + + // final x-coordinates + val finalLeftX = initLeftX - (distIncrease / 2) + val finalRightX = initRightX + (distIncrease / 2) + + // y-coordinate is the same throughout this animation + val yCoord = windowRect.centerY().toFloat() + + var adjustedSteps = MIN_STEPS_TO_ANIMATE + + // if distance per step is at least 1, then we can use the number of steps requested + if (distIncrease.toInt() / (steps * 2) >= 1) { + adjustedSteps = steps + } + + // if the distance per step is less than 1, carry out the animation in two steps + gestureHelper.pinch( + Tuple(initLeftX, yCoord), Tuple(initRightX, yCoord), + Tuple(finalLeftX, yCoord), Tuple(finalRightX, yCoord), adjustedSteps) + + waitForPipWindowToExpandFrom(wmHelper, Region.from(windowRect)) + } + + /** * Launches the app through an intent instead of interacting with the launcher and waits until * the app window is in PIP mode */ @@ -194,5 +241,8 @@ open class PipAppHelper(instrumentation: Instrumentation) : private const val MEDIA_SESSION_START_RADIO_BUTTON_ID = "media_session_start" private const val ENTER_PIP_ON_USER_LEAVE_HINT = "enter_pip_on_leave_manual" private const val ENTER_PIP_AUTOENTER = "enter_pip_on_leave_autoenter" + // minimum number of steps to take, when animating gestures, needs to be 2 + // so that there is at least a single intermediate layer that flicker tests can check + private const val MIN_STEPS_TO_ANIMATE = 2 } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt index d80cf4ee619c..80ab01624703 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt @@ -16,7 +16,6 @@ package com.android.server.wm.flicker.ime -import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerParametersRunnerFactory @@ -47,21 +46,21 @@ class SwitchImeWindowsFromGestureNavTest_ShellTransit(testSpec: FlickerTestParam Assume.assumeTrue(isShellTransitionsEnabled) } - @FlakyTest(bugId = 228012334) + @Presubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() - @FlakyTest(bugId = 228012334) + @Presubmit @Test override fun imeLayerIsVisibleWhenSwitchingToImeApp() = super.imeLayerIsVisibleWhenSwitchingToImeApp() - @FlakyTest(bugId = 228012334) + @Presubmit @Test override fun visibleLayersShownMoreThanOneConsecutiveEntry() = super.visibleLayersShownMoreThanOneConsecutiveEntry() - @FlakyTest(bugId = 228012334) + @Presubmit @Test override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = super.visibleWindowsShownMoreThanOneConsecutiveEntry() diff --git a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java index 052ce3a902c1..7a2af72c41d5 100644 --- a/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java +++ b/tests/TelephonyCommonTests/src/com/android/internal/telephony/tests/SmsApplicationTest.java @@ -153,6 +153,20 @@ public class SmsApplicationTest { } @Test + public void testGetDefaultMmsApplication() { + assertEquals(TEST_COMPONENT_NAME, + SmsApplication.getDefaultMmsApplicationAsUser(mContext, false, + UserHandle.USER_SYSTEM)); + } + + @Test + public void testGetDefaultExternalTelephonyProviderChangedApplication() { + assertEquals(TEST_COMPONENT_NAME, + SmsApplication.getDefaultExternalTelephonyProviderChangedApplicationAsUser(mContext, + false, UserHandle.USER_SYSTEM)); + } + + @Test public void testGetDefaultSmsApplicationWithAppOpsFix() throws Exception { when(mAppOpsManager.unsafeCheckOp(AppOpsManager.OPSTR_READ_SMS, SMS_APP_UID, TEST_COMPONENT_NAME.getPackageName())) diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt index 8aa3e2583071..4d69d26e46db 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/AndroidFrameworkIssueRegistry.kt @@ -42,6 +42,8 @@ class AndroidFrameworkIssueRegistry : IssueRegistry() { SaferParcelChecker.ISSUE_UNSAFE_API_USAGE, PackageVisibilityDetector.ISSUE_PACKAGE_NAME_NO_PACKAGE_VISIBILITY_FILTERS, RegisterReceiverFlagDetector.ISSUE_RECEIVER_EXPORTED_FLAG, + PermissionMethodDetector.ISSUE_PERMISSION_METHOD_USAGE, + PermissionMethodDetector.ISSUE_CAN_BE_PERMISSION_METHOD, ) override val api: Int diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt index 82eb8ed8f621..3d5d01c9b7a0 100644 --- a/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt +++ b/tools/lint/checks/src/main/java/com/google/android/lint/Constants.kt @@ -34,3 +34,7 @@ val ENFORCE_PERMISSION_METHODS = listOf( Method(CLASS_ACTIVITY_MANAGER_SERVICE, "checkPermission"), Method(CLASS_ACTIVITY_MANAGER_INTERNAL, "enforceCallingPermission") ) + +const val ANNOTATION_PERMISSION_METHOD = "android.content.pm.PermissionMethod" +const val ANNOTATION_PERMISSION_NAME = "android.content.pm.PermissionName" +const val ANNOTATION_PERMISSION_RESULT = "android.content.pm.PackageManager.PermissionResult" diff --git a/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt new file mode 100644 index 000000000000..68a450d956a8 --- /dev/null +++ b/tools/lint/checks/src/main/java/com/google/android/lint/PermissionMethodDetector.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.lint + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Implementation +import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.detector.api.JavaContext +import com.android.tools.lint.detector.api.Scope +import com.android.tools.lint.detector.api.Severity +import com.android.tools.lint.detector.api.SourceCodeScanner +import com.android.tools.lint.detector.api.getUMethod +import com.intellij.psi.PsiType +import org.jetbrains.uast.UAnnotation +import org.jetbrains.uast.UBlockExpression +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UIfExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.UReturnExpression +import org.jetbrains.uast.getContainingUMethod + +/** + * Stops incorrect usage of {@link PermissionMethod} + * TODO: add tests once re-enabled (b/240445172, b/247542171) + */ +class PermissionMethodDetector : Detector(), SourceCodeScanner { + + override fun getApplicableUastTypes(): List<Class<out UElement>> = + listOf(UAnnotation::class.java, UMethod::class.java) + + override fun createUastHandler(context: JavaContext): UElementHandler = + PermissionMethodHandler(context) + + private inner class PermissionMethodHandler(val context: JavaContext) : UElementHandler() { + override fun visitMethod(node: UMethod) { + if (hasPermissionMethodAnnotation(node)) return + if (onlyCallsPermissionMethod(node)) { + val location = context.getLocation(node.javaPsi.modifierList) + val fix = fix() + .annotate(ANNOTATION_PERMISSION_METHOD) + .range(location) + .autoFix() + .build() + + context.report( + ISSUE_CAN_BE_PERMISSION_METHOD, + location, + "Annotate method with @PermissionMethod", + fix + ) + } + } + + override fun visitAnnotation(node: UAnnotation) { + if (node.qualifiedName != ANNOTATION_PERMISSION_METHOD) return + val method = node.getContainingUMethod() ?: return + + if (!isPermissionMethodReturnType(method)) { + context.report( + ISSUE_PERMISSION_METHOD_USAGE, + context.getLocation(node), + """ + Methods annotated with `@PermissionMethod` should return `void`, \ + `boolean`, or `@PackageManager.PermissionResult int`." + """.trimIndent() + ) + } + + if (method.returnType == PsiType.INT && + method.annotations.none { it.hasQualifiedName(ANNOTATION_PERMISSION_RESULT) } + ) { + context.report( + ISSUE_PERMISSION_METHOD_USAGE, + context.getLocation(node), + """ + Methods annotated with `@PermissionMethod` that return `int` should \ + also be annotated with `@PackageManager.PermissionResult.`" + """.trimIndent() + ) + } + } + } + + companion object { + + private val EXPLANATION_PERMISSION_METHOD_USAGE = """ + `@PermissionMethod` should annotate methods that ONLY perform permission lookups. \ + Said methods should return `boolean`, `@PackageManager.PermissionResult int`, or return \ + `void` and potentially throw `SecurityException`. + """.trimIndent() + + @JvmField + val ISSUE_PERMISSION_METHOD_USAGE = Issue.create( + id = "PermissionMethodUsage", + briefDescription = "@PermissionMethod used incorrectly", + explanation = EXPLANATION_PERMISSION_METHOD_USAGE, + category = Category.CORRECTNESS, + priority = 5, + severity = Severity.ERROR, + implementation = Implementation( + PermissionMethodDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = true + ) + + private val EXPLANATION_CAN_BE_PERMISSION_METHOD = """ + Methods that only call other methods annotated with @PermissionMethod (and do NOTHING else) can themselves \ + be annotated with @PermissionMethod. For example: + ``` + void wrapperHelper() { + // Context.enforceCallingPermission is annotated with @PermissionMethod + context.enforceCallingPermission(SOME_PERMISSION) + } + ``` + """.trimIndent() + + @JvmField + val ISSUE_CAN_BE_PERMISSION_METHOD = Issue.create( + id = "CanBePermissionMethod", + briefDescription = "Method can be annotated with @PermissionMethod", + explanation = EXPLANATION_CAN_BE_PERMISSION_METHOD, + category = Category.SECURITY, + priority = 5, + severity = Severity.WARNING, + implementation = Implementation( + PermissionMethodDetector::class.java, + Scope.JAVA_FILE_SCOPE + ), + enabledByDefault = false + ) + + private fun hasPermissionMethodAnnotation(method: UMethod): Boolean = method.annotations + .any { + it.hasQualifiedName(ANNOTATION_PERMISSION_METHOD) + } + + private fun isPermissionMethodReturnType(method: UMethod): Boolean = + listOf(PsiType.VOID, PsiType.INT, PsiType.BOOLEAN).contains(method.returnType) + + /** + * Identifies methods that... + * DO call other methods annotated with @PermissionMethod + * DO NOT do anything else + */ + private fun onlyCallsPermissionMethod(method: UMethod): Boolean { + val body = method.uastBody as? UBlockExpression ?: return false + if (body.expressions.isEmpty()) return false + for (expression in body.expressions) { + when (expression) { + is UQualifiedReferenceExpression -> { + if (!isPermissionMethodCall(expression.selector)) return false + } + is UReturnExpression -> { + if (!isPermissionMethodCall(expression.returnExpression)) return false + } + is UCallExpression -> { + if (!isPermissionMethodCall(expression)) return false + } + is UIfExpression -> { + if (expression.thenExpression !is UReturnExpression) return false + if (!isPermissionMethodCall(expression.condition)) return false + } + else -> return false + } + } + return true + } + + private fun isPermissionMethodCall(expression: UExpression?): Boolean { + return when (expression) { + is UQualifiedReferenceExpression -> + return isPermissionMethodCall(expression.selector) + is UCallExpression -> { + val calledMethod = expression.resolve()?.getUMethod() ?: return false + return hasPermissionMethodAnnotation(calledMethod) + } + else -> false + } + } + } +} diff --git a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt index 3ab09a8366be..dfebdccf1d63 100644 --- a/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt +++ b/tools/processors/immutability/src/android/processor/immutability/ImmutabilityProcessor.kt @@ -37,10 +37,11 @@ val IMMUTABLE_ANNOTATION_NAME = Immutable::class.qualifiedName class ImmutabilityProcessor : AbstractProcessor() { companion object { + /** - * Types that are already immutable. + * Types that are already immutable. Will also ignore subclasses. */ - private val IGNORED_TYPES = listOf( + private val IGNORED_SUPER_TYPES = listOf( "java.io.File", "java.lang.Boolean", "java.lang.Byte", @@ -56,6 +57,15 @@ class ImmutabilityProcessor : AbstractProcessor() { "android.os.Parcelable.Creator", ) + /** + * Types that are already immutable. Must be an exact match, does not include any super + * or sub classes. + */ + private val IGNORED_EXACT_TYPES = listOf( + "java.lang.Class", + "java.lang.Object", + ) + private val IGNORED_METHODS = listOf( "writeToParcel", ) @@ -64,7 +74,8 @@ class ImmutabilityProcessor : AbstractProcessor() { private lateinit var collectionType: TypeMirror private lateinit var mapType: TypeMirror - private lateinit var ignoredTypes: List<TypeMirror> + private lateinit var ignoredSuperTypes: List<TypeMirror> + private lateinit var ignoredExactTypes: List<TypeMirror> private val seenTypesByPolicy = mutableMapOf<Set<Immutable.Policy.Exception>, Set<Type>>() @@ -76,7 +87,8 @@ class ImmutabilityProcessor : AbstractProcessor() { super.init(processingEnv) collectionType = processingEnv.erasedType("java.util.Collection")!! mapType = processingEnv.erasedType("java.util.Map")!! - ignoredTypes = IGNORED_TYPES.mapNotNull { processingEnv.erasedType(it) } + ignoredSuperTypes = IGNORED_SUPER_TYPES.mapNotNull { processingEnv.erasedType(it) } + ignoredExactTypes = IGNORED_EXACT_TYPES.mapNotNull { processingEnv.erasedType(it) } } override fun process( @@ -109,7 +121,7 @@ class ImmutabilityProcessor : AbstractProcessor() { classType: Symbol.TypeSymbol, parentPolicyExceptions: Set<Immutable.Policy.Exception>, ): Boolean { - if (classType.getAnnotation(Immutable.Ignore::class.java) != null) return false + if (isIgnored(classType)) return false val policyAnnotation = classType.getAnnotation(Immutable.Policy::class.java) val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty() @@ -131,7 +143,7 @@ class ImmutabilityProcessor : AbstractProcessor() { .fold(false) { anyError, field -> if (field.isStatic) { if (!field.isPrivate) { - var finalityError = !field.modifiers.contains(Modifier.FINAL) + val finalityError = !field.modifiers.contains(Modifier.FINAL) if (finalityError) { printError(parentChain, field, MessageUtils.staticNonFinalFailure()) } @@ -177,8 +189,10 @@ class ImmutabilityProcessor : AbstractProcessor() { val newChain = parentChain + "$classType" val hasMethodError = filteredElements + .asSequence() .filter { it.getKind() == ElementKind.METHOD } .map { it as Symbol.MethodSymbol } + .filterNot { it.isStatic } .filterNot { IGNORED_METHODS.contains(it.name.toString()) } .fold(false) { anyError, method -> // Must call visitMethod first so it doesn't get short circuited by the || @@ -208,6 +222,14 @@ class ImmutabilityProcessor : AbstractProcessor() { } } + // Check all of the super classes, since methods in those classes are also accessible + (classType as? Symbol.ClassSymbol)?.run { + (interfaces + superclass).forEach { + val element = it.asElement() ?: return@forEach + visitClass(parentChain, seenTypesByPolicy, element, element, newPolicyExceptions) + } + } + if (isRegularClass && !anyError && allowFinalClassesFinalFields && !classType.modifiers.contains(Modifier.FINAL) ) { @@ -301,16 +323,14 @@ class ImmutabilityProcessor : AbstractProcessor() { parentPolicyExceptions: Set<Immutable.Policy.Exception>, nonInterfaceClassFailure: () -> String = { MessageUtils.nonInterfaceReturnFailure() }, ): Boolean { + if (isIgnored(symbol)) return false + if (isIgnored(type)) return false if (type.isPrimitive) return false if (type.isPrimitiveOrVoid) { printError(parentChain, symbol, MessageUtils.voidReturnFailure()) return true } - if (ignoredTypes.any { processingEnv.typeUtils.isAssignable(type, it) }) { - return false - } - val policyAnnotation = symbol.getAnnotation(Immutable.Policy::class.java) val newPolicyExceptions = parentPolicyExceptions + policyAnnotation?.exceptions.orEmpty() @@ -357,16 +377,38 @@ class ImmutabilityProcessor : AbstractProcessor() { message: String, ) = processingEnv.messager.printMessage( Diagnostic.Kind.ERROR, - // Drop one from the parent chain so that the directly enclosing class isn't logged. - // It exists in the list at this point in the traversal so that further children can - // include the right reference. - parentChain.dropLast(1).joinToString() + "\n\t" + message, + parentChain.plus(element.simpleName).joinToString() + "\n\t " + message, element, ) private fun ProcessingEnvironment.erasedType(typeName: String) = elementUtils.getTypeElement(typeName)?.asType()?.let(typeUtils::erasure) - private fun isIgnored(symbol: Symbol) = - symbol.getAnnotation(Immutable.Ignore::class.java) != null + private fun isIgnored(type: Type) = + (type.getAnnotation(Immutable.Ignore::class.java) != null) + || (ignoredSuperTypes.any { type.isAssignable(it) }) + || (ignoredExactTypes.any { type.isSameType(it) }) + + private fun isIgnored(symbol: Symbol) = when { + // Anything annotated as @Ignore is always ignored + symbol.getAnnotation(Immutable.Ignore::class.java) != null -> true + // Then ignore exact types, regardless of what kind they are + ignoredExactTypes.any { symbol.type.isSameType(it) } -> true + // Then only allow methods through, since other types (fields) are usually a failure + symbol.getKind() != ElementKind.METHOD -> false + // Finally, check for any ignored super types + else -> ignoredSuperTypes.any { symbol.type.isAssignable(it) } + } + + private fun TypeMirror.isAssignable(type: TypeMirror) = try { + processingEnv.typeUtils.isAssignable(this, type) + } catch (ignored: Exception) { + false + } + + private fun TypeMirror.isSameType(type: TypeMirror) = try { + processingEnv.typeUtils.isSameType(this, type) + } catch (ignored: Exception) { + false + } } diff --git a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt index f26357ff8e4b..2f7d59a7f0e5 100644 --- a/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt +++ b/tools/processors/immutability/test/android/processor/ImmutabilityProcessorTest.kt @@ -90,7 +90,7 @@ class ImmutabilityProcessorTest { @Test fun validInterface() = test( - JavaFileObjects.forSourceString( + source = JavaFileObjects.forSourceString( "$PACKAGE_PREFIX.$DATA_CLASS_NAME", /* language=JAVA */ """ package $PACKAGE_PREFIX; @@ -227,49 +227,114 @@ class ImmutabilityProcessorTest { nonInterfaceReturnFailure(line = 9), nonInterfaceReturnFailure(line = 10, index = 0), classNotFinalFailure(line = 13, "NonFinalClassFinalFields"), - ), otherErrors = listOf( - memberNotMethodFailure(line = 4) to FINAL_CLASSES[1], - memberNotMethodFailure(line = 4) to FINAL_CLASSES[3], + ), otherErrors = mapOf( + FINAL_CLASSES[1] to listOf( + memberNotMethodFailure(line = 4), + ), + FINAL_CLASSES[3] to listOf( + memberNotMethodFailure(line = 4), + ), ) ) + @Test + fun superClass() { + val superClass = JavaFileObjects.forSourceString( + "$PACKAGE_PREFIX.SuperClass", + /* language=JAVA */ """ + package $PACKAGE_PREFIX; + + import java.util.List; + + public interface SuperClass { + InnerClass getInnerClassOne(); + + final class InnerClass { + public String innerField; + } + } + """.trimIndent() + ) + + val dataClass = JavaFileObjects.forSourceString( + "$PACKAGE_PREFIX.$DATA_CLASS_NAME", + /* language=JAVA */ """ + package $PACKAGE_PREFIX; + + import java.util.List; + + @Immutable + public interface $DATA_CLASS_NAME extends SuperClass { + String[] getArray(); + } + """.trimIndent() + ) + + test( + sources = arrayOf(superClass, dataClass), + fileToErrors = mapOf( + superClass to listOf( + classNotImmutableFailure(line = 5, className = "SuperClass"), + nonInterfaceReturnFailure(line = 6), + nonInterfaceClassFailure(8), + classNotImmutableFailure(line = 8, className = "InnerClass"), + memberNotMethodFailure(line = 9), + ), + dataClass to listOf( + arrayFailure(line = 7), + ) + ) + ) + } + private fun test( source: JavaFileObject, errors: List<CompilationError>, - otherErrors: List<Pair<CompilationError, JavaFileObject>> = emptyList(), + otherErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(), + ) = test( + sources = arrayOf(source), + fileToErrors = otherErrors + (source to errors), + ) + + private fun test( + vararg sources: JavaFileObject, + fileToErrors: Map<JavaFileObject, List<CompilationError>> = emptyMap(), ) { val compilation = javac() .withProcessors(ImmutabilityProcessor()) - .compile(FINAL_CLASSES + ANNOTATION + listOf(source)) - val allErrors = otherErrors + errors.map { it to source } - allErrors.forEach { (error, file) -> - try { - assertThat(compilation) - .hadErrorContaining(error.message) - .inFile(file) - .onLine(error.line) - } catch (e: AssertionError) { - // Wrap the exception so that the line number is logged - val wrapped = AssertionError("Expected $error, ${e.message}").apply { - stackTrace = e.stackTrace - } + .compile(FINAL_CLASSES + ANNOTATION + sources) + + fileToErrors.forEach { (file, errors) -> + errors.forEach { error -> + try { + assertThat(compilation) + .hadErrorContaining(error.message) + .inFile(file) + .onLine(error.line) + } catch (e: AssertionError) { + // Wrap the exception so that the line number is logged + val wrapped = AssertionError("Expected $error, ${e.message}").apply { + stackTrace = e.stackTrace + } - // Wrap again with Expect so that all errors are reported. This is very bad code - // but can only be fixed by updating compile-testing with a better Truth Subject - // implementation. - expect.that(wrapped).isNull() + // Wrap again with Expect so that all errors are reported. This is very bad code + // but can only be fixed by updating compile-testing with a better Truth Subject + // implementation. + expect.that(wrapped).isNull() + } } } - try { - assertThat(compilation).hadErrorCount(allErrors.size) - } catch (e: AssertionError) { + expect.that(compilation.errors().size).isEqualTo(fileToErrors.values.sumOf { it.size }) + + if (expect.hasFailures()) { expect.withMessage( compilation.errors() + .sortedBy { it.lineNumber } .joinToString(separator = "\n") { "${it.lineNumber}: ${it.getMessage(Locale.ENGLISH)?.trim()}" } - ).that(e).isNull() + ).fail() } } @@ -307,4 +372,4 @@ class ImmutabilityProcessorTest { val line: Long, val message: String, ) -}
\ No newline at end of file +} |