diff options
Diffstat (limited to 'packages/SystemUI/src')
126 files changed, 4356 insertions, 1315 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java index 48161f1a38c5..31fc760d24d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukView.java @@ -435,8 +435,8 @@ public class KeyguardSimPukView extends KeyguardPinBasedInputView { if (DEBUG) Log.d(LOG_TAG, "verifyPasswordAndUnlock " + " UpdateSim.onSimCheckResponse: " + " attemptsRemaining=" + result.getAttemptsRemaining()); - mStateMachine.reset(); } + mStateMachine.reset(); mCheckSimPukThread = null; } }); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 3acbfb87c3f4..60541eb56afc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -286,11 +286,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final Executor mBackgroundExecutor; /** - * Short delay before restarting biometric authentication after a successful try - * This should be slightly longer than the time between on<biometric>Authenticated - * (e.g. onFingerprintAuthenticated) and setKeyguardGoingAway(true). + * Short delay before restarting fingerprint authentication after a successful try. This should + * be slightly longer than the time between onFingerprintAuthenticated and + * setKeyguardGoingAway(true). */ - private static final int BIOMETRIC_CONTINUE_DELAY_MS = 500; + private static final int FINGERPRINT_CONTINUE_DELAY_MS = 500; // If the HAL dies or is unable to authenticate, keyguard should retry after a short delay private int mHardwareFingerprintUnavailableRetryCount = 0; @@ -598,7 +598,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE), - BIOMETRIC_CONTINUE_DELAY_MS); + FINGERPRINT_CONTINUE_DELAY_MS); // Only authenticate fingerprint once when assistant is visible mAssistantVisible = false; @@ -780,9 +780,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_BIOMETRIC_AUTHENTICATION_CONTINUE), - BIOMETRIC_CONTINUE_DELAY_MS); - // Only authenticate face once when assistant is visible mAssistantVisible = false; @@ -1072,6 +1069,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); } + private boolean isEncryptedOrLockdown(int userId) { + final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(userId); + final boolean isLockDown = + containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW) + || containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + final boolean isEncrypted = containsFlag(strongAuth, STRONG_AUTH_REQUIRED_AFTER_BOOT); + return isEncrypted || isLockDown; + } + public boolean userNeedsStrongAuth() { return mStrongAuthTracker.getStrongAuthForUser(getCurrentUser()) != LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; @@ -1248,6 +1254,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } }; + // Trigger the fingerprint success path so the bouncer can be shown + private final FingerprintManager.FingerprintDetectionCallback mFingerprintDetectionCallback + = this::handleFingerprintAuthenticated; + private FingerprintManager.AuthenticationCallback mFingerprintAuthenticationCallback = new AuthenticationCallback() { @@ -2050,8 +2060,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintCancelSignal.cancel(); } mFingerprintCancelSignal = new CancellationSignal(); - mFpm.authenticate(null, mFingerprintCancelSignal, 0, mFingerprintAuthenticationCallback, - null, userId); + + if (isEncryptedOrLockdown(userId)) { + mFpm.detectFingerprint(mFingerprintCancelSignal, mFingerprintDetectionCallback, + userId); + } else { + mFpm.authenticate(null, mFingerprintCancelSignal, 0, + mFingerprintAuthenticationCallback, null, userId); + } + setFingerprintRunningState(BIOMETRIC_STATE_RUNNING); } } @@ -2087,7 +2104,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean isUnlockWithFingerprintPossible(int userId) { return mFpm != null && mFpm.isHardwareDetected() && !isFingerprintDisabled(userId) - && mFpm.getEnrolledFingerprints(userId).size() > 0; + && mFpm.hasEnrolledTemplates(userId); } private boolean isUnlockWithFacePossible(int userId) { diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index d1df27683a25..7c3671385868 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -54,6 +54,7 @@ import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.power.PowerUI; +import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.recents.OverviewProxyService; import com.android.systemui.recents.Recents; import com.android.systemui.screenrecord.RecordingController; @@ -315,6 +316,7 @@ public class Dependency { @Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager; @Inject Lazy<AutoHideController> mAutoHideController; @Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener; + @Inject Lazy<PrivacyItemController> mPrivacyItemController; @Inject @Background Lazy<Looper> mBgLooper; @Inject @Background Lazy<Handler> mBgHandler; @Inject @Main Lazy<Looper> mMainLooper; @@ -516,6 +518,7 @@ public class Dependency { mProviders.put(ForegroundServiceNotificationListener.class, mForegroundServiceNotificationListener::get); mProviders.put(ClockManager.class, mClockManager::get); + mProviders.put(PrivacyItemController.class, mPrivacyItemController::get); mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get); mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get); mProviders.put(PackageManagerWrapper.class, mPackageManagerWrapper::get); diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index 0218cd237037..d1149d37d431 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -74,6 +74,7 @@ public final class Prefs { Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, Key.HAS_SEEN_BUBBLES_EDUCATION, Key.HAS_SEEN_BUBBLES_MANAGE_EDUCATION, + Key.HAS_SEEN_REVERSE_BOTTOM_SHEET, Key.CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT }) public @interface Key { @@ -122,6 +123,7 @@ public final class Prefs { String HAS_SEEN_ODI_CAPTIONS_TOOLTIP = "HasSeenODICaptionsTooltip"; String HAS_SEEN_BUBBLES_EDUCATION = "HasSeenBubblesOnboarding"; String HAS_SEEN_BUBBLES_MANAGE_EDUCATION = "HasSeenBubblesManageOnboarding"; + String HAS_SEEN_REVERSE_BOTTOM_SHEET = "HasSeenReverseBottomSheet"; String CONTROLS_STRUCTURE_SWIPE_TOOLTIP_COUNT = "ControlsStructureSwipeTooltipCount"; /** Tracks whether the user has seen the onboarding screen for priority conversations */ String HAS_SEEN_PRIORITY_ONBOARDING = "HasUserSeenPriorityOnboarding"; diff --git a/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java index 449ed8c3bcdb..57e656827f1c 100644 --- a/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/SlicePermissionActivity.java @@ -16,6 +16,7 @@ package com.android.systemui; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; +import android.annotation.Nullable; import android.app.Activity; import android.app.AlertDialog; import android.app.slice.SliceManager; @@ -29,6 +30,7 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.net.Uri; import android.os.Bundle; import android.text.BidiFormatter; +import android.util.EventLog; import android.util.Log; import android.widget.CheckBox; import android.widget.TextView; @@ -50,10 +52,12 @@ public class SlicePermissionActivity extends Activity implements OnClickListener mUri = getIntent().getParcelableExtra(SliceProvider.EXTRA_BIND_URI); mCallingPkg = getIntent().getStringExtra(SliceProvider.EXTRA_PKG); - mProviderPkg = getIntent().getStringExtra(SliceProvider.EXTRA_PROVIDER_PKG); try { PackageManager pm = getPackageManager(); + mProviderPkg = pm.resolveContentProvider(mUri.getAuthority(), + PackageManager.GET_META_DATA).applicationInfo.packageName; + verifyCallingPkg(); CharSequence app1 = BidiFormatter.getInstance().unicodeWrap(pm.getApplicationInfo( mCallingPkg, 0).loadSafeLabel(pm, PackageItemInfo.DEFAULT_MAX_LABEL_SIZE_PX, PackageItemInfo.SAFE_LABEL_FLAG_TRIM @@ -97,4 +101,27 @@ public class SlicePermissionActivity extends Activity implements OnClickListener public void onDismiss(DialogInterface dialog) { finish(); } + + private void verifyCallingPkg() { + final String providerPkg = getIntent().getStringExtra(SliceProvider.EXTRA_PROVIDER_PKG); + if (providerPkg == null || mProviderPkg.equals(providerPkg)) return; + final String callingPkg = getCallingPkg(); + EventLog.writeEvent(0x534e4554, "159145361", getUid(callingPkg)); + } + + @Nullable + private String getCallingPkg() { + final Uri referrer = getReferrer(); + if (referrer == null) return null; + return referrer.getHost(); + } + + private int getUid(@Nullable final String pkg) { + if (pkg == null) return -1; + try { + return getPackageManager().getApplicationInfo(pkg, 0).uid; + } catch (NameNotFoundException e) { + } + return -1; + } } diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java index 7e5b42653210..93a8df41c673 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java @@ -25,7 +25,9 @@ public class AppOpItem { private int mUid; private String mPackageName; private long mTimeStarted; - private String mState; + private StringBuilder mState; + // This is only used for items with mCode == AppOpsManager.OP_RECORD_AUDIO + private boolean mSilenced; public AppOpItem(int code, int uid, String packageName, long timeStarted) { this.mCode = code; @@ -36,9 +38,8 @@ public class AppOpItem { .append("AppOpItem(") .append("Op code=").append(code).append(", ") .append("UID=").append(uid).append(", ") - .append("Package name=").append(packageName) - .append(")") - .toString(); + .append("Package name=").append(packageName).append(", ") + .append("Paused="); } public int getCode() { @@ -57,8 +58,16 @@ public class AppOpItem { return mTimeStarted; } + public void setSilenced(boolean silenced) { + mSilenced = silenced; + } + + public boolean isSilenced() { + return mSilenced; + } + @Override public String toString() { - return mState; + return mState.append(mSilenced).append(")").toString(); } } diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index 941de2dc63ec..2b9514f6d23f 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -16,18 +16,31 @@ package com.android.systemui.appops; +import static android.media.AudioManager.ACTION_MICROPHONE_MUTE_CHANGED; + import android.app.AppOpsManager; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.location.LocationManager; +import android.media.AudioManager; +import android.media.AudioRecordingConfiguration; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; +import android.util.SparseArray; + +import androidx.annotation.WorkerThread; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dumpable; +import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; @@ -47,7 +60,7 @@ import javax.inject.Singleton; * NotificationPresenter to be displayed to the user. */ @Singleton -public class AppOpsControllerImpl implements AppOpsController, +public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsController, AppOpsManager.OnOpActiveChangedInternalListener, AppOpsManager.OnOpNotedListener, Dumpable { @@ -57,23 +70,38 @@ public class AppOpsControllerImpl implements AppOpsController, private static final long NOTED_OP_TIME_DELAY_MS = 5000; private static final String TAG = "AppOpsControllerImpl"; private static final boolean DEBUG = false; - private final Context mContext; + private final BroadcastDispatcher mDispatcher; private final AppOpsManager mAppOps; + private final AudioManager mAudioManager; + private final LocationManager mLocationManager; + + // mLocationProviderPackages are cached and updated only occasionally + private static final long LOCATION_PROVIDER_UPDATE_FREQUENCY_MS = 30000; + private long mLastLocationProviderPackageUpdate; + private List<String> mLocationProviderPackages; + private H mBGHandler; private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>(); private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>(); + private final PermissionFlagsCache mFlagsCache; private boolean mListening; + private boolean mMicMuted; @GuardedBy("mActiveItems") private final List<AppOpItem> mActiveItems = new ArrayList<>(); @GuardedBy("mNotedItems") private final List<AppOpItem> mNotedItems = new ArrayList<>(); + @GuardedBy("mActiveItems") + private final SparseArray<ArrayList<AudioRecordingConfiguration>> mRecordingsByUid = + new SparseArray<>(); protected static final int[] OPS = new int[] { AppOpsManager.OP_CAMERA, + AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_SYSTEM_ALERT_WINDOW, AppOpsManager.OP_RECORD_AUDIO, + AppOpsManager.OP_PHONE_CALL_MICROPHONE, AppOpsManager.OP_COARSE_LOCATION, AppOpsManager.OP_FINE_LOCATION }; @@ -82,14 +110,22 @@ public class AppOpsControllerImpl implements AppOpsController, public AppOpsControllerImpl( Context context, @Background Looper bgLooper, - DumpManager dumpManager) { - mContext = context; + DumpManager dumpManager, + PermissionFlagsCache cache, + AudioManager audioManager, + BroadcastDispatcher dispatcher + ) { + mDispatcher = dispatcher; mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); + mFlagsCache = cache; mBGHandler = new H(bgLooper); final int numOps = OPS.length; for (int i = 0; i < numOps; i++) { mCallbacksByCode.put(OPS[i], new ArraySet<>()); } + mAudioManager = audioManager; + mMicMuted = audioManager.isMicrophoneMute(); + mLocationManager = context.getSystemService(LocationManager.class); dumpManager.registerDumpable(TAG, this); } @@ -104,12 +140,22 @@ public class AppOpsControllerImpl implements AppOpsController, if (listening) { mAppOps.startWatchingActive(OPS, this); mAppOps.startWatchingNoted(OPS, this); + mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler); + mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged( + mAudioManager.getActiveRecordingConfigurations())); + mDispatcher.registerReceiverWithHandler(this, + new IntentFilter(ACTION_MICROPHONE_MUTE_CHANGED), mBGHandler); + } else { mAppOps.stopWatchingActive(this); mAppOps.stopWatchingNoted(this); + mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback); + mBGHandler.removeCallbacksAndMessages(null); // null removes all + mDispatcher.unregisterReceiver(this); synchronized (mActiveItems) { mActiveItems.clear(); + mRecordingsByUid.clear(); } synchronized (mNotedItems) { mNotedItems.clear(); @@ -182,9 +228,12 @@ public class AppOpsControllerImpl implements AppOpsController, AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName); if (item == null && active) { item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); + if (code == AppOpsManager.OP_RECORD_AUDIO) { + item.setSilenced(isAnyRecordingPausedLocked(uid)); + } mActiveItems.add(item); if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); - return true; + return !item.isSilenced(); } else if (item != null && !active) { mActiveItems.remove(item); if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); @@ -208,7 +257,7 @@ public class AppOpsControllerImpl implements AppOpsController, active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; } if (!active) { - notifySuscribers(code, uid, packageName, false); + notifySuscribersWorker(code, uid, packageName, false); } } @@ -231,10 +280,94 @@ public class AppOpsControllerImpl implements AppOpsController, } /** + * Does the app-op code refer to a user sensitive permission for the specified user id + * and package. Only user sensitive permission should be shown to the user by default. + * + * @param appOpCode The code of the app-op. + * @param uid The uid of the user. + * @param packageName The name of the package. + * + * @return {@code true} iff the app-op item is user sensitive + */ + private boolean isUserSensitive(int appOpCode, int uid, String packageName) { + String permission = AppOpsManager.opToPermission(appOpCode); + if (permission == null) { + return false; + } + int permFlags = mFlagsCache.getPermissionFlags(permission, + packageName, uid); + return (permFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) != 0; + } + + /** + * Does the app-op item refer to an operation that should be shown to the user. + * Only specficic ops (like SYSTEM_ALERT_WINDOW) or ops that refer to user sensitive + * permission should be shown to the user by default. + * + * @param item The item + * + * @return {@code true} iff the app-op item should be shown to the user + */ + private boolean isUserVisible(AppOpItem item) { + return isUserVisible(item.getCode(), item.getUid(), item.getPackageName()); + } + + /** + * Checks if a package is the current location provider. + * + * <p>Data is cached to avoid too many calls into system server + * + * @param packageName The package that might be the location provider + * + * @return {@code true} iff the package is the location provider. + */ + private boolean isLocationProvider(String packageName) { + long now = System.currentTimeMillis(); + + if (mLastLocationProviderPackageUpdate + LOCATION_PROVIDER_UPDATE_FREQUENCY_MS < now) { + mLastLocationProviderPackageUpdate = now; + mLocationProviderPackages = mLocationManager.getProviderPackages( + LocationManager.FUSED_PROVIDER); + } + + return mLocationProviderPackages.contains(packageName); + } + + /** + * Does the app-op, uid and package name, refer to an operation that should be shown to the + * user. Only specficic ops (like {@link AppOpsManager.OP_SYSTEM_ALERT_WINDOW}) or + * ops that refer to user sensitive permission should be shown to the user by default. + * + * @param item The item + * + * @return {@code true} iff the app-op for should be shown to the user + */ + private boolean isUserVisible(int appOpCode, int uid, String packageName) { + // currently OP_SYSTEM_ALERT_WINDOW and OP_MONITOR_HIGH_POWER_LOCATION + // does not correspond to a platform permission + // which may be user sensitive, so for now always show it to the user. + if (appOpCode == AppOpsManager.OP_SYSTEM_ALERT_WINDOW + || appOpCode == AppOpsManager.OP_MONITOR_HIGH_POWER_LOCATION + || appOpCode == AppOpsManager.OP_PHONE_CALL_CAMERA + || appOpCode == AppOpsManager.OP_PHONE_CALL_MICROPHONE) { + return true; + } + + if (appOpCode == AppOpsManager.OP_CAMERA && isLocationProvider(packageName)) { + return true; + } + + return isUserSensitive(appOpCode, uid, packageName); + } + + /** * Returns a copy of the list containing all the active AppOps that the controller tracks. * + * Call from a worker thread as it may perform long operations. + * * @return List of active AppOps information */ + @WorkerThread public List<AppOpItem> getActiveAppOps() { return getActiveAppOpsForUser(UserHandle.USER_ALL); } @@ -243,10 +376,13 @@ public class AppOpsControllerImpl implements AppOpsController, * Returns a copy of the list containing all the active AppOps that the controller tracks, for * a given user id. * + * Call from a worker thread as it may perform long operations. + * * @param userId User id to track, can be {@link UserHandle#USER_ALL} * * @return List of active AppOps information for that user id */ + @WorkerThread public List<AppOpItem> getActiveAppOpsForUser(int userId) { List<AppOpItem> list = new ArrayList<>(); synchronized (mActiveItems) { @@ -254,7 +390,8 @@ public class AppOpsControllerImpl implements AppOpsController, for (int i = 0; i < numActiveItems; i++) { AppOpItem item = mActiveItems.get(i); if ((userId == UserHandle.USER_ALL - || UserHandle.getUserId(item.getUid()) == userId)) { + || UserHandle.getUserId(item.getUid()) == userId) + && isUserVisible(item) && !item.isSilenced()) { list.add(item); } } @@ -264,7 +401,8 @@ public class AppOpsControllerImpl implements AppOpsController, for (int i = 0; i < numNotedItems; i++) { AppOpItem item = mNotedItems.get(i); if ((userId == UserHandle.USER_ALL - || UserHandle.getUserId(item.getUid()) == userId)) { + || UserHandle.getUserId(item.getUid()) == userId) + && isUserVisible(item)) { list.add(item); } } @@ -272,6 +410,10 @@ public class AppOpsControllerImpl implements AppOpsController, return list; } + private void notifySuscribers(int code, int uid, String packageName, boolean active) { + mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active)); + } + @Override public void onOpActiveChanged(int code, int uid, String packageName, boolean active) { if (DEBUG) { @@ -289,7 +431,7 @@ public class AppOpsControllerImpl implements AppOpsController, // If active is false, we only send the update if the op is not actively noted (prevent // early removal) if (!alsoNoted) { - mBGHandler.post(() -> notifySuscribers(code, uid, packageName, active)); + notifySuscribers(code, uid, packageName, active); } } @@ -307,12 +449,12 @@ public class AppOpsControllerImpl implements AppOpsController, alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; } if (!alsoActive) { - mBGHandler.post(() -> notifySuscribers(code, uid, packageName, true)); + notifySuscribers(code, uid, packageName, true); } } - private void notifySuscribers(int code, int uid, String packageName, boolean active) { - if (mCallbacksByCode.containsKey(code)) { + private void notifySuscribersWorker(int code, int uid, String packageName, boolean active) { + if (mCallbacksByCode.containsKey(code) && isUserVisible(code, uid, packageName)) { if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName); for (Callback cb: mCallbacksByCode.get(code)) { cb.onActiveStateChanged(code, uid, packageName, active); @@ -337,6 +479,70 @@ public class AppOpsControllerImpl implements AppOpsController, } + private boolean isAnyRecordingPausedLocked(int uid) { + if (mMicMuted) { + return true; + } + List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid); + if (configs == null) return false; + int configsNum = configs.size(); + for (int i = 0; i < configsNum; i++) { + AudioRecordingConfiguration config = configs.get(i); + if (config.isClientSilenced()) return true; + } + return false; + } + + private void updateRecordingPausedStatus() { + synchronized (mActiveItems) { + int size = mActiveItems.size(); + for (int i = 0; i < size; i++) { + AppOpItem item = mActiveItems.get(i); + if (item.getCode() == AppOpsManager.OP_RECORD_AUDIO) { + boolean paused = isAnyRecordingPausedLocked(item.getUid()); + if (item.isSilenced() != paused) { + item.setSilenced(paused); + notifySuscribers( + item.getCode(), + item.getUid(), + item.getPackageName(), + !item.isSilenced() + ); + } + } + } + } + } + + private AudioManager.AudioRecordingCallback mAudioRecordingCallback = + new AudioManager.AudioRecordingCallback() { + @Override + public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) { + synchronized (mActiveItems) { + mRecordingsByUid.clear(); + final int recordingsCount = configs.size(); + for (int i = 0; i < recordingsCount; i++) { + AudioRecordingConfiguration recording = configs.get(i); + + ArrayList<AudioRecordingConfiguration> recordings = mRecordingsByUid.get( + recording.getClientUid()); + if (recordings == null) { + recordings = new ArrayList<>(); + mRecordingsByUid.put(recording.getClientUid(), recordings); + } + recordings.add(recording); + } + } + updateRecordingPausedStatus(); + } + }; + + @Override + public void onReceive(Context context, Intent intent) { + mMicMuted = mAudioManager.isMicrophoneMute(); + updateRecordingPausedStatus(); + } + protected class H extends Handler { H(Looper looper) { super(looper); diff --git a/packages/SystemUI/src/com/android/systemui/appops/PermissionFlagsCache.kt b/packages/SystemUI/src/com/android/systemui/appops/PermissionFlagsCache.kt new file mode 100644 index 000000000000..9248b4f88a36 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/appops/PermissionFlagsCache.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.appops + +import android.content.pm.PackageManager +import android.os.UserHandle +import androidx.annotation.WorkerThread +import com.android.systemui.dagger.qualifiers.Background +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +private data class PermissionFlagKey( + val permission: String, + val packageName: String, + val uid: Int +) + +/** + * Cache for PackageManager's PermissionFlags. + * + * After a specific `{permission, package, uid}` has been requested, updates to it will be tracked, + * and changes to the uid will trigger new requests (in the background). + */ +@Singleton +class PermissionFlagsCache @Inject constructor( + private val packageManager: PackageManager, + @Background private val executor: Executor +) : PackageManager.OnPermissionsChangedListener { + + private val permissionFlagsCache = + mutableMapOf<Int, MutableMap<PermissionFlagKey, Int>>() + private var listening = false + + override fun onPermissionsChanged(uid: Int) { + executor.execute { + // Only track those that we've seen before + val keys = permissionFlagsCache.get(uid) + if (keys != null) { + keys.mapValuesTo(keys) { + getFlags(it.key) + } + } + } + } + + /** + * Retrieve permission flags from cache or PackageManager. There parameters will be passed + * directly to [PackageManager]. + * + * Calls to this method should be done from a background thread. + */ + @WorkerThread + fun getPermissionFlags(permission: String, packageName: String, uid: Int): Int { + if (!listening) { + listening = true + packageManager.addOnPermissionsChangeListener(this) + } + val key = PermissionFlagKey(permission, packageName, uid) + return permissionFlagsCache.getOrPut(uid, { mutableMapOf() }).get(key) ?: run { + getFlags(key).also { + permissionFlagsCache.get(uid)?.put(key, it) + } + } + } + + private fun getFlags(key: PermissionFlagKey): Int { + return packageManager.getPermissionFlags(key.permission, key.packageName, + UserHandle.getUserHandleForUid(key.uid)) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java index 8e49d584788a..008571099a64 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistHandleReminderExpBehavior.java @@ -26,6 +26,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ResolveInfo; +import android.database.ContentObserver; +import android.net.Uri; import android.os.Handler; import android.provider.Settings; @@ -66,8 +68,10 @@ import dagger.Lazy; @Singleton final class AssistHandleReminderExpBehavior implements BehaviorController { - private static final String LEARNING_TIME_ELAPSED_KEY = "reminder_exp_learning_time_elapsed"; - private static final String LEARNING_EVENT_COUNT_KEY = "reminder_exp_learning_event_count"; + private static final Uri LEARNING_TIME_ELAPSED_URI = + Settings.Secure.getUriFor(Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS); + private static final Uri LEARNING_EVENT_COUNT_URI = + Settings.Secure.getUriFor(Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT); private static final String LEARNED_HINT_LAST_SHOWN_KEY = "reminder_exp_learned_hint_last_shown"; private static final long DEFAULT_LEARNING_TIME_MS = TimeUnit.DAYS.toMillis(10); @@ -181,6 +185,7 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { private boolean mIsNavBarHidden; private boolean mIsLauncherShowing; private int mConsecutiveTaskSwitches; + @Nullable private ContentObserver mSettingObserver; /** Whether user has learned the gesture. */ private boolean mIsLearned; @@ -248,9 +253,22 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { mWakefulnessLifecycle.get().addObserver(mWakefulnessLifecycleObserver); mLearningTimeElapsed = Settings.Secure.getLong( - context.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, /* default = */ 0); + context.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + /* default = */ 0); mLearningCount = Settings.Secure.getInt( - context.getContentResolver(), LEARNING_EVENT_COUNT_KEY, /* default = */ 0); + context.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + /* default = */ 0); + mSettingObserver = new SettingsObserver(context, mHandler); + context.getContentResolver().registerContentObserver( + LEARNING_TIME_ELAPSED_URI, + /* notifyForDescendants = */ true, + mSettingObserver); + context.getContentResolver().registerContentObserver( + LEARNING_EVENT_COUNT_URI, + /* notifyForDescendants = */ true, + mSettingObserver); mLearnedHintLastShownEpochDay = Settings.Secure.getLong( context.getContentResolver(), LEARNED_HINT_LAST_SHOWN_KEY, /* default = */ 0); mLastLearningTimestamp = mClock.currentTimeMillis(); @@ -264,8 +282,20 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { if (mContext != null) { mBroadcastDispatcher.get().unregisterReceiver(mDefaultHomeBroadcastReceiver); mBootCompleteCache.get().removeListener(mBootCompleteListener); - Settings.Secure.putLong(mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, 0); - Settings.Secure.putInt(mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, 0); + mContext.getContentResolver().unregisterContentObserver(mSettingObserver); + mSettingObserver = null; + // putString to use overrideableByRestore + Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + Long.toString(0L), + /* overrideableByRestore = */ true); + // putString to use overrideableByRestore + Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + Integer.toString(0), + /* overrideableByRestore = */ true); Settings.Secure.putLong(mContext.getContentResolver(), LEARNED_HINT_LAST_SHOWN_KEY, 0); mContext = null; } @@ -282,8 +312,12 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { return; } - Settings.Secure.putLong( - mContext.getContentResolver(), LEARNING_EVENT_COUNT_KEY, ++mLearningCount); + // putString to use overrideableByRestore + Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + Integer.toString(++mLearningCount), + /* overrideableByRestore = */ true); } @Override @@ -460,8 +494,12 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { mIsLearned = mLearningCount >= getLearningCount() || mLearningTimeElapsed >= getLearningTimeMs(); - mHandler.post(() -> Settings.Secure.putLong( - mContext.getContentResolver(), LEARNING_TIME_ELAPSED_KEY, mLearningTimeElapsed)); + // putString to use overrideableByRestore + mHandler.post(() -> Settings.Secure.putString( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + Long.toString(mLearningTimeElapsed), + /* overrideableByRestore = */ true)); } private void resetConsecutiveTaskSwitches() { @@ -589,4 +627,32 @@ final class AssistHandleReminderExpBehavior implements BehaviorController { + "=" + getShowWhenTaught()); } + + private final class SettingsObserver extends ContentObserver { + + private final Context mContext; + + SettingsObserver(Context context, Handler handler) { + super(handler); + mContext = context; + } + + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + if (LEARNING_TIME_ELAPSED_URI.equals(uri)) { + mLastLearningTimestamp = mClock.currentTimeMillis(); + mLearningTimeElapsed = Settings.Secure.getLong( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_TIME_ELAPSED_MILLIS, + /* default = */ 0); + } else if (LEARNING_EVENT_COUNT_URI.equals(uri)) { + mLearningCount = Settings.Secure.getInt( + mContext.getContentResolver(), + Settings.Secure.ASSIST_HANDLES_LEARNING_EVENT_COUNT, + /* default = */ 0); + } + + super.onChange(selfChange, uri); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/assist/AssistantSessionEvent.kt b/packages/SystemUI/src/com/android/systemui/assist/AssistantSessionEvent.kt index 8b953fa46441..be089b12a95d 100644 --- a/packages/SystemUI/src/com/android/systemui/assist/AssistantSessionEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/assist/AssistantSessionEvent.kt @@ -21,7 +21,7 @@ import com.android.internal.logging.UiEventLogger enum class AssistantSessionEvent(private val id: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "Unknown assistant session event") - ASSISTANT_SESSION_UNKNOWN(523), + ASSISTANT_SESSION_UNKNOWN(0), @UiEvent(doc = "Assistant session dismissed due to timeout") ASSISTANT_SESSION_TIMEOUT_DISMISS(524), diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java index c6d128631930..b71e3adae8ac 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java @@ -286,6 +286,15 @@ class Bubble implements BubbleViewProvider { } /** + * Sets whether this bubble is considered visually interruptive. Normally pulled from the + * {@link NotificationEntry}, this method is purely for testing. + */ + @VisibleForTesting + void setVisuallyInterruptiveForTest(boolean visuallyInterruptive) { + mIsVisuallyInterruptive = visuallyInterruptive; + } + + /** * Starts a task to inflate & load any necessary information to display a bubble. * * @param callback the callback to notify one the bubble is ready to be displayed. @@ -411,6 +420,7 @@ class Bubble implements BubbleViewProvider { } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { // Was an intent bubble now it's a shortcut bubble... still unregister the listener mIntent.unregisterCancelListener(mIntentCancelListener); + mIntentActive = false; mIntent = null; } mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index e252195da136..5deae925ba30 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -133,7 +133,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @IntDef({DISMISS_USER_GESTURE, DISMISS_AGED, DISMISS_TASK_FINISHED, DISMISS_BLOCKED, DISMISS_NOTIF_CANCEL, DISMISS_ACCESSIBILITY_ACTION, DISMISS_NO_LONGER_BUBBLE, DISMISS_USER_CHANGED, DISMISS_GROUP_CANCELLED, DISMISS_INVALID_INTENT, - DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED}) + DISMISS_OVERFLOW_MAX_REACHED, DISMISS_SHORTCUT_REMOVED, DISMISS_PACKAGE_REMOVED, + DISMISS_NO_BUBBLE_UP}) @Target({FIELD, LOCAL_VARIABLE, PARAMETER}) @interface DismissReason {} @@ -150,6 +151,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi static final int DISMISS_OVERFLOW_MAX_REACHED = 11; static final int DISMISS_SHORTCUT_REMOVED = 12; static final int DISMISS_PACKAGE_REMOVED = 13; + static final int DISMISS_NO_BUBBLE_UP = 14; private final Context mContext; private final NotificationEntryManager mNotificationEntryManager; @@ -168,6 +170,12 @@ public class BubbleController implements ConfigurationController.ConfigurationLi @Nullable private BubbleStackView mStackView; private BubbleIconFactory mBubbleIconFactory; + /** + * The relative position of the stack when we removed it and nulled it out. If the stack is + * re-created, it will re-appear at this position. + */ + @Nullable private BubbleStackView.RelativeStackPosition mPositionFromRemovedStack; + // Tracks the id of the current (foreground) user. private int mCurrentUserId; // Saves notification keys of active bubbles when users are switched. @@ -398,7 +406,8 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (bubble.getBubbleIntent() == null) { return; } - if (bubble.isIntentActive()) { + if (bubble.isIntentActive() + || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { bubble.setPendingIntentCanceled(); return; } @@ -718,6 +727,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mContext, mBubbleData, mSurfaceSynchronizer, mFloatingContentCoordinator, mSysUiState, this::onAllBubblesAnimatedOut, this::onImeVisibilityChanged, this::hideCurrentInputMethod); + mStackView.setStackStartPosition(mPositionFromRemovedStack); mStackView.addView(mBubbleScrim); if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); @@ -787,6 +797,7 @@ public class BubbleController implements ConfigurationController.ConfigurationLi try { mAddedToWindowManager = false; if (mStackView != null) { + mPositionFromRemovedStack = mStackView.getRelativeStackPosition(); mWindowManager.removeView(mStackView); mStackView.removeView(mBubbleScrim); mStackView = null; @@ -1109,8 +1120,17 @@ public class BubbleController implements ConfigurationController.ConfigurationLi if (notif.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { notif.setInterruption(); } - Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); - inflateAndAdd(bubble, suppressFlyout, showInShade); + if (!notif.getRanking().visuallyInterruptive() + && (notif.getBubbleMetadata() != null + && !notif.getBubbleMetadata().getAutoExpandBubble()) + && mBubbleData.hasOverflowBubbleWithKey(notif.getKey())) { + // Update the bubble but don't promote it out of overflow + Bubble b = mBubbleData.getOverflowBubbleWithKey(notif.getKey()); + b.setEntry(notif); + } else { + Bubble bubble = mBubbleData.getOrCreateBubble(notif, null /* persistedBubble */); + inflateAndAdd(bubble, suppressFlyout, showInShade); + } } void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { @@ -1234,8 +1254,18 @@ public class BubbleController implements ConfigurationController.ConfigurationLi rankingMap.getRanking(key, mTmpRanking); boolean isActiveBubble = mBubbleData.hasAnyBubbleWithKey(key); if (isActiveBubble && !mTmpRanking.canBubble()) { - mBubbleData.dismissBubbleWithKey(entry.getKey(), - BubbleController.DISMISS_BLOCKED); + // If this entry is no longer allowed to bubble, dismiss with the BLOCKED reason. + // This means that the app or channel's ability to bubble has been revoked. + mBubbleData.dismissBubbleWithKey( + key, BubbleController.DISMISS_BLOCKED); + } else if (isActiveBubble + && !mNotificationInterruptStateProvider.shouldBubbleUp(entry)) { + // If this entry is allowed to bubble, but cannot currently bubble up, dismiss it. + // This happens when DND is enabled and configured to hide bubbles. Dismissing with + // the reason DISMISS_NO_BUBBLE_UP will retain the underlying notification, so that + // the bubble will be re-created if shouldBubbleUp returns true. + mBubbleData.dismissBubbleWithKey( + key, BubbleController.DISMISS_NO_BUBBLE_UP); } else if (entry != null && mTmpRanking.isBubble() && !isActiveBubble) { entry.setFlagBubble(true); onEntryUpdated(entry); @@ -1312,8 +1342,10 @@ public class BubbleController implements ConfigurationController.ConfigurationLi mStackView.removeBubble(bubble); } - // If the bubble is removed for user switching, leave the notification in place. - if (reason == DISMISS_USER_CHANGED) { + // Leave the notification in place if we're dismissing due to user switching, or + // because DND is suppressing the bubble. In both of those cases, we need to be able + // to restore the bubble from the notification later. + if (reason == DISMISS_USER_CHANGED || reason == DISMISS_NO_BUBBLE_UP) { continue; } if (reason == DISMISS_NOTIF_CANCEL) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java index d2dc506c8e5c..85ea8bc91484 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java @@ -277,7 +277,8 @@ public class BubbleData { } else { // Updates an existing bubble bubble.setSuppressFlyout(suppressFlyout); - doUpdate(bubble); + // If there is no flyout, we probably shouldn't show the bubble at the top + doUpdate(bubble, !suppressFlyout /* reorder */); } if (bubble.shouldAutoExpand()) { @@ -431,12 +432,12 @@ public class BubbleData { } } - private void doUpdate(Bubble bubble) { + private void doUpdate(Bubble bubble, boolean reorder) { if (DEBUG_BUBBLE_DATA) { Log.d(TAG, "doUpdate: " + bubble); } mStateChange.updatedBubble = bubble; - if (!isExpanded()) { + if (!isExpanded() && reorder) { int prevPos = mBubbles.indexOf(bubble); mBubbles.remove(bubble); mBubbles.add(0, bubble); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index 3d3171208b15..58d5776543a9 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -301,12 +301,11 @@ public class BubbleExpandedView extends LinearLayout { mActivityView = new ActivityView(mContext, null /* attrs */, 0 /* defStyle */, true /* singleTaskInstance */, false /* usePublicVirtualDisplay*/, - true /* disableSurfaceViewBackgroundLayer */); + true /* disableSurfaceViewBackgroundLayer */, true /* useTrustedDisplay */); // Set ActivityView's alpha value as zero, since there is no view content to be shown. setContentVisibility(false); - mActivityViewContainer.setBackgroundColor(Color.WHITE); mActivityViewContainer.setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { @@ -434,9 +433,11 @@ public class BubbleExpandedView extends LinearLayout { } void applyThemeAttrs() { - final TypedArray ta = mContext.obtainStyledAttributes( - new int[] {android.R.attr.dialogCornerRadius}); + final TypedArray ta = mContext.obtainStyledAttributes(new int[] { + android.R.attr.dialogCornerRadius, + android.R.attr.colorBackgroundFloating}); mCornerRadius = ta.getDimensionPixelSize(0, 0); + mActivityViewContainer.setBackgroundColor(ta.getColor(1, Color.WHITE)); ta.recycle(); if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows( diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java index 40a93e1cdc47..d017bc0e31c2 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java @@ -24,7 +24,8 @@ import android.content.pm.ShortcutInfo; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.Rect; +import android.graphics.Path; +import android.graphics.drawable.AdaptiveIconDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; @@ -41,15 +42,15 @@ import com.android.systemui.R; */ public class BubbleIconFactory extends BaseIconFactory { + private int mBadgeSize; + protected BubbleIconFactory(Context context) { super(context, context.getResources().getConfiguration().densityDpi, context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size)); - } - - int getBadgeSize() { - return mContext.getResources().getDimensionPixelSize( + mBadgeSize = mContext.getResources().getDimensionPixelSize( com.android.launcher3.icons.R.dimen.profile_badge_size); } + /** * Returns the drawable that the developer has provided to display in the bubble. */ @@ -79,25 +80,34 @@ public class BubbleIconFactory extends BaseIconFactory { * will include the workprofile indicator on the badge if appropriate. */ BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) { - Bitmap userBadgedBitmap = createIconBitmap( - userBadgedAppIcon, 1f, getBadgeSize()); - ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize()); - if (!isImportantConversation) { - Canvas c = new Canvas(); - c.setBitmap(userBadgedBitmap); - shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); - return createIconBitmap(userBadgedBitmap); - } else { - float ringStrokeWidth = mContext.getResources().getDimensionPixelSize( + ShadowGenerator shadowGenerator = new ShadowGenerator(mBadgeSize); + Bitmap userBadgedBitmap = createIconBitmap(userBadgedAppIcon, 1f, mBadgeSize); + + if (userBadgedAppIcon instanceof AdaptiveIconDrawable) { + userBadgedBitmap = Bitmap.createScaledBitmap( + getCircleBitmap((AdaptiveIconDrawable) userBadgedAppIcon, /* size */ + userBadgedAppIcon.getIntrinsicWidth()), + mBadgeSize, mBadgeSize, /* filter */ true); + } + + if (isImportantConversation) { + final float ringStrokeWidth = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.importance_ring_stroke_width); - int importantConversationColor = mContext.getResources().getColor( + final int importantConversationColor = mContext.getResources().getColor( com.android.settingslib.R.color.important_conversation, null); Bitmap badgeAndRing = Bitmap.createBitmap(userBadgedBitmap.getWidth(), userBadgedBitmap.getHeight(), userBadgedBitmap.getConfig()); Canvas c = new Canvas(badgeAndRing); - Rect dest = new Rect((int) ringStrokeWidth, (int) ringStrokeWidth, - c.getHeight() - (int) ringStrokeWidth, c.getWidth() - (int) ringStrokeWidth); - c.drawBitmap(userBadgedBitmap, null, dest, null); + + final int bitmapTop = (int) ringStrokeWidth; + final int bitmapLeft = (int) ringStrokeWidth; + final int bitmapWidth = c.getWidth() - 2 * (int) ringStrokeWidth; + final int bitmapHeight = c.getHeight() - 2 * (int) ringStrokeWidth; + + Bitmap scaledBitmap = Bitmap.createScaledBitmap(userBadgedBitmap, bitmapWidth, + bitmapHeight, /* filter */ true); + c.drawBitmap(scaledBitmap, bitmapTop, bitmapLeft, /* paint */null); + Paint ringPaint = new Paint(); ringPaint.setStyle(Paint.Style.STROKE); ringPaint.setColor(importantConversationColor); @@ -105,11 +115,48 @@ public class BubbleIconFactory extends BaseIconFactory { ringPaint.setStrokeWidth(ringStrokeWidth); c.drawCircle(c.getWidth() / 2, c.getHeight() / 2, c.getWidth() / 2 - ringStrokeWidth, ringPaint); + shadowGenerator.recreateIcon(Bitmap.createBitmap(badgeAndRing), c); return createIconBitmap(badgeAndRing); + } else { + Canvas c = new Canvas(); + c.setBitmap(userBadgedBitmap); + shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c); + return createIconBitmap(userBadgedBitmap); } } + public Bitmap getCircleBitmap(AdaptiveIconDrawable icon, int size) { + Drawable foreground = icon.getForeground(); + Drawable background = icon.getBackground(); + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(); + canvas.setBitmap(bitmap); + + // Clip canvas to circle. + Path circlePath = new Path(); + circlePath.addCircle(/* x */ size / 2f, + /* y */ size / 2f, + /* radius */ size / 2f, + Path.Direction.CW); + canvas.clipPath(circlePath); + + // Draw background. + background.setBounds(0, 0, size, size); + background.draw(canvas); + + // Draw foreground. The foreground and background drawables are derived from adaptive icons + // Some icon shapes fill more space than others, so adaptive icons are normalized to about + // the same size. This size is smaller than the original bounds, so we estimate + // the difference in this offset. + int offset = size / 5; + foreground.setBounds(-offset, -offset, size + offset, size + offset); + foreground.draw(canvas); + + canvas.setBitmap(null); + return bitmap; + } + /** * Returns a {@link BitmapInfo} for the entire bubble icon including the badge. */ diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java index c1dd8c36ff6f..48a9b91721f5 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java @@ -30,10 +30,6 @@ public class BubbleLoggerImpl extends UiEventLoggerImpl implements BubbleLogger * @param e UI event */ public void log(Bubble b, UiEventEnum e) { - if (b.getInstanceId() == null) { - // Added from persistence -- TODO log this with specific event? - return; - } logWithInstanceId(e, b.getAppUid(), b.getPackageName(), b.getInstanceId()); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 749b537ea364..f2d873232fb3 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -258,13 +258,8 @@ public class BubbleStackView extends FrameLayout /** Layout change listener that moves the stack to the nearest valid position on rotation. */ private OnLayoutChangeListener mOrientationChangedListener; - /** Whether the stack was on the left side of the screen prior to rotation. */ - private boolean mWasOnLeftBeforeRotation = false; - /** - * How far down the screen the stack was before rotation, in terms of percentage of the way down - * the allowable region. Defaults to -1 if not set. - */ - private float mVerticalPosPercentBeforeRotation = -1; + + @Nullable private RelativeStackPosition mRelativeStackPositionBeforeRotation; private int mMaxBubbles; private int mBubbleSize; @@ -967,9 +962,10 @@ public class BubbleStackView extends FrameLayout mExpandedViewContainer.setTranslationY(getExpandedViewY()); mExpandedViewContainer.setAlpha(1f); } - if (mVerticalPosPercentBeforeRotation >= 0) { - mStackAnimationController.moveStackToSimilarPositionAfterRotation( - mWasOnLeftBeforeRotation, mVerticalPosPercentBeforeRotation); + if (mRelativeStackPositionBeforeRotation != null) { + mStackAnimationController.setStackPosition( + mRelativeStackPositionBeforeRotation); + mRelativeStackPositionBeforeRotation = null; } removeOnLayoutChangeListener(mOrientationChangedListener); }; @@ -1231,13 +1227,7 @@ public class BubbleStackView extends FrameLayout com.android.internal.R.dimen.status_bar_height); mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top); - final RectF allowablePos = mStackAnimationController.getAllowableStackPositionRegion(); - mWasOnLeftBeforeRotation = mStackAnimationController.isStackOnLeftSide(); - mVerticalPosPercentBeforeRotation = - (mStackAnimationController.getStackPosition().y - allowablePos.top) - / (allowablePos.bottom - allowablePos.top); - mVerticalPosPercentBeforeRotation = - Math.max(0f, Math.min(1f, mVerticalPosPercentBeforeRotation)); + mRelativeStackPositionBeforeRotation = mStackAnimationController.getRelativeStackPosition(); addOnLayoutChangeListener(mOrientationChangedListener); hideFlyoutImmediate(); @@ -1506,7 +1496,7 @@ public class BubbleStackView extends FrameLayout if (getBubbleCount() == 0 && mShouldShowUserEducation) { // Override the default stack position if we're showing user education. mStackAnimationController.setStackPosition( - mStackAnimationController.getDefaultStartPosition()); + mStackAnimationController.getStartPosition()); } if (getBubbleCount() == 0) { @@ -1586,6 +1576,11 @@ public class BubbleStackView extends FrameLayout Log.d(TAG, "setSelectedBubble: " + bubbleToSelect); } + if (bubbleToSelect == null) { + mBubbleData.setShowingOverflow(false); + return; + } + // Ignore this new bubble only if it is the exact same bubble object. Otherwise, we'll want // to re-render it even if it has the same key (equals() returns true). If the currently // expanded bubble is removed and instantly re-added, we'll get back a new Bubble instance @@ -1594,10 +1589,11 @@ public class BubbleStackView extends FrameLayout if (mExpandedBubble == bubbleToSelect) { return; } - if (bubbleToSelect == null || bubbleToSelect.getKey() != BubbleOverflow.KEY) { - mBubbleData.setShowingOverflow(false); - } else { + + if (bubbleToSelect.getKey() == BubbleOverflow.KEY) { mBubbleData.setShowingOverflow(true); + } else { + mBubbleData.setShowingOverflow(false); } if (mIsExpanded && mIsExpansionAnimating) { @@ -1715,7 +1711,7 @@ public class BubbleStackView extends FrameLayout // Post so we have height of mUserEducationView mUserEducationView.post(() -> { final int viewHeight = mUserEducationView.getHeight(); - PointF stackPosition = mStackAnimationController.getDefaultStartPosition(); + PointF stackPosition = mStackAnimationController.getStartPosition(); final float translationY = stackPosition.y + (mBubbleSize / 2) - (viewHeight / 2); mUserEducationView.setTranslationY(translationY); mUserEducationView.animate() @@ -2871,10 +2867,18 @@ public class BubbleStackView extends FrameLayout .floatValue(); } + public void setStackStartPosition(RelativeStackPosition position) { + mStackAnimationController.setStackStartPosition(position); + } + public PointF getStackPosition() { return mStackAnimationController.getStackPosition(); } + public RelativeStackPosition getRelativeStackPosition() { + return mStackAnimationController.getRelativeStackPosition(); + } + /** * Logs the bubble UI event. * @@ -2938,4 +2942,47 @@ public class BubbleStackView extends FrameLayout } return bubbles; } + + /** + * Representation of stack position that uses relative properties rather than absolute + * coordinates. This is used to maintain similar stack positions across configuration changes. + */ + public static class RelativeStackPosition { + /** Whether to place the stack at the leftmost allowed position. */ + private boolean mOnLeft; + + /** + * How far down the vertically allowed region to place the stack. For example, if the stack + * allowed region is between y = 100 and y = 1100 and this is 0.2f, we'll place the stack at + * 100 + (0.2f * 1000) = 300. + */ + private float mVerticalOffsetPercent; + + public RelativeStackPosition(boolean onLeft, float verticalOffsetPercent) { + mOnLeft = onLeft; + mVerticalOffsetPercent = clampVerticalOffsetPercent(verticalOffsetPercent); + } + + /** Constructs a relative position given a region and a point in that region. */ + public RelativeStackPosition(PointF position, RectF region) { + mOnLeft = position.x < region.width() / 2; + mVerticalOffsetPercent = + clampVerticalOffsetPercent((position.y - region.top) / region.height()); + } + + /** Ensures that the offset percent is between 0f and 1f. */ + private float clampVerticalOffsetPercent(float offsetPercent) { + return Math.max(0f, Math.min(1f, offsetPercent)); + } + + /** + * Given an allowable stack position region, returns the point within that region + * represented by this relative position. + */ + public PointF getAbsolutePositionInRegion(RectF region) { + return new PointF( + mOnLeft ? region.left : region.right, + region.top + mVerticalOffsetPercent * region.height()); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java index b378469c4c98..e835ea206e59 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java @@ -35,6 +35,7 @@ import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.systemui.R; +import com.android.systemui.bubbles.BubbleStackView; import com.android.systemui.util.FloatingContentCoordinator; import com.android.systemui.util.animation.PhysicsAnimator; import com.android.systemui.util.magnetictarget.MagnetizedObject; @@ -125,6 +126,9 @@ public class StackAnimationController extends */ private Rect mAnimatingToBounds = new Rect(); + /** Initial starting location for the stack. */ + @Nullable private BubbleStackView.RelativeStackPosition mStackStartPosition; + /** Whether or not the stack's start position has been set. */ private boolean mStackMovedToStartPosition = false; @@ -431,21 +435,6 @@ public class StackAnimationController extends return stackPos; } - /** - * Moves the stack in response to rotation. We keep it in the most similar position by keeping - * it on the same side, and positioning it the same percentage of the way down the screen - * (taking status bar/nav bar into account by using the allowable region's height). - */ - public void moveStackToSimilarPositionAfterRotation(boolean wasOnLeft, float verticalPercent) { - final RectF allowablePos = getAllowableStackPositionRegion(); - final float allowableRegionHeight = allowablePos.bottom - allowablePos.top; - - final float x = wasOnLeft ? allowablePos.left : allowablePos.right; - final float y = (allowableRegionHeight * verticalPercent) + allowablePos.top; - - setStackPosition(new PointF(x, y)); - } - /** Description of current animation controller state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("StackAnimationController state:"); @@ -815,7 +804,7 @@ public class StackAnimationController extends } else { // When all children are removed ensure stack position is sane setStackPosition(mRestingStackPosition == null - ? getDefaultStartPosition() + ? getStartPosition() : mRestingStackPosition); // Remove the stack from the coordinator since we don't have any bubbles and aren't @@ -868,7 +857,7 @@ public class StackAnimationController extends mLayout.setVisibility(View.INVISIBLE); mLayout.post(() -> { setStackPosition(mRestingStackPosition == null - ? getDefaultStartPosition() + ? getStartPosition() : mRestingStackPosition); mStackMovedToStartPosition = true; mLayout.setVisibility(View.VISIBLE); @@ -938,15 +927,47 @@ public class StackAnimationController extends } } - /** Returns the default stack position, which is on the top left. */ - public PointF getDefaultStartPosition() { - boolean isRtl = mLayout != null - && mLayout.getResources().getConfiguration().getLayoutDirection() - == View.LAYOUT_DIRECTION_RTL; - return new PointF(isRtl - ? getAllowableStackPositionRegion().right - : getAllowableStackPositionRegion().left, - getAllowableStackPositionRegion().top + mStackStartingVerticalOffset); + public void setStackPosition(BubbleStackView.RelativeStackPosition position) { + setStackPosition(position.getAbsolutePositionInRegion(getAllowableStackPositionRegion())); + } + + public BubbleStackView.RelativeStackPosition getRelativeStackPosition() { + return new BubbleStackView.RelativeStackPosition( + mStackPosition, getAllowableStackPositionRegion()); + } + + /** + * Sets the starting position for the stack, where it will be located when the first bubble is + * added. + */ + public void setStackStartPosition(BubbleStackView.RelativeStackPosition position) { + mStackStartPosition = position; + } + + /** + * Returns the starting stack position. If {@link #setStackStartPosition} was called, this will + * return that position - otherwise, a reasonable default will be returned. + */ + @Nullable public PointF getStartPosition() { + if (mLayout == null) { + return null; + } + + if (mStackStartPosition == null) { + // Start on the left if we're in LTR, right otherwise. + final boolean startOnLeft = + mLayout.getResources().getConfiguration().getLayoutDirection() + != View.LAYOUT_DIRECTION_RTL; + + final float startingVerticalOffset = mLayout.getResources().getDimensionPixelOffset( + R.dimen.bubble_stack_starting_offset_y); + + mStackStartPosition = new BubbleStackView.RelativeStackPosition( + startOnLeft, + startingVerticalOffset / getAllowableStackPositionRegion().height()); + } + + return mStackStartPosition.getAbsolutePositionInRegion(getAllowableStackPositionRegion()); } private boolean isStackPositionSet() { diff --git a/packages/SystemUI/src/com/android/systemui/controls/CustomIconCache.kt b/packages/SystemUI/src/com/android/systemui/controls/CustomIconCache.kt new file mode 100644 index 000000000000..cca0f1653757 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/controls/CustomIconCache.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.controls + +import android.content.ComponentName +import android.graphics.drawable.Icon +import androidx.annotation.GuardedBy +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Icon cache for custom icons sent with controls. + * + * It assumes that only one component can be current at the time, to minimize the number of icons + * stored at a given time. + */ +@Singleton +class CustomIconCache @Inject constructor() { + + private var currentComponent: ComponentName? = null + @GuardedBy("cache") + private val cache: MutableMap<String, Icon> = LinkedHashMap() + + /** + * Store an icon in the cache. + * + * If the icons currently stored do not correspond to the component to be stored, the cache is + * cleared first. + */ + fun store(component: ComponentName, controlId: String, icon: Icon?) { + if (component != currentComponent) { + clear() + currentComponent = component + } + synchronized(cache) { + if (icon != null) { + cache.put(controlId, icon) + } else { + cache.remove(controlId) + } + } + } + + /** + * Retrieves a custom icon stored in the cache. + * + * It will return null if the component requested is not the one whose icons are stored, or if + * there is no icon cached for that id. + */ + fun retrieve(component: ComponentName, controlId: String): Icon? { + if (component != currentComponent) return null + return synchronized(cache) { + cache.get(controlId) + } + } + + private fun clear() { + synchronized(cache) { + cache.clear() + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt index 1bda841d4a63..d930c98cabe1 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt @@ -87,6 +87,10 @@ class ControlsFavoritePersistenceWrapper( * @param list a list of favorite controls. The list will be stored in the same order. */ fun storeFavorites(structures: List<StructureInfo>) { + if (structures.isEmpty() && !file.exists()) { + // Do not create a new file to store nothing + return + } executor.execute { Log.d(TAG, "Saving data to file: $file") val atomicFile = AtomicFile(file) diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt index ec8bfc6fa2ae..977e46ac3b44 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsProviderLifecycleManager.kt @@ -76,7 +76,8 @@ class ControlsProviderLifecycleManager( private const val LOAD_TIMEOUT_SECONDS = 20L // seconds private const val MAX_BIND_RETRIES = 5 private const val DEBUG = true - private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE + private val BIND_FLAGS = Context.BIND_AUTO_CREATE or Context.BIND_FOREGROUND_SERVICE or + Context.BIND_NOT_PERCEPTIBLE } private val intent = Intent().apply { diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt index c683a87d6282..40662536e57e 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlAdapter.kt @@ -72,8 +72,13 @@ class ControlAdapter( TYPE_CONTROL -> { ControlHolder( layoutInflater.inflate(R.layout.controls_base_item, parent, false).apply { - layoutParams.apply { + (layoutParams as ViewGroup.MarginLayoutParams).apply { width = ViewGroup.LayoutParams.MATCH_PARENT + // Reset margins as they will be set through the decoration + topMargin = 0 + bottomMargin = 0 + leftMargin = 0 + rightMargin = 0 } elevation = this@ControlAdapter.elevation background = parent.context.getDrawable( @@ -258,6 +263,7 @@ internal class ControlHolder( val context = itemView.context val fg = context.getResources().getColorStateList(ri.foreground, context.getTheme()) + icon.imageTintList = null ci.customIcon?.let { icon.setImageIcon(it) } ?: run { @@ -386,7 +392,7 @@ class MarginItemDecorator( val type = parent.adapter?.getItemViewType(position) if (type == ControlAdapter.TYPE_CONTROL) { outRect.apply { - top = topMargin + top = topMargin * 2 // Use double margin, as we are not setting bottom left = sideMargins right = sideMargins bottom = 0 diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt index ff40a8a883ae..f68388d5db3f 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsEditingActivity.kt @@ -29,6 +29,7 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.android.systemui.R import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsControllerImpl import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.globalactions.GlobalActionsComponent @@ -42,7 +43,8 @@ import javax.inject.Inject class ControlsEditingActivity @Inject constructor( private val controller: ControlsControllerImpl, broadcastDispatcher: BroadcastDispatcher, - private val globalActionsComponent: GlobalActionsComponent + private val globalActionsComponent: GlobalActionsComponent, + private val customIconCache: CustomIconCache ) : LifecycleActivity() { companion object { @@ -170,7 +172,7 @@ class ControlsEditingActivity @Inject constructor( private fun setUpList() { val controls = controller.getFavoritesForStructure(component, structure) - model = FavoritesModel(component, controls, favoritesModelCallback) + model = FavoritesModel(customIconCache, component, controls, favoritesModelCallback) val elevation = resources.getFloat(R.dimen.control_card_elevation) val recyclerView = requireViewById<RecyclerView>(R.id.list) recyclerView.alpha = 0.0f diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt index 4ef64a5cddbf..ad0e7a541f98 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsModel.kt @@ -114,11 +114,27 @@ data class ControlStatusWrapper( val controlStatus: ControlStatus ) : ElementWrapper(), ControlInterface by controlStatus +private fun nullIconGetter(_a: ComponentName, _b: String): Icon? = null + data class ControlInfoWrapper( override val component: ComponentName, val controlInfo: ControlInfo, override var favorite: Boolean ) : ElementWrapper(), ControlInterface { + + var customIconGetter: (ComponentName, String) -> Icon? = ::nullIconGetter + private set + + // Separate constructor so the getter is not used in auto-generated methods + constructor( + component: ComponentName, + controlInfo: ControlInfo, + favorite: Boolean, + customIconGetter: (ComponentName, String) -> Icon? + ): this(component, controlInfo, favorite) { + this.customIconGetter = customIconGetter + } + override val controlId: String get() = controlInfo.controlId override val title: CharSequence @@ -128,8 +144,7 @@ data class ControlInfoWrapper( override val deviceType: Int get() = controlInfo.deviceType override val customIcon: Icon? - // Will need to address to support for edit activity - get() = null + get() = customIconGetter(component, controlId) } data class DividerWrapper( diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt index 524250134e9b..f9ce6362f4f8 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/FavoritesModel.kt @@ -21,6 +21,7 @@ import android.util.Log import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.android.systemui.controls.ControlInterface +import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlInfo import java.util.Collections @@ -35,6 +36,7 @@ import java.util.Collections * @property favoritesModelCallback callback to notify on first change and empty favorites */ class FavoritesModel( + private val customIconCache: CustomIconCache, private val componentName: ComponentName, favorites: List<ControlInfo>, private val favoritesModelCallback: FavoritesModelCallback @@ -83,7 +85,7 @@ class FavoritesModel( } override val elements: List<ElementWrapper> = favorites.map { - ControlInfoWrapper(componentName, it, true) + ControlInfoWrapper(componentName, it, true, customIconCache::retrieve) } + DividerWrapper() /** diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 22d6b6bb75c3..e15380b42a78 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -92,7 +92,7 @@ class ControlActionCoordinatorImpl @Inject constructor( override fun setValue(cvh: ControlViewHolder, templateId: String, newValue: Float) { bouncerOrRun(Action(cvh.cws.ci.controlId, { cvh.action(FloatAction(templateId, newValue)) - }, true /* blockable */)) + }, false /* blockable */)) } override fun longPress(cvh: ControlViewHolder) { diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index 1eb7e2168a6a..5a525974f3cb 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -44,6 +44,7 @@ import android.widget.Space import android.widget.TextView import com.android.systemui.R import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo @@ -75,7 +76,8 @@ class ControlsUiControllerImpl @Inject constructor ( @Main val sharedPreferences: SharedPreferences, val controlActionCoordinator: ControlActionCoordinator, private val activityStarter: ActivityStarter, - private val shadeController: ShadeController + private val shadeController: ShadeController, + private val iconCache: CustomIconCache ) : ControlsUiController { companion object { @@ -102,6 +104,7 @@ class ControlsUiControllerImpl @Inject constructor ( private var hidden = true private lateinit var dismissGlobalActions: Runnable private val popupThemedContext = ContextThemeWrapper(context, R.style.Control_ListPopupWindow) + private var retainCache = false private val collator = Collator.getInstance(context.resources.configuration.locales[0]) private val localeComparator = compareBy<SelectionItem, CharSequence>(collator) { @@ -147,6 +150,7 @@ class ControlsUiControllerImpl @Inject constructor ( this.parent = parent this.dismissGlobalActions = dismissGlobalActions hidden = false + retainCache = false allStructures = controlsController.get().getFavorites() selectedStructure = loadPreference(allStructures) @@ -233,6 +237,8 @@ class ControlsUiControllerImpl @Inject constructor ( } putIntentExtras(i, si) startActivity(context, i) + + retainCache = true } private fun putIntentExtras(intent: Intent, si: StructureInfo) { @@ -495,13 +501,14 @@ class ControlsUiControllerImpl @Inject constructor ( controlsListingController.get().removeCallback(listingCallback) - RenderInfo.clearCache() + if (!retainCache) RenderInfo.clearCache() } override fun onRefreshState(componentName: ComponentName, controls: List<Control>) { controls.forEach { c -> controlsById.get(ControlKey(componentName, c.getControlId()))?.let { Log.d(ControlsUiController.TAG, "onRefreshState() for id: " + c.getControlId()) + iconCache.store(componentName, c.controlId, c.customIcon) val cws = ControlWithState(componentName, it.ci, c) val key = ControlKey(componentName, c.getControlId()) controlsById.put(key, cws) diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java index 56d0fa237b82..6e8d63b2c516 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java @@ -18,7 +18,9 @@ package com.android.systemui.dagger; import android.content.BroadcastReceiver; -import com.android.systemui.screenshot.GlobalScreenshot.ActionProxyReceiver; +import com.android.systemui.screenshot.ActionProxyReceiver; +import com.android.systemui.screenshot.DeleteScreenshotReceiver; +import com.android.systemui.screenshot.SmartActionsReceiver; import dagger.Binds; import dagger.Module; @@ -30,10 +32,31 @@ import dagger.multibindings.IntoMap; */ @Module public abstract class DefaultBroadcastReceiverBinder { - /** */ + /** + * + */ @Binds @IntoMap @ClassKey(ActionProxyReceiver.class) public abstract BroadcastReceiver bindActionProxyReceiver( ActionProxyReceiver broadcastReceiver); + + /** + * + */ + @Binds + @IntoMap + @ClassKey(DeleteScreenshotReceiver.class) + public abstract BroadcastReceiver bindDeleteScreenshotReceiver( + DeleteScreenshotReceiver broadcastReceiver); + + /** + * + */ + @Binds + @IntoMap + @ClassKey(SmartActionsReceiver.class) + public abstract BroadcastReceiver bindSmartActionsReceiver( + SmartActionsReceiver broadcastReceiver); + } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java index f683a639af10..8472b1b54c48 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java @@ -42,6 +42,7 @@ import android.hardware.SensorPrivacyManager; import android.hardware.display.DisplayManager; import android.media.AudioManager; import android.media.MediaRouter2Manager; +import android.media.session.MediaSessionManager; import android.net.ConnectivityManager; import android.net.NetworkScoreManager; import android.net.wifi.WifiManager; @@ -219,6 +220,11 @@ public class SystemServicesModule { } @Provides + static MediaSessionManager provideMediaSessionManager(Context context) { + return context.getSystemService(MediaSessionManager.class); + } + + @Provides @Singleton static NetworkScoreManager provideNetworkScoreManager(Context context) { return context.getSystemService(NetworkScoreManager.class); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java index aeba64a59a7a..cd0ba290db46 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java @@ -103,7 +103,7 @@ public abstract class SystemUIDefaultModule { @Binds @Singleton - public abstract QSFactory provideQSFactory(QSFactoryImpl qsFactoryImpl); + public abstract QSFactory bindQSFactory(QSFactoryImpl qsFactoryImpl); @Binds abstract DockManager bindDockManager(DockManagerImpl dockManager); diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java index ae7d82ac4a5e..253a35c55698 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeMachine.java @@ -430,8 +430,12 @@ public class DozeMachine { /** Give the Part a chance to clean itself up. */ default void destroy() {} - /** Alerts that the screenstate is being changed. */ - default void onScreenState(int state) {} + /** + * Alerts that the screenstate is being changed. + * Note: This may be called from within a call to transitionTo, so local DozeState may not + * be accurate nor match with the new displayState. + */ + default void onScreenState(int displayState) {} } /** A wrapper interface for {@link android.service.dreams.DreamService} */ diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java index 64cfb4bcd058..a11997b6b845 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java @@ -69,7 +69,6 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi * --ei brightness_bucket 1} */ private int mDebugBrightnessBucket = -1; - private DozeMachine.State mState; @VisibleForTesting public DozeScreenBrightness(Context context, DozeMachine.Service service, @@ -109,7 +108,6 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi @Override public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) { - mState = newState; switch (newState) { case INITIALIZED: case DOZE: @@ -127,10 +125,7 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi @Override public void onScreenState(int state) { - if (!mScreenOff - && (mState == DozeMachine.State.DOZE_AOD - || mState == DozeMachine.State.DOZE_AOD_DOCKED) - && (state == Display.STATE_DOZE || state == Display.STATE_DOZE_SUSPEND)) { + if (state == Display.STATE_DOZE || state == Display.STATE_DOZE_SUSPEND) { setLightSensorEnabled(true); } else { setLightSensorEnabled(false); diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java index 2702bc4f1f22..ff25439a5f9f 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java @@ -149,6 +149,7 @@ import java.util.Set; import java.util.concurrent.Executor; import javax.inject.Inject; +import javax.inject.Provider; /** * Helper to show the global actions dialog. Each item is an {@link Action} that may show depending @@ -402,7 +403,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, if (mDialog != null) { if (!mDialog.isShowingControls() && shouldShowControls()) { mDialog.showControls(mControlsUiControllerOptional.get()); - } else if (shouldShowLockMessage()) { + } else if (shouldShowLockMessage(mDialog)) { mDialog.showLockMessage(); } } @@ -699,19 +700,17 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mPowerAdapter = new MyPowerOptionsAdapter(); mDepthController.setShowingHomeControls(true); - GlobalActionsPanelPlugin.PanelViewController walletViewController = - getWalletViewController(); ControlsUiController uiController = null; if (mControlsUiControllerOptional.isPresent() && shouldShowControls()) { uiController = mControlsUiControllerOptional.get(); } ActionsDialog dialog = new ActionsDialog(mContext, mAdapter, mOverflowAdapter, - walletViewController, mDepthController, mSysuiColorExtractor, + this::getWalletViewController, mDepthController, mSysuiColorExtractor, mStatusBarService, mNotificationShadeWindowController, controlsAvailable(), uiController, mSysUiState, this::onRotate, mKeyguardShowing, mPowerAdapter); - if (shouldShowLockMessage()) { + if (shouldShowLockMessage(dialog)) { dialog.showLockMessage(); } dialog.setCanceledOnTouchOutside(false); // Handled by the custom class. @@ -2144,7 +2143,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private MultiListLayout mGlobalActionsLayout; private Drawable mBackgroundDrawable; private final SysuiColorExtractor mColorExtractor; - private final GlobalActionsPanelPlugin.PanelViewController mWalletViewController; + private final Provider<GlobalActionsPanelPlugin.PanelViewController> mWalletFactory; + @Nullable private GlobalActionsPanelPlugin.PanelViewController mWalletViewController; private boolean mKeyguardShowing; private boolean mShowing; private float mScrimAlpha; @@ -2164,7 +2164,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private TextView mLockMessage; ActionsDialog(Context context, MyAdapter adapter, MyOverflowAdapter overflowAdapter, - GlobalActionsPanelPlugin.PanelViewController walletViewController, + Provider<GlobalActionsPanelPlugin.PanelViewController> walletFactory, NotificationShadeDepthController depthController, SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService, NotificationShadeWindowController notificationShadeWindowController, @@ -2185,6 +2185,7 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mSysUiState = sysuiState; mOnRotateCallback = onRotateCallback; mKeyguardShowing = keyguardShowing; + mWalletFactory = walletFactory; // Window initialization Window window = getWindow(); @@ -2207,7 +2208,6 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, window.getAttributes().setFitInsetsTypes(0 /* types */); setTitle(R.string.global_actions); - mWalletViewController = walletViewController; initializeLayout(); } @@ -2220,8 +2220,13 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, mControlsUiController.show(mControlsView, this::dismissForControlsActivity); } + private boolean isWalletViewAvailable() { + return mWalletViewController != null && mWalletViewController.getPanelContent() != null; + } + private void initializeWalletView() { - if (mWalletViewController == null || mWalletViewController.getPanelContent() == null) { + mWalletViewController = mWalletFactory.get(); + if (!isWalletViewAvailable()) { return; } @@ -2527,6 +2532,8 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, private void dismissWallet() { if (mWalletViewController != null) { mWalletViewController.onDismissed(); + // The wallet controller should not be re-used after being dismissed. + mWalletViewController = null; } } @@ -2668,18 +2675,12 @@ public class GlobalActionsDialog implements DialogInterface.OnDismissListener, && !mControlsServiceInfos.isEmpty(); } - private boolean walletViewAvailable() { - GlobalActionsPanelPlugin.PanelViewController walletViewController = - getWalletViewController(); - return walletViewController != null && walletViewController.getPanelContent() != null; - } - - private boolean shouldShowLockMessage() { + private boolean shouldShowLockMessage(ActionsDialog dialog) { boolean isLockedAfterBoot = mLockPatternUtils.getStrongAuthForUser(getCurrentUser().id) == STRONG_AUTH_REQUIRED_AFTER_BOOT; return !mKeyguardStateController.isUnlocked() && (!mShowLockScreenCardsAndControls || isLockedAfterBoot) - && (controlsAvailable() || walletViewAvailable()); + && (controlsAvailable() || dialog.isWalletViewAvailable()); } private void onPowerMenuLockScreenSettingsChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java new file mode 100644 index 000000000000..aca033e99623 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaBrowserFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import android.content.ComponentName; +import android.content.Context; +import android.media.browse.MediaBrowser; +import android.os.Bundle; + +import javax.inject.Inject; + +/** + * Testable wrapper around {@link MediaBrowser} constructor + */ +public class MediaBrowserFactory { + private final Context mContext; + + @Inject + public MediaBrowserFactory(Context context) { + mContext = context; + } + + /** + * Creates a new MediaBrowser + * + * @param serviceComponent + * @param callback + * @param rootHints + * @return + */ + public MediaBrowser create(ComponentName serviceComponent, + MediaBrowser.ConnectionCallback callback, Bundle rootHints) { + return new MediaBrowser(mContext, serviceComponent, callback, rootHints); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index e12b7dd259a5..d8d9bd7e95b8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -11,6 +11,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import androidx.annotation.VisibleForTesting import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter @@ -22,6 +23,7 @@ import com.android.systemui.util.Utils import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.concurrency.DelayableExecutor +import java.util.TreeMap import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton @@ -41,7 +43,7 @@ class MediaCarouselController @Inject constructor( private val mediaHostStatesManager: MediaHostStatesManager, private val activityStarter: ActivityStarter, @Main executor: DelayableExecutor, - mediaManager: MediaDataFilter, + private val mediaManager: MediaDataManager, configurationController: ConfigurationController, falsingManager: FalsingManager ) { @@ -103,13 +105,12 @@ class MediaCarouselController @Inject constructor( private val mediaCarousel: MediaScrollView private val mediaCarouselScrollHandler: MediaCarouselScrollHandler val mediaFrame: ViewGroup - val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() private lateinit var settingsButton: View - private val mediaData: MutableMap<String, MediaData> = mutableMapOf() private val mediaContent: ViewGroup private val pageIndicator: PageIndicator private val visualStabilityCallback: VisualStabilityManager.Callback private var needsReordering: Boolean = false + private var keysNeedRemoval = mutableSetOf<String>() private var isRtl: Boolean = false set(value) { if (value != field) { @@ -123,7 +124,7 @@ class MediaCarouselController @Inject constructor( set(value) { if (field != value) { field = value - for (player in mediaPlayers.values) { + for (player in MediaPlayerData.players()) { player.setListening(field) } } @@ -135,6 +136,7 @@ class MediaCarouselController @Inject constructor( } override fun onOverlayChanged() { + recreatePlayers() inflateSettingsButton() } @@ -150,7 +152,7 @@ class MediaCarouselController @Inject constructor( pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation, - falsingManager) + this::closeGuts, falsingManager) isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) @@ -160,6 +162,10 @@ class MediaCarouselController @Inject constructor( needsReordering = false reorderAllPlayers() } + + keysNeedRemoval.forEach { removePlayer(it) } + keysNeedRemoval.clear() + // Let's reset our scroll position mediaCarouselScrollHandler.scrollToStart() } @@ -167,20 +173,23 @@ class MediaCarouselController @Inject constructor( true /* persistent */) mediaManager.addListener(object : MediaDataManager.Listener { override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { - oldKey?.let { mediaData.remove(it) } - if (!data.active && !Utils.useMediaResumption(context)) { - // This view is inactive, let's remove this! This happens e.g when dismissing / - // timing out a view. We still have the data around because resumption could - // be on, but we should save the resources and release this. - onMediaDataRemoved(key) + addOrUpdatePlayer(key, oldKey, data) + val canRemove = data.isPlaying?.let { !it } ?: data.isClearable && !data.active + if (canRemove && !Utils.useMediaResumption(context)) { + // This view isn't playing, let's remove this! This happens e.g when + // dismissing/timing out a view. We still have the data around because + // resumption could be on, but we should save the resources and release this. + if (visualStabilityManager.isReorderingAllowed) { + onMediaDataRemoved(key) + } else { + keysNeedRemoval.add(key) + } } else { - mediaData.put(key, data) - addOrUpdatePlayer(key, oldKey, data) + keysNeedRemoval.remove(key) } } override fun onMediaDataRemoved(key: String) { - mediaData.remove(key) removePlayer(key) } }) @@ -223,53 +232,36 @@ class MediaCarouselController @Inject constructor( } private fun reorderAllPlayers() { - for (mediaPlayer in mediaPlayers.values) { - val view = mediaPlayer.view?.player - if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { - mediaContent.removeView(view) - mediaContent.addView(view, 0) + mediaContent.removeAllViews() + for (mediaPlayer in MediaPlayerData.players()) { + mediaPlayer.view?.let { + mediaContent.addView(it.player) } } mediaCarouselScrollHandler.onPlayersChanged() } private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { - // If the key was changed, update entry - val oldData = mediaPlayers[oldKey] - if (oldData != null) { - val oldData = mediaPlayers.remove(oldKey) - mediaPlayers.put(key, oldData!!)?.let { - Log.wtf(TAG, "new key $key already exists when migrating from $oldKey") - } - } - var existingPlayer = mediaPlayers[key] + val existingPlayer = MediaPlayerData.getMediaPlayer(key, oldKey) if (existingPlayer == null) { - existingPlayer = mediaControlPanelFactory.get() - existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), - mediaContent)) - existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions - mediaPlayers[key] = existingPlayer + var newPlayer = mediaControlPanelFactory.get() + newPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), mediaContent)) + newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - existingPlayer.view?.player?.setLayoutParams(lp) - existingPlayer.bind(data) - existingPlayer.setListening(currentlyExpanded) - updatePlayerToState(existingPlayer, noAnimation = true) - if (existingPlayer.isPlaying) { - mediaContent.addView(existingPlayer.view?.player, 0) - } else { - mediaContent.addView(existingPlayer.view?.player) - } + newPlayer.view?.player?.setLayoutParams(lp) + newPlayer.bind(data, key) + newPlayer.setListening(currentlyExpanded) + MediaPlayerData.addMediaPlayer(key, data, newPlayer) + updatePlayerToState(newPlayer, noAnimation = true) + reorderAllPlayers() } else { - existingPlayer.bind(data) - if (existingPlayer.isPlaying && - mediaContent.indexOfChild(existingPlayer.view?.player) != 0) { - if (visualStabilityManager.isReorderingAllowed) { - mediaContent.removeView(existingPlayer.view?.player) - mediaContent.addView(existingPlayer.view?.player, 0) - } else { - needsReordering = true - } + existingPlayer.bind(data, key) + MediaPlayerData.addMediaPlayer(key, data, existingPlayer) + if (visualStabilityManager.isReorderingAllowed) { + reorderAllPlayers() + } else { + needsReordering = true } } updatePageIndicator() @@ -277,29 +269,30 @@ class MediaCarouselController @Inject constructor( mediaCarousel.requiresRemeasuring = true // Check postcondition: mediaContent should have the same number of children as there are // elements in mediaPlayers. - if (mediaPlayers.size != mediaContent.childCount) { + if (MediaPlayerData.players().size != mediaContent.childCount) { Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync") } } - private fun removePlayer(key: String) { - val removed = mediaPlayers.remove(key) + private fun removePlayer(key: String, dismissMediaData: Boolean = true) { + val removed = MediaPlayerData.removeMediaPlayer(key) removed?.apply { mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaContent.removeView(removed.view?.player) removed.onDestroy() mediaCarouselScrollHandler.onPlayersChanged() updatePageIndicator() + + if (dismissMediaData) { + // Inform the media manager of a potentially late dismissal + mediaManager.dismissMediaData(key, 0L) + } } } private fun recreatePlayers() { - // Note that this will scramble the order of players. Actively playing sessions will, at - // least, still be put in the front. If we want to maintain order, then more work is - // needed. - mediaData.forEach { - key, data -> - removePlayer(key) + MediaPlayerData.mediaData().forEach { (key, data) -> + removePlayer(key, dismissMediaData = false) addOrUpdatePlayer(key = key, oldKey = null, data = data) } } @@ -337,7 +330,7 @@ class MediaCarouselController @Inject constructor( currentStartLocation = startLocation currentEndLocation = endLocation currentTransitionProgress = progress - for (mediaPlayer in mediaPlayers.values) { + for (mediaPlayer in MediaPlayerData.players()) { updatePlayerToState(mediaPlayer, immediately) } maybeResetSettingsCog() @@ -386,7 +379,7 @@ class MediaCarouselController @Inject constructor( private fun updateCarouselDimensions() { var width = 0 var height = 0 - for (mediaPlayer in mediaPlayers.values) { + for (mediaPlayer in MediaPlayerData.players()) { val controller = mediaPlayer.mediaViewController // When transitioning the view to gone, the view gets smaller, but the translation // Doesn't, let's add the translation @@ -448,7 +441,7 @@ class MediaCarouselController @Inject constructor( this.desiredLocation = desiredLocation this.desiredHostState = it currentlyExpanded = it.expansion > 0 - for (mediaPlayer in mediaPlayers.values) { + for (mediaPlayer in MediaPlayerData.players()) { if (animate) { mediaPlayer.mediaViewController.animatePendingStateChange( duration = duration, @@ -469,6 +462,12 @@ class MediaCarouselController @Inject constructor( } } + fun closeGuts() { + MediaPlayerData.players().forEach { + it.closeGuts(true) + } + } + /** * Update the size of the carousel, remeasuring it if necessary. */ @@ -491,3 +490,49 @@ class MediaCarouselController @Inject constructor( } } } + +@VisibleForTesting +internal object MediaPlayerData { + private data class MediaSortKey( + val data: MediaData, + val updateTime: Long = 0 + ) + + private val comparator = + compareByDescending<MediaSortKey> { it.data.isPlaying } + .thenByDescending { it.data.isLocalSession } + .thenByDescending { !it.data.resumption } + .thenByDescending { it.updateTime } + + private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator) + private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf() + + fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel) { + removeMediaPlayer(key) + val sortKey = MediaSortKey(data, System.currentTimeMillis()) + mediaData.put(key, sortKey) + mediaPlayers.put(sortKey, player) + } + + fun getMediaPlayer(key: String, oldKey: String?): MediaControlPanel? { + // If the key was changed, update entry + oldKey?.let { + if (it != key) { + mediaData.remove(it)?.let { sortKey -> mediaData.put(key, sortKey) } + } + } + return mediaData.get(key)?.let { mediaPlayers.get(it) } + } + + fun removeMediaPlayer(key: String) = mediaData.remove(key)?.let { mediaPlayers.remove(it) } + + fun mediaData() = mediaData.entries.map { e -> Pair(e.key, e.value.data) } + + fun players() = mediaPlayers.values + + @VisibleForTesting + fun clear() { + mediaData.clear() + mediaPlayers.clear() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt index 3096908aca21..77cac5023db3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -56,6 +56,7 @@ class MediaCarouselScrollHandler( private val mainExecutor: DelayableExecutor, private val dismissCallback: () -> Unit, private var translationChangedListener: () -> Unit, + private val closeGuts: () -> Unit, private val falsingManager: FalsingManager ) { /** @@ -452,6 +453,7 @@ class MediaCarouselScrollHandler( val nowScrolledIn = scrollIntoCurrentMedia != 0 if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { activeMediaIndex = newIndex + closeGuts() updatePlayerVisibilities() } val relativeLocation = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java index 3fc162ead6d1..810cecca517f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java @@ -16,6 +16,8 @@ package com.android.systemui.media; +import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; + import android.app.PendingIntent; import android.content.Context; import android.content.Intent; @@ -45,6 +47,7 @@ import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import com.android.systemui.util.animation.TransitionLayout; import java.util.List; @@ -52,6 +55,8 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +import dagger.Lazy; + /** * A view controller used for Media Playback. */ @@ -59,6 +64,8 @@ public class MediaControlPanel { private static final String TAG = "MediaControlPanel"; private static final float DISABLED_ALPHA = 0.38f; + private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); + // Button IDs for QS controls static final int[] ACTION_IDS = { R.id.action0, @@ -75,9 +82,12 @@ public class MediaControlPanel { private Context mContext; private PlayerViewHolder mViewHolder; + private String mKey; private MediaViewController mMediaViewController; private MediaSession.Token mToken; private MediaController mController; + private KeyguardDismissUtil mKeyguardDismissUtil; + private Lazy<MediaDataManager> mMediaDataManagerLazy; private int mBackgroundColor; private int mAlbumArtSize; private int mAlbumArtRadius; @@ -93,12 +103,15 @@ public class MediaControlPanel { @Inject public MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, - SeekBarViewModel seekBarViewModel) { + SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, + KeyguardDismissUtil keyguardDismissUtil) { mContext = context; mBackgroundExecutor = backgroundExecutor; mActivityStarter = activityStarter; mSeekBarViewModel = seekBarViewModel; mMediaViewController = mediaViewController; + mMediaDataManagerLazy = lazyMediaDataManager; + mKeyguardDismissUtil = keyguardDismissUtil; loadDimens(); mViewOutlineProvider = new ViewOutlineProvider() { @@ -174,15 +187,31 @@ public class MediaControlPanel { mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); mMediaViewController.attach(player); + + mViewHolder.getPlayer().setOnLongClickListener(v -> { + if (!mMediaViewController.isGutsVisible()) { + mMediaViewController.openGuts(); + return true; + } else { + return false; + } + }); + mViewHolder.getCancel().setOnClickListener(v -> { + closeGuts(); + }); + mViewHolder.getSettings().setOnClickListener(v -> { + mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */); + }); } /** * Bind this view based on the data given */ - public void bind(@NonNull MediaData data) { + public void bind(@NonNull MediaData data, String key) { if (mViewHolder == null) { return; } + mKey = key; MediaSession.Token token = data.getToken(); mBackgroundColor = data.getBackgroundColor(); if (mToken == null || !mToken.equals(token)) { @@ -205,6 +234,7 @@ public class MediaControlPanel { PendingIntent clickIntent = data.getClickIntent(); if (clickIntent != null) { mViewHolder.getPlayer().setOnClickListener(v -> { + if (mMediaViewController.isGutsVisible()) return; mActivityStarter.postStartActivityDismissingKeyguard(clickIntent); }); } @@ -329,14 +359,38 @@ public class MediaControlPanel { final MediaController controller = getController(); mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); - // Set up long press menu - // TODO: b/156036025 bring back media guts + // Dismiss + mViewHolder.getDismiss().setOnClickListener(v -> { + if (mKey != null) { + closeGuts(); + mKeyguardDismissUtil.executeWhenUnlocked(() -> { + mMediaDataManagerLazy.get().dismissMediaData(mKey, + MediaViewController.GUTS_ANIMATION_DURATION + 100); + return true; + }, /* requiresShadeOpen */ true); + } else { + Log.w(TAG, "Dismiss media with null notification. Token uid=" + + data.getToken().getUid()); + } + }); // TODO: We don't need to refresh this state constantly, only if the state actually changed // to something which might impact the measurement mMediaViewController.refreshState(); } + /** + * Close the guts for this player. + * @param immediate {@code true} if it should be closed without animation + */ + public void closeGuts(boolean immediate) { + mMediaViewController.closeGuts(immediate); + } + + private void closeGuts() { + closeGuts(false); + } + @UiThread private Drawable scaleDrawable(Icon icon) { if (icon == null) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index dafc52ad8025..40a879abde34 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -82,6 +82,10 @@ data class MediaData( */ var resumeAction: Runnable?, /** + * Local or remote playback + */ + var isLocalSession: Boolean = true, + /** * Indicates that this player is a resumption player (ie. It only shows a play actions which * will start the app and start playing). */ @@ -90,7 +94,17 @@ data class MediaData( * Notification key for cancelling a media player after a timeout (when not using resumption.) */ val notificationKey: String? = null, - var hasCheckedForResume: Boolean = false + var hasCheckedForResume: Boolean = false, + + /** + * If apps do not report PlaybackState, set as null to imply 'undetermined' + */ + val isPlaying: Boolean? = null, + + /** + * Set from the notification and used as fallback when PlaybackState cannot be determined + */ + val isClearable: Boolean = true ) /** State of a media action. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt index e8f0e069c98d..aa3699e9a22b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt @@ -17,57 +17,48 @@ package com.android.systemui.media import javax.inject.Inject -import javax.inject.Singleton /** - * Combines updates from [MediaDataManager] with [MediaDeviceManager]. + * Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events. */ -@Singleton -class MediaDataCombineLatest @Inject constructor( - private val dataSource: MediaDataManager, - private val deviceSource: MediaDeviceManager -) { +class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener, + MediaDeviceManager.Listener { + private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf() - init { - dataSource.addListener(object : MediaDataManager.Listener { - override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { - if (oldKey != null && !oldKey.equals(key)) { - val s = entries[oldKey]?.second - entries[key] = data to entries[oldKey]?.second - entries.remove(oldKey) - } else { - entries[key] = data to entries[key]?.second - } - update(key, oldKey) - } - override fun onMediaDataRemoved(key: String) { - remove(key) - } - }) - deviceSource.addListener(object : MediaDeviceManager.Listener { - override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) { - entries[key] = entries[key]?.first to data - update(key, key) - } - override fun onKeyRemoved(key: String) { - remove(key) - } - }) + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + if (oldKey != null && oldKey != key && entries.contains(oldKey)) { + entries[key] = data to entries.remove(oldKey)?.second + update(key, oldKey) + } else { + entries[key] = data to entries[key]?.second + update(key, key) + } } - /** - * Get a map of all non-null data entries - */ - fun getData(): Map<String, MediaData> { - return entries.filter { - (key, pair) -> pair.first != null && pair.second != null - }.mapValues { - (key, pair) -> pair.first!!.copy(device = pair.second) + override fun onMediaDataRemoved(key: String) { + remove(key) + } + + override fun onMediaDeviceChanged( + key: String, + oldKey: String?, + data: MediaDeviceData? + ) { + if (oldKey != null && oldKey != key && entries.contains(oldKey)) { + entries[key] = entries.remove(oldKey)?.first to data + update(key, oldKey) + } else { + entries[key] = entries[key]?.first to data + update(key, key) } } + override fun onKeyRemoved(key: String) { + remove(key) + } + /** * Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData]. */ diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt index 662831e4a445..1f580a953d09 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt @@ -24,32 +24,32 @@ import com.android.systemui.settings.CurrentUserTracker import com.android.systemui.statusbar.NotificationLockscreenUserManager import java.util.concurrent.Executor import javax.inject.Inject -import javax.inject.Singleton private const val TAG = "MediaDataFilter" +private const val DEBUG = true /** * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user * switches (removing entries for the previous user, adding back entries for the current user) * - * This is added downstream of [MediaDataManager] since we may still need to handle callbacks from - * background users (e.g. timeouts) that UI classes should ignore. - * Instead, UI classes should listen to this so they can stay in sync with the current user. + * This is added at the end of the pipeline since we may still need to handle callbacks from + * background users (e.g. timeouts). */ -@Singleton class MediaDataFilter @Inject constructor( - private val dataSource: MediaDataCombineLatest, private val broadcastDispatcher: BroadcastDispatcher, private val mediaResumeListener: MediaResumeListener, - private val mediaDataManager: MediaDataManager, private val lockscreenUserManager: NotificationLockscreenUserManager, @Main private val executor: Executor ) : MediaDataManager.Listener { private val userTracker: CurrentUserTracker - private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + internal val listeners: Set<MediaDataManager.Listener> + get() = _listeners.toSet() + internal lateinit var mediaDataManager: MediaDataManager - // The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager - private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager + private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() init { userTracker = object : CurrentUserTracker(broadcastDispatcher) { @@ -59,31 +59,34 @@ class MediaDataFilter @Inject constructor( } } userTracker.startTracking() - dataSource.addListener(this) } override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + if (oldKey != null && oldKey != key) { + allEntries.remove(oldKey) + } + allEntries.put(key, data) + if (!lockscreenUserManager.isCurrentProfile(data.userId)) { return } - if (oldKey != null) { - mediaEntries.remove(oldKey) + if (oldKey != null && oldKey != key) { + userEntries.remove(oldKey) } - mediaEntries.put(key, data) + userEntries.put(key, data) // Notify listeners - val listenersCopy = listeners.toSet() - listenersCopy.forEach { + listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } } override fun onMediaDataRemoved(key: String) { - mediaEntries.remove(key)?.let { + allEntries.remove(key) + userEntries.remove(key)?.let { // Only notify listeners if something actually changed - val listenersCopy = listeners.toSet() - listenersCopy.forEach { + listeners.forEach { it.onMediaDataRemoved(key) } } @@ -92,22 +95,22 @@ class MediaDataFilter @Inject constructor( @VisibleForTesting internal fun handleUserSwitched(id: Int) { // If the user changes, remove all current MediaData objects and inform listeners - val listenersCopy = listeners.toSet() - val keyCopy = mediaEntries.keys.toMutableList() + val listenersCopy = listeners + val keyCopy = userEntries.keys.toMutableList() // Clear the list first, to make sure callbacks from listeners if we have any entries // are up to date - mediaEntries.clear() + userEntries.clear() keyCopy.forEach { - Log.d(TAG, "Removing $it after user change") + if (DEBUG) Log.d(TAG, "Removing $it after user change") listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } } - dataSource.getData().forEach { (key, data) -> + allEntries.forEach { (key, data) -> if (lockscreenUserManager.isCurrentProfile(data.userId)) { - Log.d(TAG, "Re-adding $key after user change") - mediaEntries.put(key, data) + if (DEBUG) Log.d(TAG, "Re-adding $key after user change") + userEntries.put(key, data) listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } @@ -119,7 +122,8 @@ class MediaDataFilter @Inject constructor( * Invoked when the user has dismissed the media carousel */ fun onSwipeToDismiss() { - val mediaKeys = mediaEntries.keys.toSet() + if (DEBUG) Log.d(TAG, "Media carousel swiped away") + val mediaKeys = userEntries.keys.toSet() mediaKeys.forEach { mediaDataManager.setTimedOut(it, timedOut = true) } @@ -128,26 +132,20 @@ class MediaDataFilter @Inject constructor( /** * Are there any media notifications active? */ - fun hasActiveMedia() = mediaEntries.any { it.value.active } + fun hasActiveMedia() = userEntries.any { it.value.active } /** * Are there any media entries we should display? - * If resumption is enabled, this will include inactive players - * If resumption is disabled, we only want to show active players */ - fun hasAnyMedia() = if (mediaResumeListener.isResumptionEnabled()) { - mediaEntries.isNotEmpty() - } else { - hasActiveMedia() - } + fun hasAnyMedia() = userEntries.isNotEmpty() /** * Add a listener for filtered [MediaData] changes */ - fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) + fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) /** * Remove a listener that was registered with addListener */ - fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) -}
\ No newline at end of file + fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index 08b700bc308d..436d4510aa67 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -31,6 +31,7 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.media.MediaDescription import android.media.MediaMetadata +import android.media.session.MediaController import android.media.session.MediaSession import android.net.Uri import android.os.UserHandle @@ -44,10 +45,12 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState import com.android.systemui.statusbar.notification.MediaNotificationProcessor import com.android.systemui.statusbar.notification.row.HybridGroupManager import com.android.systemui.util.Assert import com.android.systemui.util.Utils +import com.android.systemui.util.concurrency.DelayableExecutor import java.io.FileDescriptor import java.io.IOException import java.io.PrintWriter @@ -63,6 +66,7 @@ private val ART_URIS = arrayOf( ) private const val TAG = "MediaDataManager" +private const val DEBUG = true private const val DEFAULT_LUMINOSITY = 0.25f private const val LUMINOSITY_THRESHOLD = 0.05f private const val SATURATION_MULTIPLIER = 0.8f @@ -89,31 +93,47 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean { class MediaDataManager( private val context: Context, @Background private val backgroundExecutor: Executor, - @Main private val foregroundExecutor: Executor, + @Main private val foregroundExecutor: DelayableExecutor, private val mediaControllerFactory: MediaControllerFactory, private val broadcastDispatcher: BroadcastDispatcher, dumpManager: DumpManager, mediaTimeoutListener: MediaTimeoutListener, mediaResumeListener: MediaResumeListener, + mediaSessionBasedFilter: MediaSessionBasedFilter, + mediaDeviceManager: MediaDeviceManager, + mediaDataCombineLatest: MediaDataCombineLatest, + private val mediaDataFilter: MediaDataFilter, private var useMediaResumption: Boolean, private val useQsMediaPlayer: Boolean ) : Dumpable { - private val listeners: MutableSet<Listener> = mutableSetOf() + // Internal listeners are part of the internal pipeline. External listeners (those registered + // with [MediaDeviceManager.addListener]) receive events after they have propagated through + // the internal pipeline. + // Another way to think of the distinction between internal and external listeners is the + // following. Internal listeners are listeners that MediaDataManager depends on, and external + // listeners are listeners that depend on MediaDataManager. + // TODO(b/159539991#comment5): Move internal listeners to separate package. + private val internalListeners: MutableSet<Listener> = mutableSetOf() private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() @Inject constructor( context: Context, @Background backgroundExecutor: Executor, - @Main foregroundExecutor: Executor, + @Main foregroundExecutor: DelayableExecutor, mediaControllerFactory: MediaControllerFactory, dumpManager: DumpManager, broadcastDispatcher: BroadcastDispatcher, mediaTimeoutListener: MediaTimeoutListener, - mediaResumeListener: MediaResumeListener + mediaResumeListener: MediaResumeListener, + mediaSessionBasedFilter: MediaSessionBasedFilter, + mediaDeviceManager: MediaDeviceManager, + mediaDataCombineLatest: MediaDataCombineLatest, + mediaDataFilter: MediaDataFilter ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory, broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, + mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context)) private val appChangeReceiver = object : BroadcastReceiver() { @@ -136,12 +156,26 @@ class MediaDataManager( init { dumpManager.registerDumpable(TAG, this) + + // Initialize the internal processing pipeline. The listeners at the front of the pipeline + // are set as internal listeners so that they receive events. From there, events are + // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, + // so it is responsible for dispatching events to external listeners. To achieve this, + // external listeners that are registered with [MediaDataManager.addListener] are actually + // registered as listeners to mediaDataFilter. + addInternalListener(mediaTimeoutListener) + addInternalListener(mediaResumeListener) + addInternalListener(mediaSessionBasedFilter) + mediaSessionBasedFilter.addListener(mediaDeviceManager) + mediaSessionBasedFilter.addListener(mediaDataCombineLatest) + mediaDeviceManager.addListener(mediaDataCombineLatest) + mediaDataCombineLatest.addListener(mediaDataFilter) + + // Set up links back into the pipeline for listeners that need to send events upstream. mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean -> setTimedOut(token, timedOut) } - addListener(mediaTimeoutListener) - mediaResumeListener.setManager(this) - addListener(mediaResumeListener) + mediaDataFilter.mediaDataManager = this val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) @@ -179,13 +213,9 @@ class MediaDataManager( private fun removeAllForPackage(packageName: String) { Assert.isMainThread() - val listenersCopy = listeners.toSet() val toRemove = mediaEntries.filter { it.value.packageName == packageName } toRemove.forEach { - mediaEntries.remove(it.key) - listenersCopy.forEach { listener -> - listener.onMediaDataRemoved(it.key) - } + removeEntry(it.key) } } @@ -245,15 +275,48 @@ class MediaDataManager( /** * Add a listener for changes in this class */ - fun addListener(listener: Listener) = listeners.add(listener) + fun addListener(listener: Listener) { + // mediaDataFilter is the current end of the internal pipeline. Register external + // listeners as listeners to it. + mediaDataFilter.addListener(listener) + } /** * Remove a listener for changes in this class */ - fun removeListener(listener: Listener) = listeners.remove(listener) + fun removeListener(listener: Listener) { + // Since mediaDataFilter is the current end of the internal pipelie, external listeners + // have been registered to it. So, they need to be removed from it too. + mediaDataFilter.removeListener(listener) + } + + /** + * Add a listener for internal events. + */ + private fun addInternalListener(listener: Listener) = internalListeners.add(listener) + + /** + * Notify internal listeners of loaded event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } + } + + /** + * Notify internal listeners of removed event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifyMediaDataRemoved(key: String) { + internalListeners.forEach { it.onMediaDataRemoved(key) } + } /** - * Called whenever the player has been paused or stopped for a while. + * Called whenever the player has been paused or stopped for a while, or swiped from QQS. * This will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ @@ -263,10 +326,30 @@ class MediaDataManager( return } it.active = !timedOut + if (DEBUG) Log.d(TAG, "Updating $token timedOut: $timedOut") onMediaDataLoaded(token, token, it) } } + private fun removeEntry(key: String) { + mediaEntries.remove(key) + notifyMediaDataRemoved(key) + } + + fun dismissMediaData(key: String, delay: Long) { + backgroundExecutor.execute { + mediaEntries[key]?.let { mediaData -> + if (mediaData.isLocalSession) { + mediaData.token?.let { + val mediaController = mediaControllerFactory.create(it) + mediaController.transportControls.stop() + } + } + } + } + foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) + } + private fun loadMediaDataInBgForResumption( userId: Int, desc: MediaDescription, @@ -283,7 +366,9 @@ class MediaDataManager( return } - Log.d(TAG, "adding track for $userId from browser: $desc") + if (DEBUG) { + Log.d(TAG, "adding track for $userId from browser: $desc") + } // Album art var artworkBitmap = desc.iconBitmap @@ -314,20 +399,16 @@ class MediaDataManager( ) { val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION) as MediaSession.Token? - val metadata = mediaControllerFactory.create(token).metadata - - if (metadata == null) { - // TODO: handle this better, removing media notification - return - } + val mediaController = mediaControllerFactory.create(token) + val metadata = mediaController.metadata // Foreground and Background colors computed from album art val notif: Notification = sbn.notification - var artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART) + var artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) if (artworkBitmap == null) { - artworkBitmap = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) } - if (artworkBitmap == null) { + if (artworkBitmap == null && metadata != null) { artworkBitmap = loadBitmapFromUri(metadata) } val artWorkIcon = if (artworkBitmap == null) { @@ -364,16 +445,16 @@ class MediaDataManager( sbn.user.identifier) // Song name - var song: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) if (song == null) { - song = metadata.getString(MediaMetadata.METADATA_KEY_TITLE) + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) } if (song == null) { song = HybridGroupManager.resolveTitle(notif) } // Artist name - var artist: CharSequence? = metadata.getString(MediaMetadata.METADATA_KEY_ARTIST) + var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) if (artist == null) { artist = HybridGroupManager.resolveText(notif) } @@ -389,7 +470,7 @@ class MediaDataManager( if (actions != null) { for ((index, action) in actions.withIndex()) { if (action.getIcon() == null) { - Log.i(TAG, "No icon for action $index ${action.title}") + if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") actionsToShowCollapsed.remove(index) continue } @@ -412,6 +493,10 @@ class MediaDataManager( } } + val isLocalSession = mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL ?: true + val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null + foregroundExecutor.execute { val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true @@ -419,8 +504,9 @@ class MediaDataManager( onMediaDataLoaded(key, oldKey, MediaData(sbn.normalizedUserId, true, bgColor, app, smallIconDrawable, artist, song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, notif.contentIntent, null, - active, resumeAction = resumeAction, notificationKey = key, - hasCheckedForResume = hasCheckedForResume)) + active, resumeAction = resumeAction, isLocalSession = isLocalSession, + notificationKey = key, hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, isClearable = sbn.isClearable())) } } @@ -433,7 +519,7 @@ class MediaDataManager( if (!TextUtils.isEmpty(uriString)) { val albumArt = loadBitmapFromUri(Uri.parse(uriString)) if (albumArt != null) { - Log.d(TAG, "loaded art from $uri") + if (DEBUG) Log.d(TAG, "loaded art from $uri") return albumArt } } @@ -464,7 +550,10 @@ class MediaDataManager( decoder, info, source -> decoder.isMutableRequired = true } } catch (e: IOException) { - e.printStackTrace() + Log.e(TAG, "Unable to load bitmap", e) + null + } catch (e: RuntimeException) { + Log.e(TAG, "Unable to load bitmap", e) null } } @@ -509,10 +598,7 @@ class MediaDataManager( if (mediaEntries.containsKey(key)) { // Otherwise this was removed already mediaEntries.put(key, data) - val listenersCopy = listeners.toSet() - listenersCopy.forEach { - it.onMediaDataLoaded(key, oldKey, data) - } + notifyMediaDataLoaded(key, oldKey, data) } } @@ -520,7 +606,7 @@ class MediaDataManager( Assert.isMainThread() val removed = mediaEntries.remove(key) if (useMediaResumption && removed?.resumeAction != null) { - Log.d(TAG, "Not removing $key because resumable") + if (DEBUG) Log.d(TAG, "Not removing $key because resumable") // Move to resume key (aka package name) if that key doesn't already exist. val resumeAction = getResumeMediaAction(removed.resumeAction!!) val updated = removed.copy(token = null, actions = listOf(resumeAction), @@ -528,31 +614,21 @@ class MediaDataManager( val pkg = removed?.packageName val migrate = mediaEntries.put(pkg, updated) == null // Notify listeners of "new" controls when migrating or removed and update when not - val listenersCopy = listeners.toSet() if (migrate) { - listenersCopy.forEach { - it.onMediaDataLoaded(pkg, key, updated) - } + notifyMediaDataLoaded(pkg, key, updated) } else { // Since packageName is used for the key of the resumption controls, it is // possible that another notification has already been reused for the resumption // controls of this package. In this case, rather than renaming this player as // packageName, just remove it and then send a update to the existing resumption // controls. - listenersCopy.forEach { - it.onMediaDataRemoved(key) - } - listenersCopy.forEach { - it.onMediaDataLoaded(pkg, pkg, updated) - } + notifyMediaDataRemoved(key) + notifyMediaDataLoaded(pkg, pkg, updated) } return } if (removed != null) { - val listenersCopy = listeners.toSet() - listenersCopy.forEach { - it.onMediaDataRemoved(key) - } + notifyMediaDataRemoved(key) } } @@ -565,17 +641,31 @@ class MediaDataManager( if (!useMediaResumption) { // Remove any existing resume controls - val listenersCopy = listeners.toSet() val filtered = mediaEntries.filter { !it.value.active } filtered.forEach { mediaEntries.remove(it.key) - listenersCopy.forEach { listener -> - listener.onMediaDataRemoved(it.key) - } + notifyMediaDataRemoved(it.key) } } } + /** + * Invoked when the user has dismissed the media carousel + */ + fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() + + /** + * Are there any media notifications active? + */ + fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() + + /** + * Are there any media entries we should display? + * If resumption is enabled, this will include inactive players + * If resumption is disabled, we only want to show active players + */ + fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() + interface Listener { /** @@ -595,7 +685,8 @@ class MediaDataManager( override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { pw.apply { - println("listeners: $listeners") + println("internalListeners: $internalListeners") + println("externalListeners: ${mediaDataFilter.listeners}") println("mediaEntries: $mediaEntries") println("useMediaResumption: $useMediaResumption") } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt index 7ae2dc5c0941..a993d00df01e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt @@ -16,37 +16,40 @@ package com.android.systemui.media -import android.content.Context import android.media.MediaRouter2Manager import android.media.session.MediaController +import androidx.annotation.AnyThread +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.Dumpable +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import java.io.FileDescriptor import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject -import javax.inject.Singleton + +private const val PLAYBACK_TYPE_UNKNOWN = 0 /** * Provides information about the route (ie. device) where playback is occurring. */ -@Singleton class MediaDeviceManager @Inject constructor( - private val context: Context, + private val controllerFactory: MediaControllerFactory, private val localMediaManagerFactory: LocalMediaManagerFactory, private val mr2manager: MediaRouter2Manager, @Main private val fgExecutor: Executor, - private val mediaDataManager: MediaDataManager, - private val dumpManager: DumpManager + @Background private val bgExecutor: Executor, + dumpManager: DumpManager ) : MediaDataManager.Listener, Dumpable { + private val listeners: MutableSet<Listener> = mutableSetOf() - private val entries: MutableMap<String, Token> = mutableMapOf() + private val entries: MutableMap<String, Entry> = mutableMapOf() init { - mediaDataManager.addListener(this) dumpManager.registerDumpable(javaClass.name, this) } @@ -69,9 +72,10 @@ class MediaDeviceManager @Inject constructor( if (entry == null || entry?.token != data.token) { entry?.stop() val controller = data.token?.let { - MediaController(context, it) + controllerFactory.create(it) } - entry = Token(key, controller, localMediaManagerFactory.create(data.packageName)) + entry = Entry(key, oldKey, controller, + localMediaManagerFactory.create(data.packageName)) entries[key] = entry entry.start() } @@ -98,66 +102,98 @@ class MediaDeviceManager @Inject constructor( } } - private fun processDevice(key: String, device: MediaDevice?) { + @MainThread + private fun processDevice(key: String, oldKey: String?, device: MediaDevice?) { val enabled = device != null val data = MediaDeviceData(enabled, device?.iconWithoutBackground, device?.name) listeners.forEach { - it.onMediaDeviceChanged(key, data) + it.onMediaDeviceChanged(key, oldKey, data) } } interface Listener { /** Called when the route has changed for a given notification. */ - fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) + fun onMediaDeviceChanged(key: String, oldKey: String?, data: MediaDeviceData?) /** Called when the notification was removed. */ fun onKeyRemoved(key: String) } - private inner class Token( + private inner class Entry( val key: String, + val oldKey: String?, val controller: MediaController?, val localMediaManager: LocalMediaManager - ) : LocalMediaManager.DeviceCallback { + ) : LocalMediaManager.DeviceCallback, MediaController.Callback() { + val token get() = controller?.sessionToken private var started = false + private var playbackType = PLAYBACK_TYPE_UNKNOWN private var current: MediaDevice? = null set(value) { if (!started || value != field) { field = value - processDevice(key, value) + fgExecutor.execute { + processDevice(key, oldKey, value) + } } } - fun start() { + + @AnyThread + fun start() = bgExecutor.execute { localMediaManager.registerCallback(this) localMediaManager.startScan() + playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN + controller?.registerCallback(this) updateCurrent() started = true } - fun stop() { + + @AnyThread + fun stop() = bgExecutor.execute { started = false + controller?.unregisterCallback(this) localMediaManager.stopScan() localMediaManager.unregisterCallback(this) } + fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<String>) { - val route = controller?.let { + val routingSession = controller?.let { mr2manager.getRoutingSessionForMediaController(it) } + val selectedRoutes = routingSession?.let { + mr2manager.getSelectedRoutes(it) + } with(pw) { println(" current device is ${current?.name}") val type = controller?.playbackInfo?.playbackType - println(" PlaybackType=$type (1 for local, 2 for remote)") - println(" route=$route") + println(" PlaybackType=$type (1 for local, 2 for remote) cached=$playbackType") + println(" routingSession=$routingSession") + println(" selectedRoutes=$selectedRoutes") + } + } + + @WorkerThread + override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) { + val newPlaybackType = info?.playbackType ?: PLAYBACK_TYPE_UNKNOWN + if (newPlaybackType == playbackType) { + return } + playbackType = newPlaybackType + updateCurrent() } - override fun onDeviceListUpdate(devices: List<MediaDevice>?) = fgExecutor.execute { + + override fun onDeviceListUpdate(devices: List<MediaDevice>?) = bgExecutor.execute { updateCurrent() } + override fun onSelectedDeviceStateChanged(device: MediaDevice, state: Int) { - fgExecutor.execute { + bgExecutor.execute { updateCurrent() } } + + @WorkerThread private fun updateCurrent() { val device = localMediaManager.getCurrentConnectedDevice() controller?.let { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index fc33391a9ad1..b31390cf7474 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -293,6 +293,13 @@ class MediaHierarchyManager @Inject constructor( return viewHost } + /** + * Close the guts in all players in [MediaCarouselController]. + */ + fun closeGuts() { + mediaCarouselController.closeGuts() + } + private fun createUniqueObjectHost(): UniqueObjectHostView { val viewHost = UniqueObjectHostView(context) viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { @@ -382,6 +389,14 @@ class MediaHierarchyManager @Inject constructor( if (isCurrentlyInGuidedTransformation()) { return false } + // This is an invalid transition, and can happen when using the camera gesture from the + // lock screen. Disallow. + if (previousLocation == LOCATION_LOCKSCREEN && + desiredLocation == LOCATION_QQS && + statusbarState == StatusBarState.SHADE) { + return false + } + if (currentLocation == LOCATION_QQS && previousLocation == LOCATION_LOCKSCREEN && (statusBarStateController.leaveOpenOnKeyguardHide() || @@ -597,8 +612,8 @@ class MediaHierarchyManager @Inject constructor( // When collapsing on the lockscreen, we want to remain in QS return LOCATION_QS } - if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN - && !fullyAwake) { + if (location != LOCATION_LOCKSCREEN && desiredLocation == LOCATION_LOCKSCREEN && + !fullyAwake) { // When unlocking from dozing / while waking up, the media shouldn't be transitioning // in an animated way. Let's keep it in the lockscreen until we're fully awake and // reattach it without an animation diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 3598719fcb3a..ce184aa23a57 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -14,7 +14,7 @@ import javax.inject.Inject class MediaHost @Inject constructor( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, - private val mediaDataFilter: MediaDataFilter, + private val mediaDataManager: MediaDataManager, private val mediaHostStatesManager: MediaHostStatesManager ) : MediaHostState by state { lateinit var hostView: UniqueObjectHostView @@ -79,12 +79,12 @@ class MediaHost @Inject constructor( // be a delay until the views and the controllers are initialized, leaving us // with either a blank view or the controllers not yet initialized and the // measuring wrong - mediaDataFilter.addListener(listener) + mediaDataManager.addListener(listener) updateViewVisibility() } override fun onViewDetachedFromWindow(v: View?) { - mediaDataFilter.removeListener(listener) + mediaDataManager.removeListener(listener) } }) @@ -113,9 +113,9 @@ class MediaHost @Inject constructor( private fun updateViewVisibility() { visible = if (showsOnlyActiveMedia) { - mediaDataFilter.hasActiveMedia() + mediaDataManager.hasActiveMedia() } else { - mediaDataFilter.hasAnyMedia() + mediaDataManager.hasAnyMedia() } val newVisibility = if (visible) View.VISIBLE else View.GONE if (newVisibility != hostView.visibility) { @@ -289,4 +289,4 @@ interface MediaHostState { * Get a copy of this view state, deepcopying all appropriate members */ fun copy(): MediaHostState -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index 4ec746fcb153..936db8735ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -28,6 +28,7 @@ import android.os.UserHandle import android.provider.Settings import android.service.media.MediaBrowserService import android.util.Log +import com.android.internal.annotations.VisibleForTesting import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.tuner.TunerService @@ -47,7 +48,8 @@ class MediaResumeListener @Inject constructor( private val context: Context, private val broadcastDispatcher: BroadcastDispatcher, @Background private val backgroundExecutor: Executor, - private val tunerService: TunerService + private val tunerService: TunerService, + private val mediaBrowserFactory: ResumeMediaBrowserFactory ) : MediaDataManager.Listener { private var useMediaResumption: Boolean = Utils.useMediaResumption(context) @@ -58,7 +60,8 @@ class MediaResumeListener @Inject constructor( private var mediaBrowser: ResumeMediaBrowser? = null private var currentUserId: Int = context.userId - private val userChangeReceiver = object : BroadcastReceiver() { + @VisibleForTesting + val userChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (Intent.ACTION_USER_UNLOCKED == intent.action) { loadMediaResumptionControls() @@ -116,8 +119,6 @@ class MediaResumeListener @Inject constructor( }, Settings.Secure.MEDIA_CONTROLS_RESUME) } - fun isResumptionEnabled() = useMediaResumption - private fun loadSavedComponents() { // Make sure list is empty (if we switched users) resumeComponents.clear() @@ -144,7 +145,7 @@ class MediaResumeListener @Inject constructor( } resumeComponents.forEach { - val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it) + val browser = mediaBrowserFactory.create(mediaBrowserCallback, it) browser.findRecentMedia() } } @@ -183,14 +184,10 @@ class MediaResumeListener @Inject constructor( private fun tryUpdateResumptionList(key: String, componentName: ComponentName) { Log.d(TAG, "Testing if we can connect to $componentName") mediaBrowser?.disconnect() - mediaBrowser = ResumeMediaBrowser(context, + mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { - Log.d(TAG, "yes we can resume with $componentName") - mediaDataManager.setResumeAction(key, getResumeAction(componentName)) - updateResumptionList(componentName) - mediaBrowser?.disconnect() - mediaBrowser = null + Log.d(TAG, "Connected to $componentName") } override fun onError() { @@ -199,6 +196,19 @@ class MediaResumeListener @Inject constructor( mediaBrowser?.disconnect() mediaBrowser = null } + + override fun addTrack( + desc: MediaDescription, + component: ComponentName, + browser: ResumeMediaBrowser + ) { + // Since this is a test, just save the component for later + Log.d(TAG, "Can get resumable media from $componentName") + mediaDataManager.setResumeAction(key, getResumeAction(componentName)) + updateResumptionList(componentName) + mediaBrowser?.disconnect() + mediaBrowser = null + } }, componentName) mediaBrowser?.testConnection() @@ -235,7 +245,7 @@ class MediaResumeListener @Inject constructor( private fun getResumeAction(componentName: ComponentName): Runnable { return Runnable { mediaBrowser?.disconnect() - mediaBrowser = ResumeMediaBrowser(context, + mediaBrowser = mediaBrowserFactory.create( object : ResumeMediaBrowser.Callback() { override fun onConnected() { if (mediaBrowser?.token == null) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt new file mode 100644 index 000000000000..f695622b943a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 + +import android.content.ComponentName +import android.content.Context +import android.media.session.MediaController +import android.media.session.MediaController.PlaybackInfo +import android.media.session.MediaSession +import android.media.session.MediaSessionManager +import android.util.Log +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins +import java.util.concurrent.Executor +import javax.inject.Inject + +private const val TAG = "MediaSessionBasedFilter" + +/** + * Filters media loaded events for local media sessions while an app is casting. + * + * When an app is casting there can be one remote media sessions and potentially more local media + * sessions. In this situation, there should only be a media object for the remote session. To + * achieve this, update events for the local session need to be filtered. + */ +class MediaSessionBasedFilter @Inject constructor( + context: Context, + private val sessionManager: MediaSessionManager, + @Main private val foregroundExecutor: Executor, + @Background private val backgroundExecutor: Executor +) : MediaDataManager.Listener { + + private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + + // Keep track of MediaControllers for a given package to check if an app is casting and it + // filter loaded events for local sessions. + private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> = + LinkedHashMap() + + // Keep track of the key used for the session tokens. This information is used to know when to + // dispatch a removed event so that a media object for a local session will be removed. + private val keyedTokens: MutableMap<String, MutableSet<MediaSession.Token>> = mutableMapOf() + + // Keep track of which media session tokens have associated notifications. + private val tokensWithNotifications: MutableSet<MediaSession.Token> = mutableSetOf() + + private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener { + override fun onActiveSessionsChanged(controllers: List<MediaController>) { + handleControllersChanged(controllers) + } + } + + init { + backgroundExecutor.execute { + val name = ComponentName(context, NotificationListenerWithPlugins::class.java) + sessionManager.addOnActiveSessionsChangedListener(sessionListener, name) + handleControllersChanged(sessionManager.getActiveSessions(name)) + } + } + + /** + * Add a listener for filtered [MediaData] changes + */ + fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) + + /** + * Remove a listener that was registered with addListener + */ + fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener) + + /** + * May filter loaded events by not passing them along to listeners. + * + * If an app has only one session with playback type PLAYBACK_TYPE_REMOTE, then assuming that + * the app is casting. Sometimes apps will send redundant updates to a local session with + * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability + * of the media controls. + */ + override fun onMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + backgroundExecutor.execute { + info.token?.let { + tokensWithNotifications.add(it) + } + val isMigration = oldKey != null && key != oldKey + if (isMigration) { + keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) } + } + if (info.token != null) { + keyedTokens.get(key)?.let { + tokens -> + tokens.add(info.token) + } ?: run { + val tokens = mutableSetOf(info.token) + keyedTokens.put(key, tokens) + } + } + // Determine if an app is casting by checking if it has a session with playback type + // PLAYBACK_TYPE_REMOTE. + val remoteControllers = packageControllers.get(info.packageName)?.filter { + it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE + } + // Limiting search to only apps with a single remote session. + val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null + if (isMigration || remote == null || remote.sessionToken == info.token || + !tokensWithNotifications.contains(remote.sessionToken)) { + // Not filtering in this case. Passing the event along to listeners. + dispatchMediaDataLoaded(key, oldKey, info) + } else { + // Filtering this event because the app is casting and the loaded events is for a + // local session. + Log.d(TAG, "filtering key=$key local=${info.token} remote=${remote?.sessionToken}") + // If the local session uses a different notification key, then lets go a step + // farther and dismiss the media data so that media controls for the local session + // don't hang around while casting. + if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) { + dispatchMediaDataRemoved(key) + } + } + } + } + + override fun onMediaDataRemoved(key: String) { + // Queue on background thread to ensure ordering of loaded and removed events is maintained. + backgroundExecutor.execute { + keyedTokens.remove(key) + dispatchMediaDataRemoved(key) + } + } + + private fun dispatchMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + foregroundExecutor.execute { + listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info) } + } + } + + private fun dispatchMediaDataRemoved(key: String) { + foregroundExecutor.execute { + listeners.toSet().forEach { it.onMediaDataRemoved(key) } + } + } + + private fun handleControllersChanged(controllers: List<MediaController>) { + packageControllers.clear() + controllers.forEach { + controller -> + packageControllers.get(controller.packageName)?.let { + tokens -> + tokens.add(controller) + } ?: run { + val tokens = mutableListOf(controller) + packageControllers.put(controller.packageName, tokens) + } + } + tokensWithNotifications.retainAll(controllers.map { it.sessionToken }) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 8662aacfdab2..dcb7767a680a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -54,32 +54,35 @@ class MediaTimeoutListener @Inject constructor( if (mediaListeners.containsKey(key)) { return } - // Having an old key means that we're migrating from/to resumption. We should invalidate - // the old listener and create a new one. + // Having an old key means that we're migrating from/to resumption. We should update + // the old listener to make sure that events will be dispatched to the new location. val migrating = oldKey != null && key != oldKey var wasPlaying = false if (migrating) { - if (mediaListeners.containsKey(oldKey)) { - val oldListener = mediaListeners.remove(oldKey) - wasPlaying = oldListener?.playing ?: false - oldListener?.destroy() + val reusedListener = mediaListeners.remove(oldKey) + if (reusedListener != null) { + wasPlaying = reusedListener.playing ?: false if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption") + reusedListener.mediaData = data + reusedListener.key = key + mediaListeners[key] = reusedListener + if (wasPlaying != reusedListener.playing) { + // If a player becomes active because of a migration, we'll need to broadcast + // its state. Doing it now would lead to reentrant callbacks, so let's wait + // until we're done. + mainExecutor.execute { + if (mediaListeners[key]?.playing == true) { + if (DEBUG) Log.d(TAG, "deliver delayed playback state for $key") + timeoutCallback.invoke(key, false /* timedOut */) + } + } + } + return } else { Log.w(TAG, "Old key $oldKey for player $key doesn't exist. Continuing...") } } mediaListeners[key] = PlaybackStateListener(key, data) - - // If a player becomes active because of a migration, we'll need to broadcast its state. - // Doing it now would lead to reentrant callbacks, so let's wait until we're done. - if (migrating && mediaListeners[key]?.playing != wasPlaying) { - mainExecutor.execute { - if (mediaListeners[key]?.playing == true) { - if (DEBUG) Log.d(TAG, "deliver delayed playback state for $key") - timeoutCallback.invoke(key, false /* timedOut */) - } - } - } } override fun onMediaDataRemoved(key: String) { @@ -91,30 +94,39 @@ class MediaTimeoutListener @Inject constructor( } private inner class PlaybackStateListener( - private val key: String, + var key: String, data: MediaData ) : MediaController.Callback() { var timedOut = false var playing: Boolean? = null + var mediaData: MediaData = data + set(value) { + mediaController?.unregisterCallback(this) + field = value + mediaController = if (field.token != null) { + mediaControllerFactory.create(field.token) + } else { + null + } + mediaController?.registerCallback(this) + // Let's register the cancellations, but not dispatch events now. + // Timeouts didn't happen yet and reentrant events are troublesome. + processState(mediaController?.playbackState, dispatchEvents = false) + } + // Resume controls may have null token - private val mediaController = if (data.token != null) { - mediaControllerFactory.create(data.token) - } else { - null - } + private var mediaController: MediaController? = null private var cancellation: Runnable? = null init { - mediaController?.registerCallback(this) - // Let's register the cancellations, but not dispatch events now. - // Timeouts didn't happen yet and reentrant events are troublesome. - processState(mediaController?.playbackState, dispatchEvents = false) + mediaData = data } fun destroy() { mediaController?.unregisterCallback(this) + cancellation?.run() } override fun onPlaybackStateChanged(state: PlaybackState?) { @@ -171,4 +183,4 @@ class MediaTimeoutListener @Inject constructor( cancellation = null } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index 38817d7b579e..92eeed46388d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -37,6 +37,11 @@ class MediaViewController @Inject constructor( private val mediaHostStatesManager: MediaHostStatesManager ) { + companion object { + @JvmField + val GUTS_ANIMATION_DURATION = 500L + } + /** * A listener when the current dimensions of the player change */ @@ -169,6 +174,12 @@ class MediaViewController @Inject constructor( */ val expandedLayout = ConstraintSet() + /** + * Whether the guts are visible for the associated player. + */ + var isGutsVisible = false + private set + init { collapsedLayout.load(context, R.xml.media_collapsed) expandedLayout.load(context, R.xml.media_expanded) @@ -189,6 +200,37 @@ class MediaViewController @Inject constructor( configurationController.removeCallback(configurationListener) } + /** + * Show guts with an animated transition. + */ + fun openGuts() { + if (isGutsVisible) return + isGutsVisible = true + animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) + setCurrentState(currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = false) + } + + /** + * Close the guts for the associated player. + * + * @param immediate if `false`, it will animate the transition. + */ + @JvmOverloads + fun closeGuts(immediate: Boolean = false) { + if (!isGutsVisible) return + isGutsVisible = false + if (!immediate) { + animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L) + } + setCurrentState(currentStartLocation, + currentEndLocation, + currentTransitionProgress, + applyImmediately = immediate) + } + private fun ensureAllMeasurements() { val mediaStates = mediaHostStatesManager.mediaHostStates for (entry in mediaStates) { @@ -203,6 +245,24 @@ class MediaViewController @Inject constructor( if (expansion > 0) expandedLayout else collapsedLayout /** + * Set the views to be showing/hidden based on the [isGutsVisible] for a given + * [TransitionViewState]. + */ + private fun setGutsViewState(viewState: TransitionViewState) { + PlayerViewHolder.controlsIds.forEach { id -> + viewState.widgetStates.get(id)?.let { state -> + // Make sure to use the unmodified state if guts are not visible + state.alpha = if (isGutsVisible) 0f else state.alpha + state.gone = if (isGutsVisible) true else state.gone + } + } + PlayerViewHolder.gutsIds.forEach { id -> + viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f + viewState.widgetStates.get(id)?.gone = !isGutsVisible + } + } + + /** * Obtain a new viewState for a given media state. This usually returns a cached state, but if * it's not available, it will recreate one by measuring, which may be expensive. */ @@ -211,7 +271,7 @@ class MediaViewController @Inject constructor( return null } // Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey - var cacheKey = getKey(state, tmpKey) + var cacheKey = getKey(state, isGutsVisible, tmpKey) val viewState = viewStates[cacheKey] if (viewState != null) { // we already have cached this measurement, let's continue @@ -228,6 +288,7 @@ class MediaViewController @Inject constructor( constraintSetForExpansion(state.expansion), TransitionViewState()) + setGutsViewState(result) // We don't want to cache interpolated or null states as this could quickly fill up // our cache. We only cache the start and the end states since the interpolation // is cheap @@ -252,11 +313,12 @@ class MediaViewController @Inject constructor( return result } - private fun getKey(state: MediaHostState, result: CacheKey): CacheKey { + private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey { result.apply { heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0 widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0 expansion = state.expansion + gutsVisible = guts } return result } @@ -432,5 +494,6 @@ class MediaViewController @Inject constructor( private data class CacheKey( var widthMeasureSpec: Int = -1, var heightMeasureSpec: Int = -1, - var expansion: Float = 0.0f + var expansion: Float = 0.0f, + var gutsVisible: Boolean = false ) diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt index 600fdc27ef89..666a6038a8b6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt @@ -59,6 +59,11 @@ class PlayerViewHolder private constructor(itemView: View) { val action3 = itemView.requireViewById<ImageButton>(R.id.action3) val action4 = itemView.requireViewById<ImageButton>(R.id.action4) + // Settings screen + val cancel = itemView.requireViewById<View>(R.id.cancel) + val dismiss = itemView.requireViewById<View>(R.id.dismiss) + val settings = itemView.requireViewById<View>(R.id.settings) + init { (player.background as IlluminationDrawable).let { it.registerLightSource(seamless) @@ -67,6 +72,9 @@ class PlayerViewHolder private constructor(itemView: View) { it.registerLightSource(action2) it.registerLightSource(action3) it.registerLightSource(action4) + it.registerLightSource(cancel) + it.registerLightSource(dismiss) + it.registerLightSource(settings) } } @@ -83,9 +91,6 @@ class PlayerViewHolder private constructor(itemView: View) { } } - // Settings screen - val options = itemView.requireViewById<View>(R.id.qs_media_controls_options) - companion object { /** * Creates a PlayerViewHolder. @@ -105,5 +110,29 @@ class PlayerViewHolder private constructor(itemView: View) { progressTimes.layoutDirection = View.LAYOUT_DIRECTION_LTR } } + + val controlsIds = setOf( + R.id.icon, + R.id.app_name, + R.id.album_art, + R.id.header_title, + R.id.header_artist, + R.id.media_seamless, + R.id.notification_media_progress_time, + R.id.media_progress_bar, + R.id.action0, + R.id.action1, + R.id.action2, + R.id.action3, + R.id.action4, + R.id.icon + ) + val gutsIds = setOf( + R.id.media_text, + R.id.remove_text, + R.id.cancel, + R.id.dismiss, + R.id.settings + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java index 68b6785849aa..a4d44367be73 100644 --- a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java @@ -30,6 +30,8 @@ import android.service.media.MediaBrowserService; import android.text.TextUtils; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.util.List; /** @@ -46,6 +48,7 @@ public class ResumeMediaBrowser { private static final String TAG = "ResumeMediaBrowser"; private final Context mContext; private final Callback mCallback; + private MediaBrowserFactory mBrowserFactory; private MediaBrowser mMediaBrowser; private ComponentName mComponentName; @@ -55,10 +58,12 @@ public class ResumeMediaBrowser { * @param callback used to report media items found * @param componentName Component name of the MediaBrowserService this browser will connect to */ - public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) { + public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName, + MediaBrowserFactory browserFactory) { mContext = context; mCallback = callback; mComponentName = componentName; + mBrowserFactory = browserFactory; } /** @@ -74,7 +79,7 @@ public class ResumeMediaBrowser { disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, + mMediaBrowser = mBrowserFactory.create( mComponentName, mConnectionCallback, rootHints); @@ -88,17 +93,19 @@ public class ResumeMediaBrowser { List<MediaBrowser.MediaItem> children) { if (children.size() == 0) { Log.d(TAG, "No children found for " + mComponentName); - return; - } - // We ask apps to return a playable item as the first child when sending - // a request with EXTRA_RECENT; if they don't, no resume controls - MediaBrowser.MediaItem child = children.get(0); - MediaDescription desc = child.getDescription(); - if (child.isPlayable() && mMediaBrowser != null) { - mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), - ResumeMediaBrowser.this); + mCallback.onError(); } else { - Log.d(TAG, "Child found but not playable for " + mComponentName); + // We ask apps to return a playable item as the first child when sending + // a request with EXTRA_RECENT; if they don't, no resume controls + MediaBrowser.MediaItem child = children.get(0); + MediaDescription desc = child.getDescription(); + if (child.isPlayable() && mMediaBrowser != null) { + mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), + ResumeMediaBrowser.this); + } else { + Log.d(TAG, "Child found but not playable for " + mComponentName); + mCallback.onError(); + } } disconnect(); } @@ -131,7 +138,7 @@ public class ResumeMediaBrowser { Log.d(TAG, "Service connected for " + mComponentName); if (mMediaBrowser != null && mMediaBrowser.isConnected()) { String root = mMediaBrowser.getRoot(); - if (!TextUtils.isEmpty(root)) { + if (!TextUtils.isEmpty(root) && mMediaBrowser != null) { mCallback.onConnected(); mMediaBrowser.subscribe(root, mSubscriptionCallback); return; @@ -182,7 +189,7 @@ public class ResumeMediaBrowser { disconnect(); Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, mComponentName, + mMediaBrowser = mBrowserFactory.create(mComponentName, new MediaBrowser.ConnectionCallback() { @Override public void onConnected() { @@ -192,7 +199,7 @@ public class ResumeMediaBrowser { return; } MediaSession.Token token = mMediaBrowser.getSessionToken(); - MediaController controller = new MediaController(mContext, token); + MediaController controller = createMediaController(token); controller.getTransportControls(); controller.getTransportControls().prepare(); controller.getTransportControls().play(); @@ -212,6 +219,11 @@ public class ResumeMediaBrowser { mMediaBrowser.connect(); } + @VisibleForTesting + protected MediaController createMediaController(MediaSession.Token token) { + return new MediaController(mContext, token); + } + /** * Get the media session token * @return the token, or null if the MediaBrowser is null or disconnected @@ -235,42 +247,19 @@ public class ResumeMediaBrowser { /** * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser. - * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called - * depending on whether it was successful. + * If it can connect, ResumeMediaBrowser.Callback#onConnected will be called. If valid media is + * found, then ResumeMediaBrowser.Callback#addTrack will also be called. This allows for more + * detailed logging if the service has issues. If it cannot connect, or cannot find valid media, + * then ResumeMediaBrowser.Callback#onError will be called. * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed. */ public void testConnection() { disconnect(); - final MediaBrowser.ConnectionCallback connectionCallback = - new MediaBrowser.ConnectionCallback() { - @Override - public void onConnected() { - Log.d(TAG, "connected"); - if (mMediaBrowser == null || !mMediaBrowser.isConnected() - || TextUtils.isEmpty(mMediaBrowser.getRoot())) { - mCallback.onError(); - } else { - mCallback.onConnected(); - } - } - - @Override - public void onConnectionSuspended() { - Log.d(TAG, "suspended"); - mCallback.onError(); - } - - @Override - public void onConnectionFailed() { - Log.d(TAG, "failed"); - mCallback.onError(); - } - }; Bundle rootHints = new Bundle(); rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true); - mMediaBrowser = new MediaBrowser(mContext, + mMediaBrowser = mBrowserFactory.create( mComponentName, - connectionCallback, + mConnectionCallback, rootHints); mMediaBrowser.connect(); } diff --git a/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java new file mode 100644 index 000000000000..2261aa5ac265 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowserFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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; + +import android.content.ComponentName; +import android.content.Context; + +import javax.inject.Inject; + +/** + * Testable wrapper around {@link ResumeMediaBrowser} constructor + */ +public class ResumeMediaBrowserFactory { + private final Context mContext; + private final MediaBrowserFactory mBrowserFactory; + + @Inject + public ResumeMediaBrowserFactory(Context context, MediaBrowserFactory browserFactory) { + mContext = context; + mBrowserFactory = browserFactory; + } + + /** + * Creates a new ResumeMediaBrowser. + * + * @param callback will be called on connection or error, and addTrack when media item found + * @param componentName component to browse + * @return + */ + public ResumeMediaBrowser create(ResumeMediaBrowser.Callback callback, + ComponentName componentName) { + return new ResumeMediaBrowser(mContext, callback, componentName, mBrowserFactory); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt index c2631c923e45..d789501ffdef 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarObserver.kt @@ -28,10 +28,14 @@ import com.android.systemui.R */ class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarViewModel.Progress> { - val seekBarDefaultMaxHeight = holder.seekBar.context.resources + val seekBarEnabledMaxHeight = holder.seekBar.context.resources .getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_height) val seekBarDisabledHeight = holder.seekBar.context.resources .getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_height) + val seekBarEnabledVerticalPadding = holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_enabled_seekbar_vertical_padding) + val seekBarDisabledVerticalPadding = holder.seekBar.context.resources + .getDimensionPixelSize(R.dimen.qs_media_disabled_seekbar_vertical_padding) /** Updates seek bar views when the data model changes. */ @UiThread @@ -39,6 +43,7 @@ class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarVi if (!data.enabled) { if (holder.seekBar.maxHeight != seekBarDisabledHeight) { holder.seekBar.maxHeight = seekBarDisabledHeight + setVerticalPadding(seekBarDisabledVerticalPadding) } holder.seekBar.setEnabled(false) holder.seekBar.getThumb().setAlpha(0) @@ -51,8 +56,15 @@ class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarVi holder.seekBar.getThumb().setAlpha(if (data.seekAvailable) 255 else 0) holder.seekBar.setEnabled(data.seekAvailable) - if (holder.seekBar.maxHeight != seekBarDefaultMaxHeight) { - holder.seekBar.maxHeight = seekBarDefaultMaxHeight + if (holder.seekBar.maxHeight != seekBarEnabledMaxHeight) { + holder.seekBar.maxHeight = seekBarEnabledMaxHeight + setVerticalPadding(seekBarEnabledVerticalPadding) + } + + data.duration?.let { + holder.seekBar.setMax(it) + holder.totalTimeView.setText(DateUtils.formatElapsedTime( + it / DateUtils.SECOND_IN_MILLIS)) } data.elapsedTime?.let { @@ -60,11 +72,12 @@ class SeekBarObserver(private val holder: PlayerViewHolder) : Observer<SeekBarVi holder.elapsedTimeView.setText(DateUtils.formatElapsedTime( it / DateUtils.SECOND_IN_MILLIS)) } + } - data.duration?.let { - holder.seekBar.setMax(it) - holder.totalTimeView.setText(DateUtils.formatElapsedTime( - it / DateUtils.SECOND_IN_MILLIS)) - } + @UiThread + fun setVerticalPadding(padding: Int) { + val leftPadding = holder.seekBar.paddingLeft + val rightPadding = holder.seekBar.paddingRight + holder.seekBar.setPadding(leftPadding, padding, rightPadding, padding) } } diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt index 1dca3f1297b1..9e326aaec3c1 100644 --- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt @@ -71,7 +71,7 @@ private fun PlaybackState.computePosition(duration: Long): Long { /** ViewModel for seek bar in QS media player. */ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: RepeatableExecutor) { - private var _data = Progress(false, false, null, null) + private var _data = Progress(false, false, null, 0) set(value) { field = value _progress.postValue(value) @@ -186,10 +186,10 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R val mediaMetadata = controller?.metadata val seekAvailable = ((playbackState?.actions ?: 0L) and PlaybackState.ACTION_SEEK_TO) != 0L val position = playbackState?.position?.toInt() - val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() + val duration = mediaMetadata?.getLong(MediaMetadata.METADATA_KEY_DURATION)?.toInt() ?: 0 val enabled = if (playbackState == null || playbackState?.getState() == PlaybackState.STATE_NONE || - (duration != null && duration <= 0)) false else true + (duration <= 0)) false else true _data = Progress(enabled, seekAvailable, position, duration) checkIfPollingNeeded() } @@ -408,6 +408,6 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R val enabled: Boolean, val seekAvailable: Boolean, val elapsedTime: Int?, - val duration: Int? + val duration: Int ) } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java index ead17867844a..72019315139b 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java @@ -21,7 +21,6 @@ import android.animation.Animator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.annotation.IntDef; -import android.content.Context; import android.graphics.Rect; import android.view.SurfaceControl; @@ -56,13 +55,15 @@ public class PipAnimationController { public static final int TRANSITION_DIRECTION_TO_PIP = 2; public static final int TRANSITION_DIRECTION_TO_FULLSCREEN = 3; public static final int TRANSITION_DIRECTION_TO_SPLIT_SCREEN = 4; + public static final int TRANSITION_DIRECTION_REMOVE_STACK = 5; @IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = { TRANSITION_DIRECTION_NONE, TRANSITION_DIRECTION_SAME, TRANSITION_DIRECTION_TO_PIP, TRANSITION_DIRECTION_TO_FULLSCREEN, - TRANSITION_DIRECTION_TO_SPLIT_SCREEN + TRANSITION_DIRECTION_TO_SPLIT_SCREEN, + TRANSITION_DIRECTION_REMOVE_STACK }) @Retention(RetentionPolicy.SOURCE) public @interface TransitionDirection {} @@ -88,7 +89,7 @@ public class PipAnimationController { }); @Inject - PipAnimationController(Context context, PipSurfaceTransactionHelper helper) { + PipAnimationController(PipSurfaceTransactionHelper helper) { mSurfaceTransactionHelper = helper; } @@ -338,6 +339,10 @@ public class PipAnimationController { @Override void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { + if (getTransitionDirection() == TRANSITION_DIRECTION_REMOVE_STACK) { + // while removing the pip stack, no extra work needs to be done here. + return; + } getSurfaceTransactionHelper() .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()) diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java index 8bbd15babf19..583953ce34af 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java @@ -177,7 +177,7 @@ public class PipBoundsHandler { } if (isValidPictureInPictureAspectRatio(mAspectRatio)) { transformBoundsToAspectRatio(normalBounds, mAspectRatio, - false /* useCurrentMinEdgeSize */); + false /* useCurrentMinEdgeSize */, false /* useCurrentSize */); } displayInfo.copyFrom(mDisplayInfo); } @@ -278,7 +278,9 @@ public class PipBoundsHandler { destinationBounds = new Rect(bounds); } if (isValidPictureInPictureAspectRatio(aspectRatio)) { - transformBoundsToAspectRatio(destinationBounds, aspectRatio, useCurrentMinEdgeSize); + boolean useCurrentSize = bounds == null && mReentrySize != null; + transformBoundsToAspectRatio(destinationBounds, aspectRatio, useCurrentMinEdgeSize, + useCurrentSize); } mAspectRatio = aspectRatio; return destinationBounds; @@ -384,7 +386,8 @@ public class PipBoundsHandler { * @param stackBounds */ public void transformBoundsToAspectRatio(Rect stackBounds) { - transformBoundsToAspectRatio(stackBounds, mAspectRatio, true); + transformBoundsToAspectRatio(stackBounds, mAspectRatio, true /* useCurrentMinEdgeSize */, + true /* useCurrentSize */); } /** @@ -392,18 +395,16 @@ public class PipBoundsHandler { * specified aspect ratio. */ private void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio, - boolean useCurrentMinEdgeSize) { + boolean useCurrentMinEdgeSize, boolean useCurrentSize) { // Save the snap fraction and adjust the size based on the new aspect ratio. final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds)); - final int minEdgeSize; + final int minEdgeSize = useCurrentMinEdgeSize ? mCurrentMinSize : mDefaultMinSize; final Size size; - if (useCurrentMinEdgeSize) { - minEdgeSize = mCurrentMinSize; + if (useCurrentMinEdgeSize || useCurrentSize) { size = mSnapAlgorithm.getSizeForAspectRatio( new Size(stackBounds.width(), stackBounds.height()), aspectRatio, minEdgeSize); } else { - minEdgeSize = mDefaultMinSize; size = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio, minEdgeSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight); } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index 0141dee04086..54df53dbe6d7 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -24,6 +24,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_ALPHA; import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_BOUNDS; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_NONE; +import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_FULLSCREEN; import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; @@ -39,7 +40,6 @@ import android.app.PictureInPictureParams; import android.content.ComponentName; import android.content.Context; import android.content.pm.ActivityInfo; -import android.content.res.Configuration; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; @@ -94,15 +94,46 @@ public class PipTaskOrganizer extends TaskOrganizer implements private static final int MSG_FINISH_RESIZE = 4; private static final int MSG_RESIZE_USER = 5; + // Not a complete set of states but serves what we want right now. + private enum State { + UNDEFINED(0), + TASK_APPEARED(1), + ENTERING_PIP(2), + EXITING_PIP(3); + + private final int mStateValue; + + State(int value) { + mStateValue = value; + } + + private boolean isInPip() { + return mStateValue >= TASK_APPEARED.mStateValue + && mStateValue != EXITING_PIP.mStateValue; + } + + /** + * Resize request can be initiated in other component, ignore if we are no longer in PIP, + * still waiting for animation or we're exiting from it. + * + * @return {@code true} if the resize request should be blocked/ignored. + */ + private boolean shouldBlockResizeRequest() { + return mStateValue < ENTERING_PIP.mStateValue + || mStateValue == EXITING_PIP.mStateValue; + } + } + private final Handler mMainHandler; private final Handler mUpdateHandler; private final PipBoundsHandler mPipBoundsHandler; private final PipAnimationController mPipAnimationController; + private final PipUiEventLogger mPipUiEventLoggerLogger; private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>(); private final Rect mLastReportedBounds = new Rect(); private final int mEnterExitAnimationDuration; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; - private final Map<IBinder, Configuration> mInitialState = new HashMap<>(); + private final Map<IBinder, PipWindowConfigurationCompact> mCompactState = new HashMap<>(); private final Divider mSplitDivider; // These callbacks are called on the update thread @@ -187,8 +218,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements private ActivityManager.RunningTaskInfo mTaskInfo; private WindowContainerToken mToken; private SurfaceControl mLeash; - private boolean mInPip; - private boolean mExitingPip; + private State mState = State.UNDEFINED; private @PipAnimationController.AnimationType int mOneShotAnimationType = ANIM_TYPE_BOUNDS; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; @@ -200,12 +230,15 @@ public class PipTaskOrganizer extends TaskOrganizer implements */ private boolean mShouldDeferEnteringPip; + private @ActivityInfo.ScreenOrientation int mRequestedOrientation; + @Inject public PipTaskOrganizer(Context context, @NonNull PipBoundsHandler boundsHandler, @NonNull PipSurfaceTransactionHelper surfaceTransactionHelper, @Nullable Divider divider, @NonNull DisplayController displayController, - @NonNull PipAnimationController pipAnimationController) { + @NonNull PipAnimationController pipAnimationController, + @NonNull PipUiEventLogger pipUiEventLogger) { mMainHandler = new Handler(Looper.getMainLooper()); mUpdateHandler = new Handler(PipUpdateThread.get().getLooper(), mUpdateCallbacks); mPipBoundsHandler = boundsHandler; @@ -213,6 +246,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements .getInteger(R.integer.config_pipResizeAnimationDuration); mSurfaceTransactionHelper = surfaceTransactionHelper; mPipAnimationController = pipAnimationController; + mPipUiEventLoggerLogger = pipUiEventLogger; mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new; mSplitDivider = divider; displayController.addDisplayWindowListener(this); @@ -236,11 +270,11 @@ public class PipTaskOrganizer extends TaskOrganizer implements } public boolean isInPip() { - return mInPip; + return mState.isInPip(); } public boolean isDeferringEnterPipAnimation() { - return mInPip && mShouldDeferEnteringPip; + return mState.isInPip() && mShouldDeferEnteringPip; } /** @@ -269,21 +303,31 @@ public class PipTaskOrganizer extends TaskOrganizer implements * @param animationDurationMs duration in millisecond for the exiting PiP transition */ public void exitPip(int animationDurationMs) { - if (!mInPip || mExitingPip || mToken == null) { + if (!mState.isInPip() || mToken == null) { Log.wtf(TAG, "Not allowed to exitPip in current state" - + " mInPip=" + mInPip + " mExitingPip=" + mExitingPip + " mToken=" + mToken); + + " mState=" + mState + " mToken=" + mToken); return; } - final Configuration initialConfig = mInitialState.remove(mToken.asBinder()); - final boolean orientationDiffers = initialConfig.windowConfiguration.getRotation() + final PipWindowConfigurationCompact config = mCompactState.remove(mToken.asBinder()); + if (config == null) { + Log.wtf(TAG, "Token not in record, this should not happen mToken=" + mToken); + return; + } + + mPipUiEventLoggerLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); + config.syncWithScreenOrientation(mRequestedOrientation, + mPipBoundsHandler.getDisplayRotation()); + final boolean orientationDiffers = config.getRotation() != mPipBoundsHandler.getDisplayRotation(); final WindowContainerTransaction wct = new WindowContainerTransaction(); - final Rect destinationBounds = initialConfig.windowConfiguration.getBounds(); + final Rect destinationBounds = config.getBounds(); final int direction = syncWithSplitScreenBounds(destinationBounds) ? TRANSITION_DIRECTION_TO_SPLIT_SCREEN : TRANSITION_DIRECTION_TO_FULLSCREEN; if (orientationDiffers) { + mState = State.EXITING_PIP; // Send started callback though animation is ignored. sendOnPipTransitionStarted(direction); // Don't bother doing an animation if the display rotation differs or if it's in @@ -292,7 +336,6 @@ public class PipTaskOrganizer extends TaskOrganizer implements WindowOrganizer.applyTransaction(wct); // Send finished callback though animation is ignored. sendOnPipTransitionFinished(direction); - mInPip = false; } else { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); @@ -311,11 +354,10 @@ public class PipTaskOrganizer extends TaskOrganizer implements scheduleAnimateResizePip(mLastReportedBounds, destinationBounds, null /* sourceHintRect */, direction, animationDurationMs, null /* updateBoundsCallback */); - mInPip = false; + mState = State.EXITING_PIP; } }); } - mExitingPip = true; } private void applyWindowingModeChangeOnExit(WindowContainerTransaction wct, int direction) { @@ -332,26 +374,35 @@ public class PipTaskOrganizer extends TaskOrganizer implements * Removes PiP immediately. */ public void removePip() { - if (!mInPip || mExitingPip || mToken == null) { + if (!mState.isInPip() || mToken == null) { Log.wtf(TAG, "Not allowed to removePip in current state" - + " mInPip=" + mInPip + " mExitingPip=" + mExitingPip + " mToken=" + mToken); + + " mState=" + mState + " mToken=" + mToken); return; } - getUpdateHandler().post(() -> { - try { - // Reset the task bounds first to ensure the activity configuration is reset as well - final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.setBounds(mToken, null); - WindowOrganizer.applyTransaction(wct); - - ActivityTaskManager.getService().removeStacksInWindowingModes( - new int[]{ WINDOWING_MODE_PINNED }); - } catch (RemoteException e) { - Log.e(TAG, "Failed to remove PiP", e); - } - }); - mInitialState.remove(mToken.asBinder()); - mExitingPip = true; + + // removePipImmediately is expected when the following animation finishes. + mUpdateHandler.post(() -> mPipAnimationController + .getAnimator(mLeash, mLastReportedBounds, 1f, 0f) + .setTransitionDirection(TRANSITION_DIRECTION_REMOVE_STACK) + .setPipAnimationCallback(mPipAnimationCallback) + .setDuration(mEnterExitAnimationDuration) + .start()); + mCompactState.remove(mToken.asBinder()); + mState = State.EXITING_PIP; + } + + private void removePipImmediately() { + try { + // Reset the task bounds first to ensure the activity configuration is reset as well + final WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mToken, null); + WindowOrganizer.applyTransaction(wct); + + ActivityTaskManager.getService().removeStacksInWindowingModes( + new int[]{ WINDOWING_MODE_PINNED }); + } catch (RemoteException e) { + Log.e(TAG, "Failed to remove PiP", e); + } } @Override @@ -359,11 +410,15 @@ public class PipTaskOrganizer extends TaskOrganizer implements Objects.requireNonNull(info, "Requires RunningTaskInfo"); mTaskInfo = info; mToken = mTaskInfo.token; - mInPip = true; - mExitingPip = false; + mState = State.TASK_APPEARED; mLeash = leash; - mInitialState.put(mToken.asBinder(), new Configuration(mTaskInfo.configuration)); + mCompactState.put(mToken.asBinder(), + new PipWindowConfigurationCompact(mTaskInfo.configuration.windowConfiguration)); mPictureInPictureParams = mTaskInfo.pictureInPictureParams; + mRequestedOrientation = info.requestedOrientation; + + mPipUiEventLoggerLogger.setTaskInfo(mTaskInfo); + mPipUiEventLoggerLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_ENTER); if (mShouldDeferEnteringPip) { if (DEBUG) Log.d(TAG, "Defer entering PiP animation, fixed rotation is ongoing"); @@ -387,6 +442,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, null /* updateBoundsCallback */); + mState = State.ENTERING_PIP; } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { enterPipWithAlphaAnimation(destinationBounds, mEnterExitAnimationDuration); mOneShotAnimationType = ANIM_TYPE_BOUNDS; @@ -432,6 +488,9 @@ public class PipTaskOrganizer extends TaskOrganizer implements .setPipAnimationCallback(mPipAnimationCallback) .setDuration(durationMs) .start()); + // mState is set right after the animation is kicked off to block any resize + // requests such as offsetPip that may have been called prior to the transition. + mState = State.ENTERING_PIP; } }); } @@ -484,7 +543,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements */ @Override public void onTaskVanished(ActivityManager.RunningTaskInfo info) { - if (!mInPip) { + if (!mState.isInPip()) { return; } final WindowContainerToken token = info.token; @@ -495,13 +554,15 @@ public class PipTaskOrganizer extends TaskOrganizer implements } mShouldDeferEnteringPip = false; mPictureInPictureParams = null; - mInPip = false; - mExitingPip = false; + mState = State.UNDEFINED; + mPipUiEventLoggerLogger.setTaskInfo(null); } @Override public void onTaskInfoChanged(ActivityManager.RunningTaskInfo info) { Objects.requireNonNull(mToken, "onTaskInfoChanged requires valid existing mToken"); + mRequestedOrientation = info.requestedOrientation; + // check PictureInPictureParams for aspect ratio change. final PictureInPictureParams newParams = info.pictureInPictureParams; if (newParams == null || !applyPictureInPictureParams(newParams)) { Log.d(TAG, "Ignored onTaskInfoChanged with PiP param: " + newParams); @@ -528,7 +589,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements @Override public void onFixedRotationFinished(int displayId) { - if (mShouldDeferEnteringPip && mInPip) { + if (mShouldDeferEnteringPip && mState.isInPip()) { final Rect destinationBounds = mPipBoundsHandler.getDestinationBounds( mTaskInfo.topActivity, getAspectRatioOrDefault(mPictureInPictureParams), null /* bounds */, getMinimalSize(mTaskInfo.topActivityInfo)); @@ -539,8 +600,6 @@ public class PipTaskOrganizer extends TaskOrganizer implements } /** - * TODO(b/152809058): consolidate the display info handling logic in SysUI - * * @param destinationBoundsOut the current destination bounds will be populated to this param */ @SuppressWarnings("unchecked") @@ -551,7 +610,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements mPipAnimationController.getCurrentAnimator(); if (animator == null || !animator.isRunning() || animator.getTransitionDirection() != TRANSITION_DIRECTION_TO_PIP) { - if (mInPip && fromRotation) { + if (mState.isInPip() && fromRotation) { // If we are rotating while there is a current animation, immediately cancel the // animation (remove the listeners so we don't trigger the normal finish resize // call that should only happen on the update thread) @@ -612,7 +671,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements * {@link PictureInPictureParams} would affect the bounds. */ private boolean applyPictureInPictureParams(@NonNull PictureInPictureParams params) { - final boolean changed = (mPictureInPictureParams == null) ? true : !Objects.equals( + final boolean changed = (mPictureInPictureParams == null) || !Objects.equals( mPictureInPictureParams.getAspectRatioRational(), params.getAspectRatioRational()); if (changed) { mPictureInPictureParams = params; @@ -637,10 +696,10 @@ public class PipTaskOrganizer extends TaskOrganizer implements private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs, Consumer<Rect> updateBoundsCallback) { - if (!mInPip) { + if (!mState.isInPip()) { // TODO: tend to use shouldBlockResizeRequest here as well but need to consider // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window - // container transaction callback and we want to set the mExitingPip immediately. + // container transaction callback and we want to set the mState immediately. return; } @@ -697,7 +756,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements private void scheduleFinishResizePip(Rect destinationBounds, @PipAnimationController.TransitionDirection int direction, Consumer<Rect> updateBoundsCallback) { - if (shouldBlockResizeRequest()) { + if (mState.shouldBlockResizeRequest()) { return; } @@ -716,7 +775,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) .resetScale(tx, mLeash, destinationBounds) - .round(tx, mLeash, mInPip); + .round(tx, mLeash, mState.isInPip()); return tx; } @@ -725,7 +784,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements */ public void scheduleOffsetPip(Rect originalBounds, int offset, int duration, Consumer<Rect> updateBoundsCallback) { - if (shouldBlockResizeRequest()) { + if (mState.shouldBlockResizeRequest()) { return; } if (mShouldDeferEnteringPip) { @@ -770,7 +829,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .crop(tx, mLeash, destinationBounds) - .round(tx, mLeash, mInPip); + .round(tx, mLeash, mState.isInPip()); tx.apply(); } @@ -803,7 +862,10 @@ public class PipTaskOrganizer extends TaskOrganizer implements + "directly"); } mLastReportedBounds.set(destinationBounds); - if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { + if (direction == TRANSITION_DIRECTION_REMOVE_STACK) { + removePipImmediately(); + return; + } else if (isInPipDirection(direction) && type == ANIM_TYPE_ALPHA) { return; } @@ -899,16 +961,6 @@ public class PipTaskOrganizer extends TaskOrganizer implements } /** - * Resize request can be initiated in other component, ignore if we are no longer in PIP - * or we're exiting from it. - * - * @return {@code true} if the resize request should be blocked/ignored. - */ - private boolean shouldBlockResizeRequest() { - return !mInPip || mExitingPip; - } - - /** * Sync with {@link #mSplitDivider} on destination bounds if PiP is going to split screen. * * @param destinationBoundsOut contain the updated destination bounds if applicable @@ -936,14 +988,14 @@ public class PipTaskOrganizer extends TaskOrganizer implements pw.println(innerPrefix + "mToken=" + mToken + " binder=" + (mToken != null ? mToken.asBinder() : null)); pw.println(innerPrefix + "mLeash=" + mLeash); - pw.println(innerPrefix + "mInPip=" + mInPip); + pw.println(innerPrefix + "mState=" + mState); pw.println(innerPrefix + "mOneShotAnimationType=" + mOneShotAnimationType); pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams); pw.println(innerPrefix + "mLastReportedBounds=" + mLastReportedBounds); pw.println(innerPrefix + "mInitialState:"); - for (Map.Entry<IBinder, Configuration> e : mInitialState.entrySet()) { + for (Map.Entry<IBinder, PipWindowConfigurationCompact> e : mCompactState.entrySet()) { pw.println(innerPrefix + " binder=" + e.getKey() - + " winConfig=" + e.getValue().windowConfiguration); + + " config=" + e.getValue()); } } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipUiEventLogger.java b/packages/SystemUI/src/com/android/systemui/pip/PipUiEventLogger.java new file mode 100644 index 000000000000..970203578e73 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/PipUiEventLogger.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.pip; + +import android.app.TaskInfo; +import android.content.pm.PackageManager; + +import com.android.internal.logging.UiEvent; +import com.android.internal.logging.UiEventLogger; + +import javax.inject.Inject; +import javax.inject.Singleton; + + +/** + * Helper class that ends PiP log to UiEvent, see also go/uievent + */ +@Singleton +public class PipUiEventLogger { + + private static final int INVALID_PACKAGE_UID = -1; + + private final UiEventLogger mUiEventLogger; + private final PackageManager mPackageManager; + + private String mPackageName; + private int mPackageUid = INVALID_PACKAGE_UID; + + @Inject + public PipUiEventLogger(UiEventLogger uiEventLogger, PackageManager packageManager) { + mUiEventLogger = uiEventLogger; + mPackageManager = packageManager; + } + + public void setTaskInfo(TaskInfo taskInfo) { + if (taskInfo == null) { + mPackageName = null; + mPackageUid = INVALID_PACKAGE_UID; + } else { + mPackageName = taskInfo.topActivity.getPackageName(); + mPackageUid = getUid(mPackageName, taskInfo.userId); + } + } + + /** + * Sends log via UiEvent, reference go/uievent for how to debug locally + */ + public void log(PipUiEventEnum event) { + if (mPackageName == null || mPackageUid == INVALID_PACKAGE_UID) { + return; + } + mUiEventLogger.log(event, mPackageUid, mPackageName); + } + + private int getUid(String packageName, int userId) { + int uid = INVALID_PACKAGE_UID; + try { + uid = mPackageManager.getApplicationInfoAsUser( + packageName, 0 /* ApplicationInfoFlags */, userId).uid; + } catch (PackageManager.NameNotFoundException e) { + // do nothing. + } + return uid; + } + + /** + * Enums for logging the PiP events to UiEvent + */ + public enum PipUiEventEnum implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Activity enters picture-in-picture mode") + PICTURE_IN_PICTURE_ENTER(603), + + @UiEvent(doc = "Expands from picture-in-picture to fullscreen") + PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN(604), + + @UiEvent(doc = "Removes picture-in-picture by tap close button") + PICTURE_IN_PICTURE_TAP_TO_REMOVE(605), + + @UiEvent(doc = "Removes picture-in-picture by drag to dismiss area") + PICTURE_IN_PICTURE_DRAG_TO_REMOVE(606), + + @UiEvent(doc = "Shows picture-in-picture menu") + PICTURE_IN_PICTURE_SHOW_MENU(607), + + @UiEvent(doc = "Hides picture-in-picture menu") + PICTURE_IN_PICTURE_HIDE_MENU(608), + + @UiEvent(doc = "Changes the aspect ratio of picture-in-picture window. This is inherited" + + " from previous Tron-based logging and currently not in use.") + PICTURE_IN_PICTURE_CHANGE_ASPECT_RATIO(609), + + @UiEvent(doc = "User resize of the picture-in-picture window") + PICTURE_IN_PICTURE_RESIZE(610); + + private final int mId; + + PipUiEventEnum(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipWindowConfigurationCompact.java b/packages/SystemUI/src/com/android/systemui/pip/PipWindowConfigurationCompact.java new file mode 100644 index 000000000000..ba104d676cd2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/pip/PipWindowConfigurationCompact.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.pip; + +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + +import android.app.WindowConfiguration; +import android.content.pm.ActivityInfo; +import android.graphics.Rect; +import android.view.Surface; + +/** + * Compact {@link WindowConfiguration} for PiP usage and supports operations such as rotate. + */ +class PipWindowConfigurationCompact { + private @Surface.Rotation int mRotation; + private Rect mBounds; + + PipWindowConfigurationCompact(WindowConfiguration windowConfiguration) { + mRotation = windowConfiguration.getRotation(); + mBounds = windowConfiguration.getBounds(); + } + + @Surface.Rotation int getRotation() { + return mRotation; + } + + Rect getBounds() { + return mBounds; + } + + void syncWithScreenOrientation(@ActivityInfo.ScreenOrientation int screenOrientation, + @Surface.Rotation int displayRotation) { + if (mBounds.top != 0 || mBounds.left != 0) { + // Supports fullscreen bounds like (0, 0, width, height) only now. + return; + } + boolean rotateNeeded = false; + if (ActivityInfo.isFixedOrientationPortrait(screenOrientation) + && (mRotation == ROTATION_90 || mRotation == ROTATION_270)) { + mRotation = ROTATION_0; + rotateNeeded = true; + } else if (ActivityInfo.isFixedOrientationLandscape(screenOrientation) + && (mRotation == ROTATION_0 || mRotation == ROTATION_180)) { + mRotation = ROTATION_90; + rotateNeeded = true; + } else if (screenOrientation == SCREEN_ORIENTATION_UNSPECIFIED + && mRotation != displayRotation) { + mRotation = displayRotation; + rotateNeeded = true; + } + if (rotateNeeded) { + mBounds.set(0, 0, mBounds.height(), mBounds.width()); + } + } + + @Override + public String toString() { + return "PipWindowConfigurationCompact(rotation=" + mRotation + + " bounds=" + mBounds + ")"; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java index 7d35416a8d1d..9d9c5a678baf 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -46,6 +46,7 @@ import com.android.systemui.pip.BasePipManager; import com.android.systemui.pip.PipBoundsHandler; import com.android.systemui.pip.PipSnapAlgorithm; import com.android.systemui.pip.PipTaskOrganizer; +import com.android.systemui.pip.PipUiEventLogger; import com.android.systemui.shared.recents.IPinnedStackAnimationListener; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputConsumerController; @@ -229,7 +230,10 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onAspectRatioChanged(float aspectRatio) { - mHandler.post(() -> mPipBoundsHandler.onAspectRatioChanged(aspectRatio)); + mHandler.post(() -> { + mPipBoundsHandler.onAspectRatioChanged(aspectRatio); + mTouchHandler.onAspectRatioChanged(); + }); } } @@ -241,7 +245,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio PipBoundsHandler pipBoundsHandler, PipSnapAlgorithm pipSnapAlgorithm, PipTaskOrganizer pipTaskOrganizer, - SysUiState sysUiState) { + SysUiState sysUiState, + PipUiEventLogger pipUiEventLogger) { mContext = context; mActivityManager = ActivityManager.getService(); @@ -262,7 +267,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio mInputConsumerController); mTouchHandler = new PipTouchHandler(context, mActivityManager, mMenuController, mInputConsumerController, mPipBoundsHandler, mPipTaskOrganizer, - floatingContentCoordinator, deviceConfig, pipSnapAlgorithm, sysUiState); + floatingContentCoordinator, deviceConfig, pipSnapAlgorithm, sysUiState, + pipUiEventLogger); mAppOpsListener = new PipAppOpsListener(context, mActivityManager, mTouchHandler.getMotionHelper()); displayController.addDisplayChangingController(mRotationController); @@ -355,17 +361,8 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onPipTransitionStarted(ComponentName activity, int direction) { if (isOutPipDirection(direction)) { - // On phones, the expansion animation that happens on pip tap before restoring - // to fullscreen makes it so that the bounds received here are the expanded - // bounds. We want to restore to the unexpanded bounds when re-entering pip, - // so we save the bounds before expansion (normal) instead of the current - // bounds. - mReentryBounds.set(mTouchHandler.getNormalBounds()); - // Apply the snap fraction of the current bounds to the normal bounds. - final Rect bounds = mPipTaskOrganizer.getLastReportedBounds(); - float snapFraction = mPipBoundsHandler.getSnapFraction(bounds); - mPipBoundsHandler.applySnapFraction(mReentryBounds, snapFraction); - // Save reentry bounds (normal non-expand bounds with current position applied). + // Exiting PIP, save the reentry bounds to restore to when re-entering. + updateReentryBounds(); mPipBoundsHandler.onSaveReentryBounds(activity, mReentryBounds); } // Disable touches while the animation is running @@ -379,6 +376,18 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio } } + /** + * Update the bounds used to save the re-entry size and snap fraction when exiting PIP. + */ + public void updateReentryBounds() { + final Rect reentryBounds = mTouchHandler.getUserResizeBounds(); + // Apply the snap fraction of the current bounds to the normal bounds. + final Rect bounds = mPipTaskOrganizer.getLastReportedBounds(); + float snapFraction = mPipBoundsHandler.getSnapFraction(bounds); + mPipBoundsHandler.applySnapFraction(reentryBounds, snapFraction); + mReentryBounds.set(reentryBounds); + } + @Override public void onPipTransitionFinished(ComponentName activity, int direction) { onPipTransitionFinishedOrCanceled(direction); diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java index 2a83aa06a237..586399c6dfd5 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMenuActivity.java @@ -562,8 +562,10 @@ public class PipMenuActivity extends Activity { // TODO: Check if the action drawable has changed before we reload it action.getIcon().loadDrawableAsync(this, d -> { - d.setTint(Color.WHITE); - actionView.setImageDrawable(d); + if (d != null) { + d.setTint(Color.WHITE); + actionView.setImageDrawable(d); + } }, mHandler); actionView.setContentDescription(action.getContentDescription()); if (action.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java index 8a2e4ae11878..9f0b1de21b52 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipMotionHelper.java @@ -26,9 +26,10 @@ import android.os.Debug; import android.util.Log; import android.view.Choreographer; +import androidx.dynamicanimation.animation.AnimationHandler; +import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler; import androidx.dynamicanimation.animation.SpringForce; -import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.pip.PipSnapAlgorithm; import com.android.systemui.pip.PipTaskOrganizer; import com.android.systemui.util.FloatingContentCoordinator; @@ -74,9 +75,6 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** The region that all of PIP must stay within. */ private final Rect mFloatingAllowedArea = new Rect(); - private final SfVsyncFrameCallbackProvider mSfVsyncFrameProvider = - new SfVsyncFrameCallbackProvider(); - /** * Temporary bounds used when PIP is being dragged or animated. These bounds are applied to PIP * using {@link PipTaskOrganizer#scheduleUserResizePip}, so that we can animate shrinking into @@ -94,8 +92,13 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, /** Coordinator instance for resolving conflicts with other floating content. */ private FloatingContentCoordinator mFloatingContentCoordinator; - /** Callback that re-sizes PIP to the animated bounds. */ - private final Choreographer.FrameCallback mResizePipVsyncCallback; + private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal = + ThreadLocal.withInitial(() -> { + FrameCallbackScheduler scheduler = runnable -> + Choreographer.getSfInstance().postFrameCallback(t -> runnable.run()); + AnimationHandler handler = new AnimationHandler(scheduler); + return handler; + }); /** * PhysicsAnimator instance for animating {@link #mTemporaryBounds} using physics animations. @@ -171,16 +174,15 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, mSnapAlgorithm = snapAlgorithm; mFloatingContentCoordinator = floatingContentCoordinator; mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback); + mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler( + mSfAnimationHandlerThreadLocal.get()); - mResizePipVsyncCallback = l -> { + mResizePipUpdateListener = (target, values) -> { if (!mTemporaryBounds.isEmpty()) { mPipTaskOrganizer.scheduleUserResizePip( mBounds, mTemporaryBounds, null); } }; - - mResizePipUpdateListener = (target, values) -> - mSfVsyncFrameProvider.postFrameCallback(mResizePipVsyncCallback); } @NonNull diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java index 1ca53f907994..badd8835cdd4 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java @@ -53,12 +53,12 @@ import com.android.systemui.R; import com.android.systemui.model.SysUiState; import com.android.systemui.pip.PipBoundsHandler; import com.android.systemui.pip.PipTaskOrganizer; +import com.android.systemui.pip.PipUiEventLogger; import com.android.systemui.util.DeviceConfigProxy; import java.io.PrintWriter; import java.util.concurrent.Executor; import java.util.function.Function; -import java.util.function.Supplier; /** * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to @@ -80,6 +80,7 @@ public class PipResizeGestureHandler { private final Context mContext; private final PipBoundsHandler mPipBoundsHandler; private final PipMotionHelper mMotionHelper; + private final PipMenuActivityController mMenuController; private final int mDisplayId; private final Executor mMainExecutor; private final SysUiState mSysUiState; @@ -89,6 +90,7 @@ public class PipResizeGestureHandler { private final Point mMaxSize = new Point(); private final Point mMinSize = new Point(); private final Rect mLastResizeBounds = new Rect(); + private final Rect mUserResizeBounds = new Rect(); private final Rect mLastDownBounds = new Rect(); private final Rect mDragCornerSize = new Rect(); private final Rect mTmpTopLeftCorner = new Rect(); @@ -110,22 +112,26 @@ public class PipResizeGestureHandler { private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; private PipTaskOrganizer mPipTaskOrganizer; + private PipUiEventLogger mPipUiEventLogger; private int mCtrlType; public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, - PipTaskOrganizer pipTaskOrganizer, Function<Rect, Rect> movementBoundsSupplier, - Runnable updateMovementBoundsRunnable, SysUiState sysUiState) { + PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController pipMenuController, + Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, + SysUiState sysUiState, PipUiEventLogger pipUiEventLogger) { mContext = context; mDisplayId = context.getDisplayId(); mMainExecutor = context.getMainExecutor(); mPipBoundsHandler = pipBoundsHandler; + mMenuController = pipMenuController; mMotionHelper = motionHelper; mPipTaskOrganizer = pipTaskOrganizer; mMovementBoundsSupplier = movementBoundsSupplier; mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; mSysUiState = sysUiState; + mPipUiEventLogger = pipUiEventLogger; context.getDisplay().getRealSize(mMaxSize); reloadResources(); @@ -182,6 +188,7 @@ public class PipResizeGestureHandler { void onActivityUnpinned() { mIsAttached = false; + mUserResizeBounds.setEmpty(); updateIsEnabled(); } @@ -317,6 +324,10 @@ public class PipResizeGestureHandler { mInputMonitor.pilferPointers(); } if (mThresholdCrossed) { + if (mMenuController.isMenuActivityVisible()) { + mMenuController.hideMenuWithoutResize(); + mMenuController.hideMenu(); + } final Rect currentPipBounds = mMotionHelper.getBounds(); mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x, @@ -330,6 +341,7 @@ public class PipResizeGestureHandler { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (!mLastResizeBounds.isEmpty()) { + mUserResizeBounds.set(mLastResizeBounds); mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, (Rect bounds) -> { new Handler(Looper.getMainLooper()).post(() -> { @@ -338,6 +350,8 @@ public class PipResizeGestureHandler { resetState(); }); }); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); } else { resetState(); } @@ -352,6 +366,18 @@ public class PipResizeGestureHandler { mThresholdCrossed = false; } + void setUserResizeBounds(Rect bounds) { + mUserResizeBounds.set(bounds); + } + + void invalidateUserResizeBounds() { + mUserResizeBounds.setEmpty(); + } + + Rect getUserResizeBounds() { + return mUserResizeBounds; + } + void updateMaxSize(int maxX, int maxY) { mMaxSize.set(maxX, maxY); } diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java index a8130a1e6d1a..11e609b2ffef 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -34,7 +34,6 @@ import android.graphics.drawable.TransitionDrawable; import android.os.Handler; import android.os.RemoteException; import android.util.Log; -import android.util.Pair; import android.util.Size; import android.view.Gravity; import android.view.IPinnedStackController; @@ -55,13 +54,13 @@ import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.os.logging.MetricsLoggerWrapper; import com.android.systemui.R; import com.android.systemui.model.SysUiState; import com.android.systemui.pip.PipAnimationController; import com.android.systemui.pip.PipBoundsHandler; import com.android.systemui.pip.PipSnapAlgorithm; import com.android.systemui.pip.PipTaskOrganizer; +import com.android.systemui.pip.PipUiEventLogger; import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.util.DeviceConfigProxy; import com.android.systemui.util.DismissCircleView; @@ -94,6 +93,8 @@ public class PipTouchHandler { private final WindowManager mWindowManager; private final IActivityManager mActivityManager; private final PipBoundsHandler mPipBoundsHandler; + private final PipUiEventLogger mPipUiEventLogger; + private PipResizeGestureHandler mPipResizeGestureHandler; private IPinnedStackController mPinnedStackController; @@ -132,9 +133,6 @@ public class PipTouchHandler { // The current movement bounds private Rect mMovementBounds = new Rect(); - // The current resized bounds, changed by user resize. - // This is used during expand/un-expand to save/restore the user's resized size. - @VisibleForTesting Rect mResizedBounds = new Rect(); // The reference inset bounds, used to determine the dismiss fraction private Rect mInsetBounds = new Rect(); @@ -198,11 +196,7 @@ public class PipTouchHandler { @Override public void onPipDismiss() { - Pair<ComponentName, Integer> topPipActivity = PipUtils.getTopPipActivity(mContext, - mActivityManager); - if (topPipActivity.first != null) { - MetricsLoggerWrapper.logPictureInPictureDismissByTap(mContext, topPipActivity); - } + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); mTouchState.removeDoubleTapTimeoutCallback(); mMotionHelper.dismissPip(); } @@ -223,7 +217,8 @@ public class PipTouchHandler { FloatingContentCoordinator floatingContentCoordinator, DeviceConfigProxy deviceConfig, PipSnapAlgorithm pipSnapAlgorithm, - SysUiState sysUiState) { + SysUiState sysUiState, + PipUiEventLogger pipUiEventLogger) { // Initialize the Pip input consumer mContext = context; mActivityManager = activityManager; @@ -237,8 +232,8 @@ public class PipTouchHandler { mSnapAlgorithm, floatingContentCoordinator); mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsHandler, mMotionHelper, - deviceConfig, pipTaskOrganizer, this::getMovementBounds, - this::updateMovementBounds, sysUiState); + deviceConfig, pipTaskOrganizer, menuController, this::getMovementBounds, + this::updateMovementBounds, sysUiState, pipUiEventLogger); mTouchState = new PipTouchState(ViewConfiguration.get(context), mHandler, () -> mMenuController.showMenuWithDelay(MENU_STATE_FULL, mMotionHelper.getBounds(), true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle()), @@ -259,6 +254,8 @@ public class PipTouchHandler { pipTaskOrganizer, pipSnapAlgorithm, this::onAccessibilityShowMenu, this::updateMovementBounds, mHandler); + mPipUiEventLogger = pipUiEventLogger; + mTargetView = new DismissCircleView(context); mTargetViewContainer = new FrameLayout(context); mTargetViewContainer.setBackgroundDrawable( @@ -303,11 +300,8 @@ public class PipTouchHandler { hideDismissTarget(); }); - Pair<ComponentName, Integer> topPipActivity = PipUtils.getTopPipActivity(mContext, - mActivityManager); - if (topPipActivity.first != null) { - MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext, topPipActivity); - } + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE); } }); @@ -379,7 +373,6 @@ public class PipTouchHandler { mFloatingContentCoordinator.onContentRemoved(mMotionHelper); } - mResizedBounds.setEmpty(); mPipResizeGestureHandler.onActivityUnpinned(); } @@ -389,9 +382,8 @@ public class PipTouchHandler { mMotionHelper.synchronizePinnedStackBounds(); updateMovementBounds(); if (direction == TRANSITION_DIRECTION_TO_PIP) { - // updates mResizedBounds only if it's an entering PiP animation - // mResized should be otherwise updated in setMenuState. - mResizedBounds.set(mMotionHelper.getBounds()); + // Set the initial bounds as the user resize bounds. + mPipResizeGestureHandler.setUserResizeBounds(mMotionHelper.getBounds()); } if (mShowPipMenuOnAnimationEnd) { @@ -430,8 +422,21 @@ public class PipTouchHandler { } } + /** + * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window. + */ + public void onAspectRatioChanged() { + mPipResizeGestureHandler.invalidateUserResizeBounds(); + } + public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) { + // Set the user resized bounds equal to the new normal bounds in case they were + // invalidated (e.g. by an aspect ratio change). + if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) { + mPipResizeGestureHandler.setUserResizeBounds(normalBounds); + } + final int bottomOffset = mIsImeShowing ? mImeHeight : 0; final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation); if (fromDisplayRotationChanged) { @@ -804,9 +809,7 @@ public class PipTouchHandler { // Save the current snap fraction and if we do not drag or move the PiP, then // we store back to this snap fraction. Otherwise, we'll reset the snap // fraction and snap to the closest edge. - // Also save the current resized bounds so when the menu disappears, we can restore it. if (resize) { - mResizedBounds.set(mMotionHelper.getBounds()); Rect expandedBounds = new Rect(mExpandedBounds); mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds, mMovementBounds, mExpandedMovementBounds, callback); @@ -835,7 +838,7 @@ public class PipTouchHandler { } if (mDeferResizeToNormalBoundsUntilRotation == -1) { - Rect restoreBounds = new Rect(mResizedBounds); + Rect restoreBounds = new Rect(getUserResizeBounds()); Rect restoredMovementBounds = new Rect(); mSnapAlgorithm.getMovementBounds(restoreBounds, mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0); @@ -852,8 +855,10 @@ public class PipTouchHandler { // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip // as well, or it can't handle a11y focus and pip menu can't perform any action. onRegistrationChanged(menuState == MENU_STATE_NONE); - if (menuState != MENU_STATE_CLOSE) { - MetricsLoggerWrapper.logPictureInPictureMenuVisible(mContext, menuState == MENU_STATE_FULL); + if (menuState == MENU_STATE_NONE) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU); + } else if (menuState == MENU_STATE_FULL) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU); } } @@ -886,6 +891,10 @@ public class PipTouchHandler { return mNormalBounds; } + Rect getUserResizeBounds() { + return mPipResizeGestureHandler.getUserResizeBounds(); + } + /** * Gesture controlling normal movement of the PIP. */ diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index 10b04c0129e4..6abbbbeaa397 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -24,6 +24,7 @@ import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioAttributes; @@ -376,13 +377,15 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { return; } mHighTempWarning = true; + final String message = mContext.getString(R.string.high_temp_notif_message); final Notification.Builder nb = new Notification.Builder(mContext, NotificationChannels.ALERTS) .setSmallIcon(R.drawable.ic_device_thermostat_24) .setWhen(0) .setShowWhen(false) .setContentTitle(mContext.getString(R.string.high_temp_title)) - .setContentText(mContext.getString(R.string.high_temp_notif_message)) + .setContentText(message) + .setStyle(new Notification.BigTextStyle().bigText(message)) .setVisibility(Notification.VISIBILITY_PUBLIC) .setContentIntent(pendingBroadcast(ACTION_CLICKED_TEMP_WARNING)) .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_TEMP_WARNING)) @@ -402,6 +405,23 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { d.setPositiveButton(com.android.internal.R.string.ok, null); d.setShowForAllUsers(true); d.setOnDismissListener(dialog -> mHighTempDialog = null); + final String url = mContext.getString(R.string.high_temp_dialog_help_url); + if (!url.isEmpty()) { + d.setNeutralButton(R.string.high_temp_dialog_help_text, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final Intent helpIntent = + new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(url)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Dependency.get(ActivityStarter.class).startActivity(helpIntent, + true /* dismissShade */, resultCode -> { + mHighTempDialog = null; + }); + } + }); + } d.show(); mHighTempDialog = d; } @@ -420,19 +440,38 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { d.setPositiveButton(com.android.internal.R.string.ok, null); d.setShowForAllUsers(true); d.setOnDismissListener(dialog -> mThermalShutdownDialog = null); + final String url = mContext.getString(R.string.thermal_shutdown_dialog_help_url); + if (!url.isEmpty()) { + d.setNeutralButton(R.string.thermal_shutdown_dialog_help_text, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final Intent helpIntent = + new Intent(Intent.ACTION_VIEW) + .setData(Uri.parse(url)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Dependency.get(ActivityStarter.class).startActivity(helpIntent, + true /* dismissShade */, resultCode -> { + mThermalShutdownDialog = null; + }); + } + }); + } d.show(); mThermalShutdownDialog = d; } @Override public void showThermalShutdownWarning() { + final String message = mContext.getString(R.string.thermal_shutdown_message); final Notification.Builder nb = new Notification.Builder(mContext, NotificationChannels.ALERTS) .setSmallIcon(R.drawable.ic_device_thermostat_24) .setWhen(0) .setShowWhen(false) .setContentTitle(mContext.getString(R.string.thermal_shutdown_title)) - .setContentText(mContext.getString(R.string.thermal_shutdown_message)) + .setContentText(message) + .setStyle(new Notification.BigTextStyle().bigText(message)) .setVisibility(Notification.VISIBILITY_PUBLIC) .setContentIntent(pendingBroadcast(ACTION_CLICKED_THERMAL_SHUTDOWN_WARNING)) .setDeleteIntent( diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt new file mode 100644 index 000000000000..48769cda8481 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT 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.privacy + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import com.android.systemui.R + +class OngoingPrivacyChip @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttrs: Int = 0, + defStyleRes: Int = 0 +) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) { + + private val iconMarginExpanded = context.resources.getDimensionPixelSize( + R.dimen.ongoing_appops_chip_icon_margin_expanded) + private val iconMarginCollapsed = context.resources.getDimensionPixelSize( + R.dimen.ongoing_appops_chip_icon_margin_collapsed) + private val iconSize = + context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_size) + private val iconColor = context.resources.getColor( + R.color.status_bar_clock_color, context.theme) + private val sidePadding = + context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding) + private val backgroundDrawable = context.getDrawable(R.drawable.privacy_chip_bg) + private lateinit var iconsContainer: LinearLayout + private lateinit var back: FrameLayout + var expanded = false + set(value) { + if (value != field) { + field = value + updateView() + } + } + + var builder = PrivacyChipBuilder(context, emptyList<PrivacyItem>()) + var privacyList = emptyList<PrivacyItem>() + set(value) { + field = value + builder = PrivacyChipBuilder(context, value) + updateView() + } + + override fun onFinishInflate() { + super.onFinishInflate() + + back = requireViewById(R.id.background) + iconsContainer = requireViewById(R.id.icons_container) + } + + // Should only be called if the builder icons or app changed + private fun updateView() { + back.background = if (expanded) backgroundDrawable else null + val padding = if (expanded) sidePadding else 0 + back.setPaddingRelative(padding, 0, padding, 0) + fun setIcons(chipBuilder: PrivacyChipBuilder, iconsContainer: ViewGroup) { + iconsContainer.removeAllViews() + chipBuilder.generateIcons().forEachIndexed { i, it -> + it.mutate() + it.setTint(iconColor) + val image = ImageView(context).apply { + setImageDrawable(it) + scaleType = ImageView.ScaleType.CENTER_INSIDE + } + iconsContainer.addView(image, iconSize, iconSize) + if (i != 0) { + val lp = image.layoutParams as MarginLayoutParams + lp.marginStart = if (expanded) iconMarginExpanded else iconMarginCollapsed + image.layoutParams = lp + } + } + } + + if (!privacyList.isEmpty()) { + generateContentDescription() + setIcons(builder, iconsContainer) + val lp = iconsContainer.layoutParams as FrameLayout.LayoutParams + lp.gravity = Gravity.CENTER_VERTICAL or + (if (expanded) Gravity.CENTER_HORIZONTAL else Gravity.END) + iconsContainer.layoutParams = lp + } else { + iconsContainer.removeAllViews() + } + requestLayout() + } + + private fun generateContentDescription() { + val typesText = builder.joinTypes() + contentDescription = context.getString( + R.string.ongoing_privacy_chip_content_multiple_apps, typesText) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt new file mode 100644 index 000000000000..1d2e74703b42 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT 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.privacy + +import android.content.Context +import com.android.systemui.R + +class PrivacyChipBuilder(private val context: Context, itemsList: List<PrivacyItem>) { + + val appsAndTypes: List<Pair<PrivacyApplication, List<PrivacyType>>> + val types: List<PrivacyType> + private val separator = context.getString(R.string.ongoing_privacy_dialog_separator) + private val lastSeparator = context.getString(R.string.ongoing_privacy_dialog_last_separator) + + init { + appsAndTypes = itemsList.groupBy({ it.application }, { it.privacyType }) + .toList() + .sortedWith(compareBy({ -it.second.size }, // Sort by number of AppOps + { it.second.min() })) // Sort by "smallest" AppOpp (Location is largest) + types = itemsList.map { it.privacyType }.distinct().sorted() + } + + fun generateIcons() = types.map { it.getIcon(context) } + + private fun <T> List<T>.joinWithAnd(): StringBuilder { + return subList(0, size - 1).joinTo(StringBuilder(), separator = separator).apply { + append(lastSeparator) + append(this@joinWithAnd.last()) + } + } + + fun joinTypes(): String { + return when (types.size) { + 0 -> "" + 1 -> types[0].getName(context) + else -> types.map { it.getName(context) }.joinWithAnd().toString() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt new file mode 100644 index 000000000000..1f24fde1377e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.privacy + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger + +enum class PrivacyChipEvent(private val _id: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "Privacy chip is viewed by the user. Logged at most once per time QS is visible") + ONGOING_INDICATORS_CHIP_VIEW(601), + + @UiEvent(doc = "Privacy chip is clicked") + ONGOING_INDICATORS_CHIP_CLICK(602); + + override fun getId() = _id +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt new file mode 100644 index 000000000000..3da1363f2a56 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT 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.privacy + +import android.content.Context +import com.android.systemui.R + +typealias Privacy = PrivacyType + +enum class PrivacyType(val nameId: Int, val iconId: Int) { + // This is uses the icons used by the corresponding permission groups in the AndroidManifest + TYPE_CAMERA(R.string.privacy_type_camera, + com.android.internal.R.drawable.perm_group_camera), + TYPE_MICROPHONE(R.string.privacy_type_microphone, + com.android.internal.R.drawable.perm_group_microphone), + TYPE_LOCATION(R.string.privacy_type_location, + com.android.internal.R.drawable.perm_group_location); + + fun getName(context: Context) = context.resources.getString(nameId) + + fun getIcon(context: Context) = context.resources.getDrawable(iconId, context.theme) +} + +data class PrivacyItem(val privacyType: PrivacyType, val application: PrivacyApplication) + +data class PrivacyApplication(val packageName: String, val uid: Int) diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt new file mode 100644 index 000000000000..59118bf3534e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.privacy + +import android.app.ActivityManager +import android.app.AppOpsManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.os.UserHandle +import android.os.UserManager +import android.provider.DeviceConfig +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags +import com.android.systemui.Dumpable +import com.android.systemui.appops.AppOpItem +import com.android.systemui.appops.AppOpsController +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.DeviceConfigProxy +import com.android.systemui.util.concurrency.DelayableExecutor +import java.io.FileDescriptor +import java.io.PrintWriter +import java.lang.ref.WeakReference +import java.util.concurrent.Executor +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PrivacyItemController @Inject constructor( + private val appOpsController: AppOpsController, + @Main uiExecutor: DelayableExecutor, + @Background private val bgExecutor: Executor, + private val broadcastDispatcher: BroadcastDispatcher, + private val deviceConfigProxy: DeviceConfigProxy, + private val userManager: UserManager, + dumpManager: DumpManager +) : Dumpable { + + @VisibleForTesting + internal companion object { + val OPS_MIC_CAMERA = intArrayOf(AppOpsManager.OP_CAMERA, + AppOpsManager.OP_PHONE_CALL_CAMERA, AppOpsManager.OP_RECORD_AUDIO, + AppOpsManager.OP_PHONE_CALL_MICROPHONE) + val OPS_LOCATION = intArrayOf( + AppOpsManager.OP_COARSE_LOCATION, + AppOpsManager.OP_FINE_LOCATION) + val OPS = OPS_MIC_CAMERA + OPS_LOCATION + val intentFilter = IntentFilter().apply { + addAction(Intent.ACTION_USER_SWITCHED) + addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) + addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) + } + const val TAG = "PrivacyItemController" + private const val ALL_INDICATORS = + SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED + private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED + } + + @VisibleForTesting + internal var privacyList = emptyList<PrivacyItem>() + @Synchronized get() = field.toList() // Returns a shallow copy of the list + @Synchronized set + + fun isAllIndicatorsEnabled(): Boolean { + return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, + ALL_INDICATORS, false) + } + + private fun isMicCameraEnabled(): Boolean { + return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, + MIC_CAMERA, false) + } + + private var currentUserIds = emptyList<Int>() + private var listening = false + private val callbacks = mutableListOf<WeakReference<Callback>>() + private val internalUiExecutor = MyExecutor(WeakReference(this), uiExecutor) + + private val notifyChanges = Runnable { + val list = privacyList + callbacks.forEach { it.get()?.onPrivacyItemsChanged(list) } + } + + private val updateListAndNotifyChanges = Runnable { + updatePrivacyList() + uiExecutor.execute(notifyChanges) + } + + var allIndicatorsAvailable = isAllIndicatorsEnabled() + private set + var micCameraAvailable = isMicCameraEnabled() + private set + + private val devicePropertiesChangedListener = + object : DeviceConfig.OnPropertiesChangedListener { + override fun onPropertiesChanged(properties: DeviceConfig.Properties) { + if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) && + (properties.keyset.contains(ALL_INDICATORS) || + properties.keyset.contains(MIC_CAMERA))) { + + // Running on the ui executor so can iterate on callbacks + if (properties.keyset.contains(ALL_INDICATORS)) { + allIndicatorsAvailable = properties.getBoolean(ALL_INDICATORS, false) + callbacks.forEach { it.get()?.onFlagAllChanged(allIndicatorsAvailable) } + } + + if (properties.keyset.contains(MIC_CAMERA)) { + micCameraAvailable = properties.getBoolean(MIC_CAMERA, false) + callbacks.forEach { it.get()?.onFlagMicCameraChanged(micCameraAvailable) } + } + internalUiExecutor.updateListeningState() + } + } + } + + private val cb = object : AppOpsController.Callback { + override fun onActiveStateChanged( + code: Int, + uid: Int, + packageName: String, + active: Boolean + ) { + // Check if we care about this code right now + if (!allIndicatorsAvailable && code in OPS_LOCATION) { + return + } + val userId = UserHandle.getUserId(uid) + if (userId in currentUserIds) { + update(false) + } + } + } + + @VisibleForTesting + internal var userSwitcherReceiver = Receiver() + set(value) { + unregisterReceiver() + field = value + if (listening) registerReceiver() + } + + init { + deviceConfigProxy.addOnPropertiesChangedListener( + DeviceConfig.NAMESPACE_PRIVACY, + uiExecutor, + devicePropertiesChangedListener) + dumpManager.registerDumpable(TAG, this) + } + + private fun unregisterReceiver() { + broadcastDispatcher.unregisterReceiver(userSwitcherReceiver) + } + + private fun registerReceiver() { + broadcastDispatcher.registerReceiver(userSwitcherReceiver, intentFilter, + null /* handler */, UserHandle.ALL) + } + + private fun update(updateUsers: Boolean) { + bgExecutor.execute { + if (updateUsers) { + val currentUser = ActivityManager.getCurrentUser() + currentUserIds = userManager.getProfiles(currentUser).map { it.id } + } + updateListAndNotifyChanges.run() + } + } + + /** + * Updates listening status based on whether there are callbacks and the indicators are enabled. + * + * Always listen to all OPS so we don't have to figure out what we should be listening to. We + * still have to filter anyway. Updates are filtered in the callback. + * + * This is only called from private (add/remove)Callback and from the config listener, all in + * main thread. + */ + private fun setListeningState() { + val listen = !callbacks.isEmpty() and (allIndicatorsAvailable || micCameraAvailable) + if (listening == listen) return + listening = listen + if (listening) { + appOpsController.addCallback(OPS, cb) + registerReceiver() + update(true) + } else { + appOpsController.removeCallback(OPS, cb) + unregisterReceiver() + // Make sure that we remove all indicators and notify listeners if we are not + // listening anymore due to indicators being disabled + update(false) + } + } + + private fun addCallback(callback: WeakReference<Callback>) { + callbacks.add(callback) + if (callbacks.isNotEmpty() && !listening) { + internalUiExecutor.updateListeningState() + } + // Notify this callback if we didn't set to listening + else if (listening) { + internalUiExecutor.execute(NotifyChangesToCallback(callback.get(), privacyList)) + } + } + + private fun removeCallback(callback: WeakReference<Callback>) { + // Removes also if the callback is null + callbacks.removeIf { it.get()?.equals(callback.get()) ?: true } + if (callbacks.isEmpty()) { + internalUiExecutor.updateListeningState() + } + } + + fun addCallback(callback: Callback) { + internalUiExecutor.addCallback(callback) + } + + fun removeCallback(callback: Callback) { + internalUiExecutor.removeCallback(callback) + } + + private fun updatePrivacyList() { + if (!listening) { + privacyList = emptyList() + return + } + val list = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) } + .mapNotNull { toPrivacyItem(it) }.distinct() + privacyList = list + } + + private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? { + val type: PrivacyType = when (appOpItem.code) { + AppOpsManager.OP_PHONE_CALL_CAMERA, + AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA + AppOpsManager.OP_COARSE_LOCATION, + AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION + AppOpsManager.OP_PHONE_CALL_MICROPHONE, + AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE + else -> return null + } + if (type == PrivacyType.TYPE_LOCATION && !allIndicatorsAvailable) return null + val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid) + return PrivacyItem(type, app) + } + + // Used by containing class to get notified of changes + interface Callback { + fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) + + @JvmDefault + fun onFlagAllChanged(flag: Boolean) {} + + @JvmDefault + fun onFlagMicCameraChanged(flag: Boolean) {} + } + + internal inner class Receiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intentFilter.hasAction(intent.action)) { + update(true) + } + } + } + + private class NotifyChangesToCallback( + private val callback: Callback?, + private val list: List<PrivacyItem> + ) : Runnable { + override fun run() { + callback?.onPrivacyItemsChanged(list) + } + } + + override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { + pw.println("PrivacyItemController state:") + pw.println(" Listening: $listening") + pw.println(" Current user ids: $currentUserIds") + pw.println(" Privacy Items:") + privacyList.forEach { + pw.print(" ") + pw.println(it.toString()) + } + pw.println(" Callbacks:") + callbacks.forEach { + it.get()?.let { + pw.print(" ") + pw.println(it.toString()) + } + } + } + + private class MyExecutor( + private val outerClass: WeakReference<PrivacyItemController>, + private val delegate: DelayableExecutor + ) : Executor { + + private var listeningCanceller: Runnable? = null + + override fun execute(command: Runnable) { + delegate.execute(command) + } + + fun updateListeningState() { + listeningCanceller?.run() + listeningCanceller = delegate.executeDelayed({ + outerClass.get()?.setListeningState() + }, 0L) + } + + fun addCallback(callback: Callback) { + outerClass.get()?.addCallback(WeakReference(callback)) + } + + fun removeCallback(callback: Callback) { + outerClass.get()?.removeCallback(WeakReference(callback)) + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 3eed8ad89075..44803aedc4ea 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -177,6 +177,16 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { } @Override + public void endFakeDrag() { + try { + super.endFakeDrag(); + } catch (NullPointerException e) { + // Not sure what's going on. Let's log it + Log.e(TAG, "endFakeDrag called without velocityTracker", e); + } + } + + @Override public void computeScroll() { if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { if (!isFakeDragging()) { @@ -185,7 +195,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { fakeDragBy(getScrollX() - mScroller.getCurrX()); } else if (isFakeDragging()) { endFakeDrag(); - mBounceAnimatorSet.start(); + if (mBounceAnimatorSet != null) { + mBounceAnimatorSet.start(); + } setOffscreenPageLimit(1); } super.computeScroll(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java index c4bb4e86e41e..6e4ab9a3323a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFooterImpl.java @@ -78,6 +78,8 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, private SettingsButton mSettingsButton; protected View mSettingsContainer; private PageIndicator mPageIndicator; + private TextView mBuildText; + private boolean mShouldShowBuildText; private boolean mQsDisabled; private QSPanel mQsPanel; @@ -147,6 +149,7 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mActionsContainer = findViewById(R.id.qs_footer_actions_container); mEditContainer = findViewById(R.id.qs_footer_actions_edit_container); + mBuildText = findViewById(R.id.build); // RenderThread is doing more harm than good when touching the header (to expand quick // settings), so disable it for this view @@ -162,16 +165,19 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, } private void setBuildText() { - TextView v = findViewById(R.id.build); - if (v == null) return; + if (mBuildText == null) return; if (DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(mContext)) { - v.setText(mContext.getString( + mBuildText.setText(mContext.getString( com.android.internal.R.string.bugreport_status, Build.VERSION.RELEASE_OR_CODENAME, Build.ID)); - v.setVisibility(View.VISIBLE); + // Set as selected for marquee before its made visible, then it won't be announced when + // it's made visible. + mBuildText.setSelected(true); + mShouldShowBuildText = true; } else { - v.setVisibility(View.GONE); + mShouldShowBuildText = false; + mBuildText.setSelected(false); } } @@ -321,6 +327,8 @@ public class QSFooterImpl extends FrameLayout implements QSFooter, mMultiUserSwitch.setVisibility(showUserSwitcher() ? View.VISIBLE : View.INVISIBLE); mEditContainer.setVisibility(isDemo || !mExpanded ? View.INVISIBLE : View.VISIBLE); mSettingsButton.setVisibility(isDemo || !mExpanded ? View.INVISIBLE : View.VISIBLE); + + mBuildText.setVisibility(mExpanded && mShouldShowBuildText ? View.VISIBLE : View.GONE); } private boolean showUserSwitcher() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index b07b1a9561ff..58c723c58daa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -31,6 +31,7 @@ import android.graphics.Color; import android.graphics.Rect; import android.media.AudioManager; import android.os.Handler; +import android.os.Looper; import android.provider.AlarmClock; import android.provider.Settings; import android.service.notification.ZenModeConfig; @@ -46,7 +47,9 @@ import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.RelativeLayout; +import android.widget.Space; import android.widget.TextView; import androidx.annotation.NonNull; @@ -55,6 +58,7 @@ import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; +import com.android.internal.logging.UiEventLogger; import com.android.settingslib.Utils; import com.android.systemui.BatteryMeterView; import com.android.systemui.DualToneHandler; @@ -63,6 +67,11 @@ import com.android.systemui.R; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.privacy.OngoingPrivacyChip; +import com.android.systemui.privacy.PrivacyChipBuilder; +import com.android.systemui.privacy.PrivacyChipEvent; +import com.android.systemui.privacy.PrivacyItem; +import com.android.systemui.privacy.PrivacyItemController; import com.android.systemui.qs.QSDetail.Callback; import com.android.systemui.qs.carrier.QSCarrierGroup; import com.android.systemui.statusbar.CommandQueue; @@ -101,7 +110,6 @@ public class QuickStatusBarHeader extends RelativeLayout implements private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0; public static final int MAX_TOOLTIP_SHOWN_COUNT = 2; - private final Handler mHandler = new Handler(); private final NextAlarmController mAlarmController; private final ZenModeController mZenController; private final StatusBarIconController mStatusBarIconController; @@ -140,9 +148,15 @@ public class QuickStatusBarHeader extends RelativeLayout implements private View mRingerContainer; private Clock mClockView; private DateView mDateView; + private OngoingPrivacyChip mPrivacyChip; + private Space mSpace; private BatteryMeterView mBatteryRemainingIcon; private RingerModeTracker mRingerModeTracker; + private boolean mAllIndicatorsEnabled; + private boolean mMicCameraIndicatorsEnabled; + private PrivacyItemController mPrivacyItemController; + private final UiEventLogger mUiEventLogger; // Used for RingerModeTracker private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this); @@ -156,22 +170,56 @@ public class QuickStatusBarHeader extends RelativeLayout implements private int mCutOutPaddingRight; private float mExpandedHeaderAlpha = 1.0f; private float mKeyguardExpansionFraction; + private boolean mPrivacyChipLogged = false; + + private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() { + @Override + public void onPrivacyItemsChanged(List<PrivacyItem> privacyItems) { + mPrivacyChip.setPrivacyList(privacyItems); + setChipVisibility(!privacyItems.isEmpty()); + } + + @Override + public void onFlagAllChanged(boolean flag) { + if (mAllIndicatorsEnabled != flag) { + mAllIndicatorsEnabled = flag; + update(); + } + } + + @Override + public void onFlagMicCameraChanged(boolean flag) { + if (mMicCameraIndicatorsEnabled != flag) { + mMicCameraIndicatorsEnabled = flag; + update(); + } + } + + private void update() { + StatusIconContainer iconContainer = requireViewById(R.id.statusIcons); + iconContainer.setIgnoredSlots(getIgnoredIconSlots()); + setChipVisibility(!mPrivacyChip.getPrivacyList().isEmpty()); + } + }; @Inject public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs, NextAlarmController nextAlarmController, ZenModeController zenModeController, StatusBarIconController statusBarIconController, - ActivityStarter activityStarter, - CommandQueue commandQueue, RingerModeTracker ringerModeTracker) { + ActivityStarter activityStarter, PrivacyItemController privacyItemController, + CommandQueue commandQueue, RingerModeTracker ringerModeTracker, + UiEventLogger uiEventLogger) { super(context, attrs); mAlarmController = nextAlarmController; mZenController = zenModeController; mStatusBarIconController = statusBarIconController; mActivityStarter = activityStarter; + mPrivacyItemController = privacyItemController; mDualToneHandler = new DualToneHandler( new ContextThemeWrapper(context, R.style.QSHeaderTheme)); mCommandQueue = commandQueue; mRingerModeTracker = ringerModeTracker; + mUiEventLogger = uiEventLogger; } @Override @@ -198,8 +246,11 @@ public class QuickStatusBarHeader extends RelativeLayout implements mRingerModeTextView = findViewById(R.id.ringer_mode_text); mRingerContainer = findViewById(R.id.ringer_container); mRingerContainer.setOnClickListener(this::onClick); + mPrivacyChip = findViewById(R.id.privacy_chip); + mPrivacyChip.setOnClickListener(this::onClick); mCarrierGroup = findViewById(R.id.carrier_group); + updateResources(); Rect tintArea = new Rect(0, 0, 0, 0); @@ -219,6 +270,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements mClockView = findViewById(R.id.clock); mClockView.setOnClickListener(this); mDateView = findViewById(R.id.date); + mSpace = findViewById(R.id.space); // Tint for the battery icons are handled in setupHost() mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon); @@ -229,6 +281,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE); mRingerModeTextView.setSelected(true); mNextAlarmTextView.setSelected(true); + + mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); + mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); } public QuickQSPanel getHeaderQsPanel() { @@ -237,10 +292,16 @@ public class QuickStatusBarHeader extends RelativeLayout implements private List<String> getIgnoredIconSlots() { ArrayList<String> ignored = new ArrayList<>(); - ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_camera)); - ignored.add(mContext.getResources().getString( - com.android.internal.R.string.status_bar_microphone)); + if (getChipEnabled()) { + ignored.add(mContext.getResources().getString( + com.android.internal.R.string.status_bar_camera)); + ignored.add(mContext.getResources().getString( + com.android.internal.R.string.status_bar_microphone)); + if (mAllIndicatorsEnabled) { + ignored.add(mContext.getResources().getString( + com.android.internal.R.string.status_bar_location)); + } + } return ignored; } @@ -256,6 +317,20 @@ public class QuickStatusBarHeader extends RelativeLayout implements } } + private void setChipVisibility(boolean chipVisible) { + if (chipVisible && getChipEnabled()) { + mPrivacyChip.setVisibility(View.VISIBLE); + // Makes sure that the chip is logged as viewed at most once each time QS is opened + // mListening makes sure that the callback didn't return after the user closed QS + if (!mPrivacyChipLogged && mListening) { + mPrivacyChipLogged = true; + mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW); + } + } else { + mPrivacyChip.setVisibility(View.GONE); + } + } + private boolean updateRingerStatus() { boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE; CharSequence originalRingerText = mRingerModeTextView.getText(); @@ -363,6 +438,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements updateStatusIconAlphaAnimator(); updateHeaderTextContainerAlphaAnimator(); + updatePrivacyChipAlphaAnimator(); } private void updateStatusIconAlphaAnimator() { @@ -377,6 +453,12 @@ public class QuickStatusBarHeader extends RelativeLayout implements .build(); } + private void updatePrivacyChipAlphaAnimator() { + mPrivacyChipAlphaAnimator = new TouchAnimator.Builder() + .addFloat(mPrivacyChip, "alpha", 1, 0, 1) + .build(); + } + public void setExpanded(boolean expanded) { if (mExpanded == expanded) return; mExpanded = expanded; @@ -415,6 +497,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements mHeaderTextContainerView.setVisibility(INVISIBLE); } } + if (mPrivacyChipAlphaAnimator != null) { + mPrivacyChip.setExpanded(expansionFraction > 0.5); + mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction); + } if (expansionFraction < 1 && expansionFraction > 0.99) { if (mHeaderQsPanel.switchTileLayout()) { updateResources(); @@ -453,6 +539,31 @@ public class QuickStatusBarHeader extends RelativeLayout implements Pair<Integer, Integer> padding = StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner( cutout, cornerCutoutPadding, -1); + if (padding == null) { + mSystemIconsView.setPaddingRelative( + getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0, + getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0); + } else { + mSystemIconsView.setPadding(padding.first, 0, padding.second, 0); + + } + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpace.getLayoutParams(); + boolean cornerCutout = cornerCutoutPadding != null + && (cornerCutoutPadding.first == 0 || cornerCutoutPadding.second == 0); + if (cutout != null) { + Rect topCutout = cutout.getBoundingRectTop(); + if (topCutout.isEmpty() || cornerCutout) { + mHasTopCutout = false; + lp.width = 0; + mSpace.setVisibility(View.GONE); + } else { + mHasTopCutout = true; + lp.width = topCutout.width(); + mSpace.setVisibility(View.VISIBLE); + } + } + mSpace.setLayoutParams(lp); + setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE); mCutOutPaddingLeft = padding.first; mCutOutPaddingRight = padding.second; mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top; @@ -513,10 +624,16 @@ public class QuickStatusBarHeader extends RelativeLayout implements mZenController.addCallback(this); mAlarmController.addCallback(this); mLifecycle.setCurrentState(Lifecycle.State.RESUMED); + // Get the most up to date info + mAllIndicatorsEnabled = mPrivacyItemController.getAllIndicatorsAvailable(); + mMicCameraIndicatorsEnabled = mPrivacyItemController.getMicCameraAvailable(); + mPrivacyItemController.addCallback(mPICCallback); } else { mZenController.removeCallback(this); mAlarmController.removeCallback(this); mLifecycle.setCurrentState(Lifecycle.State.CREATED); + mPrivacyItemController.removeCallback(mPICCallback); + mPrivacyChipLogged = false; } } @@ -534,6 +651,17 @@ public class QuickStatusBarHeader extends RelativeLayout implements mActivityStarter.postStartActivityDismissingKeyguard(new Intent( AlarmClock.ACTION_SHOW_ALARMS), 0); } + } else if (v == mPrivacyChip) { + // Makes sure that the builder is grabbed as soon as the chip is pressed + PrivacyChipBuilder builder = mPrivacyChip.getBuilder(); + if (builder.getAppsAndTypes().size() == 0) return; + Handler mUiHandler = new Handler(Looper.getMainLooper()); + mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK); + mUiHandler.post(() -> { + mActivityStarter.postStartActivityDismissingKeyguard( + new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0); + mHost.collapsePanels(); + }); } else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) { mActivityStarter.postStartActivityDismissingKeyguard(new Intent( Settings.ACTION_SOUND_SETTINGS), 0); @@ -640,4 +768,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements updateHeaderTextContainerAlphaAnimator(); } } + + private boolean getChipEnabled() { + return mMicCameraIndicatorsEnabled || mAllIndicatorsEnabled; + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index e738cec4962a..bffeb3ec3c70 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -14,11 +14,8 @@ package com.android.systemui.qs.customize; -import android.app.AlertDialog; -import android.app.AlertDialog.Builder; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.drawable.Drawable; @@ -28,10 +25,11 @@ import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLayoutChangeListener; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup; import androidx.recyclerview.widget.ItemTouchHelper; @@ -49,7 +47,6 @@ import com.android.systemui.qs.customize.TileQueryHelper.TileInfo; import com.android.systemui.qs.customize.TileQueryHelper.TileStateListener; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSIconViewImpl; -import com.android.systemui.statusbar.phone.SystemUIDialog; import java.util.ArrayList; import java.util.List; @@ -78,10 +75,10 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta private final List<TileInfo> mTiles = new ArrayList<>(); private final ItemTouchHelper mItemTouchHelper; private final ItemDecoration mDecoration; - private final AccessibilityManager mAccessibilityManager; private final int mMinNumTiles; private int mEditIndex; private int mTileDividerIndex; + private int mFocusIndex; private boolean mNeedsFocus; private List<String> mCurrentSpecs; private List<TileInfo> mOtherTiles; @@ -90,17 +87,28 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta private Holder mCurrentDrag; private int mAccessibilityAction = ACTION_NONE; private int mAccessibilityFromIndex; - private CharSequence mAccessibilityFromLabel; private QSTileHost mHost; private final UiEventLogger mUiEventLogger; + private final AccessibilityDelegateCompat mAccessibilityDelegate; + private RecyclerView mRecyclerView; public TileAdapter(Context context, UiEventLogger uiEventLogger) { mContext = context; mUiEventLogger = uiEventLogger; - mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mItemTouchHelper = new ItemTouchHelper(mCallbacks); mDecoration = new TileItemDecoration(context); mMinNumTiles = context.getResources().getInteger(R.integer.quick_settings_min_num_tiles); + mAccessibilityDelegate = new TileAdapterDelegate(); + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + mRecyclerView = recyclerView; + } + + @Override + public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) { + mRecyclerView = null; } public void setHost(QSTileHost host) { @@ -130,7 +138,6 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta // Remove blank tile from last spot mTiles.remove(--mEditIndex); // Update the tile divider position - mTileDividerIndex--; notifyDataSetChanged(); } mAccessibilityAction = ACTION_NONE; @@ -241,14 +248,12 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta } private void setSelectableForHeaders(View view) { - if (mAccessibilityManager.isTouchExplorationEnabled()) { - final boolean selectable = mAccessibilityAction == ACTION_NONE; - view.setFocusable(selectable); - view.setImportantForAccessibility(selectable - ? View.IMPORTANT_FOR_ACCESSIBILITY_YES - : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - view.setFocusableInTouchMode(selectable); - } + final boolean selectable = mAccessibilityAction == ACTION_NONE; + view.setFocusable(selectable); + view.setImportantForAccessibility(selectable + ? View.IMPORTANT_FOR_ACCESSIBILITY_YES + : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + view.setFocusableInTouchMode(selectable); } @Override @@ -285,12 +290,11 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta holder.mTileView.setVisibility(View.VISIBLE); holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); holder.mTileView.setContentDescription(mContext.getString( - R.string.accessibility_qs_edit_tile_add, mAccessibilityFromLabel, - position)); + R.string.accessibility_qs_edit_tile_add_to_position, position)); holder.mTileView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - selectPosition(holder.getAdapterPosition(), v); + selectPosition(holder.getLayoutPosition()); } }); focusOnHolder(holder); @@ -299,54 +303,49 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta TileInfo info = mTiles.get(position); - if (position > mEditIndex) { - info.state.contentDescription = mContext.getString( - R.string.accessibility_qs_edit_add_tile_label, info.state.label); - } else if (mAccessibilityAction == ACTION_ADD) { + final boolean selectable = 0 < position && position < mEditIndex; + if (selectable && mAccessibilityAction == ACTION_ADD) { info.state.contentDescription = mContext.getString( - R.string.accessibility_qs_edit_tile_add, mAccessibilityFromLabel, position); - } else if (mAccessibilityAction == ACTION_MOVE) { + R.string.accessibility_qs_edit_tile_add_to_position, position); + } else if (selectable && mAccessibilityAction == ACTION_MOVE) { info.state.contentDescription = mContext.getString( - R.string.accessibility_qs_edit_tile_move, mAccessibilityFromLabel, position); + R.string.accessibility_qs_edit_tile_move_to_position, position); } else { - info.state.contentDescription = mContext.getString( - R.string.accessibility_qs_edit_tile_label, position, info.state.label); + info.state.contentDescription = info.state.label; } + info.state.expandedAccessibilityClassName = ""; + holder.mTileView.handleStateChanged(info.state); holder.mTileView.setShowAppLabel(position > mEditIndex && !info.isSystem); + holder.mTileView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); + holder.mTileView.setClickable(true); + holder.mTileView.setOnClickListener(null); + holder.mTileView.setFocusable(true); + holder.mTileView.setFocusableInTouchMode(true); - if (mAccessibilityManager.isTouchExplorationEnabled()) { - final boolean selectable = mAccessibilityAction == ACTION_NONE || position < mEditIndex; + if (mAccessibilityAction != ACTION_NONE) { holder.mTileView.setClickable(selectable); holder.mTileView.setFocusable(selectable); + holder.mTileView.setFocusableInTouchMode(selectable); holder.mTileView.setImportantForAccessibility(selectable ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); - holder.mTileView.setFocusableInTouchMode(selectable); if (selectable) { holder.mTileView.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - int position = holder.getAdapterPosition(); + int position = holder.getLayoutPosition(); if (position == RecyclerView.NO_POSITION) return; if (mAccessibilityAction != ACTION_NONE) { - selectPosition(position, v); - } else { - if (position < mEditIndex && canRemoveTiles()) { - showAccessibilityDialog(position, v); - } else if (position < mEditIndex && !canRemoveTiles()) { - startAccessibleMove(position); - } else { - startAccessibleAdd(position); - } + selectPosition(position); } } }); - if (position == mAccessibilityFromIndex) { - focusOnHolder(holder); - } } } + if (position == mFocusIndex) { + focusOnHolder(holder); + } } private void focusOnHolder(Holder holder) { @@ -360,9 +359,13 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta int oldLeft, int oldTop, int oldRight, int oldBottom) { holder.mTileView.removeOnLayoutChangeListener(this); holder.mTileView.requestFocus(); + if (mAccessibilityAction == ACTION_NONE) { + holder.mTileView.clearFocus(); + } } }); mNeedsFocus = false; + mFocusIndex = RecyclerView.NO_POSITION; } } @@ -370,72 +373,77 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta return mCurrentSpecs.size() > mMinNumTiles; } - private void selectPosition(int position, View v) { + private void selectPosition(int position) { if (mAccessibilityAction == ACTION_ADD) { // Remove the placeholder. mTiles.remove(mEditIndex--); - notifyItemRemoved(mEditIndex); } mAccessibilityAction = ACTION_NONE; - move(mAccessibilityFromIndex, position, v); + move(mAccessibilityFromIndex, position, false); + mFocusIndex = position; + mNeedsFocus = true; notifyDataSetChanged(); } - private void showAccessibilityDialog(final int position, final View v) { - final TileInfo info = mTiles.get(position); - CharSequence[] options = new CharSequence[] { - mContext.getString(R.string.accessibility_qs_edit_move_tile, info.state.label), - mContext.getString(R.string.accessibility_qs_edit_remove_tile, info.state.label), - }; - AlertDialog dialog = new Builder(mContext) - .setItems(options, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == 0) { - startAccessibleMove(position); - } else { - move(position, info.isSystem ? mEditIndex : mTileDividerIndex, v); - notifyItemChanged(mTileDividerIndex); - notifyDataSetChanged(); - } - } - }).setNegativeButton(android.R.string.cancel, null) - .create(); - SystemUIDialog.setShowForAllUsers(dialog, true); - SystemUIDialog.applyFlags(dialog); - dialog.show(); - } - private void startAccessibleAdd(int position) { mAccessibilityFromIndex = position; - mAccessibilityFromLabel = mTiles.get(position).state.label; mAccessibilityAction = ACTION_ADD; // Add placeholder for last slot. mTiles.add(mEditIndex++, null); // Update the tile divider position mTileDividerIndex++; + mFocusIndex = mEditIndex - 1; mNeedsFocus = true; + if (mRecyclerView != null) { + mRecyclerView.post(() -> mRecyclerView.smoothScrollToPosition(mFocusIndex)); + } notifyDataSetChanged(); } private void startAccessibleMove(int position) { mAccessibilityFromIndex = position; - mAccessibilityFromLabel = mTiles.get(position).state.label; mAccessibilityAction = ACTION_MOVE; + mFocusIndex = position; mNeedsFocus = true; notifyDataSetChanged(); } + private boolean canRemoveFromPosition(int position) { + return canRemoveTiles() && isCurrentTile(position); + } + + private boolean isCurrentTile(int position) { + return position < mEditIndex; + } + + private boolean canAddFromPosition(int position) { + return position > mEditIndex; + } + + private void addFromPosition(int position) { + if (!canAddFromPosition(position)) return; + move(position, mEditIndex); + } + + private void removeFromPosition(int position) { + if (!canRemoveFromPosition(position)) return; + TileInfo info = mTiles.get(position); + move(position, info.isSystem ? mEditIndex : mTileDividerIndex); + } + public SpanSizeLookup getSizeLookup() { return mSizeLookup; } - private boolean move(int from, int to, View v) { + private boolean move(int from, int to) { + return move(from, to, true); + } + + private boolean move(int from, int to, boolean notify) { if (to == from) { return true; } - CharSequence fromLabel = mTiles.get(from).state.label; - move(from, to, mTiles); + move(from, to, mTiles, notify); updateDividerLocations(); if (to >= mEditIndex) { mUiEventLogger.log(QSEditEvent.QS_EDIT_REMOVE, 0, strip(mTiles.get(to))); @@ -477,9 +485,11 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta return spec; } - private <T> void move(int from, int to, List<T> list) { + private <T> void move(int from, int to, List<T> list, boolean notify) { list.add(to, list.remove(from)); - notifyItemMoved(from, to); + if (notify) { + notifyItemMoved(from, to); + } } public class Holder extends ViewHolder { @@ -491,6 +501,8 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta mTileView = (CustomizeTileView) ((FrameLayout) itemView).getChildAt(0); mTileView.setBackground(null); mTileView.getIcon().disableAnimation(); + mTileView.setTag(this); + ViewCompat.setAccessibilityDelegate(mTileView, mAccessibilityDelegate); } } @@ -527,6 +539,46 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta .setDuration(DRAG_LENGTH) .alpha(.6f); } + + boolean canRemove() { + return canRemoveFromPosition(getLayoutPosition()); + } + + boolean canAdd() { + return canAddFromPosition(getLayoutPosition()); + } + + void toggleState() { + if (canAdd()) { + add(); + } else { + remove(); + } + } + + private void add() { + addFromPosition(getLayoutPosition()); + } + + private void remove() { + removeFromPosition(getLayoutPosition()); + } + + boolean isCurrentTile() { + return TileAdapter.this.isCurrentTile(getLayoutPosition()); + } + + void startAccessibleAdd() { + TileAdapter.this.startAccessibleAdd(getLayoutPosition()); + } + + void startAccessibleMove() { + TileAdapter.this.startAccessibleMove(getLayoutPosition()); + } + + boolean canTakeAccessibleAction() { + return mAccessibilityAction == ACTION_NONE; + } } private final SpanSizeLookup mSizeLookup = new SpanSizeLookup() { @@ -648,7 +700,7 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta to == 0 || to == RecyclerView.NO_POSITION) { return false; } - return move(from, to, target.itemView); + return move(from, to); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java new file mode 100644 index 000000000000..1e426adac0b8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapterDelegate.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.customize; + +import android.os.Bundle; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + +import com.android.systemui.R; + +import java.util.List; + +/** + * Accessibility delegate for {@link TileAdapter} views. + * + * This delegate will populate the accessibility info with the proper actions that can be taken for + * the different tiles: + * <ul> + * <li>Add to end if the tile is not a current tile (by double tap).</li> + * <li>Add to a given position (by context menu). This will let the user select a position.</li> + * <li>Remove, if the tile is a current tile (by double tap).</li> + * <li>Move to a given position (by context menu). This will let the user select a position.</li> + * </ul> + * + * This only handles generating the associated actions. The logic for selecting positions is handled + * by {@link TileAdapter}. + * + * In order for the delegate to work properly, the asociated {@link TileAdapter.Holder} should be + * passed along with the view using {@link View#setTag}. + */ +class TileAdapterDelegate extends AccessibilityDelegateCompat { + + private static final int MOVE_TO_POSITION_ID = R.id.accessibility_action_qs_move_to_position; + private static final int ADD_TO_POSITION_ID = R.id.accessibility_action_qs_add_to_position; + + private TileAdapter.Holder getHolder(View view) { + return (TileAdapter.Holder) view.getTag(); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + TileAdapter.Holder holder = getHolder(host); + info.setCollectionItemInfo(null); + info.setStateDescription(""); + if (holder == null || !holder.canTakeAccessibleAction()) { + // If there's not a holder (not a regular Tile) or an action cannot be taken + // because we are in the middle of an accessibility action, don't create a special node. + return; + } + + addClickAction(host, info, holder); + maybeAddActionAddToPosition(host, info, holder); + maybeAddActionMoveToPosition(host, info, holder); + + if (holder.isCurrentTile()) { + info.setStateDescription(host.getContext().getString( + R.string.accessibility_qs_edit_position, holder.getLayoutPosition())); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + TileAdapter.Holder holder = getHolder(host); + + if (holder == null || !holder.canTakeAccessibleAction()) { + // If there's not a holder (not a regular Tile) or an action cannot be taken + // because we are in the middle of an accessibility action, perform the default action. + return super.performAccessibilityAction(host, action, args); + } + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + holder.toggleState(); + return true; + } else if (action == MOVE_TO_POSITION_ID) { + holder.startAccessibleMove(); + return true; + } else if (action == ADD_TO_POSITION_ID) { + holder.startAccessibleAdd(); + return true; + } else { + return super.performAccessibilityAction(host, action, args); + } + } + + private void addClickAction( + View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) { + String clickActionString; + if (holder.canAdd()) { + clickActionString = host.getContext().getString( + R.string.accessibility_qs_edit_tile_add_action); + } else if (holder.canRemove()) { + clickActionString = host.getContext().getString( + R.string.accessibility_qs_edit_remove_tile_action); + } else { + // Remove the default click action if tile can't either be added or removed (for example + // if there's the minimum number of tiles) + List<AccessibilityNodeInfoCompat.AccessibilityActionCompat> listOfActions = + info.getActionList(); // This is a copy + int numActions = listOfActions.size(); + for (int i = 0; i < numActions; i++) { + if (listOfActions.get(i).getId() == AccessibilityNodeInfo.ACTION_CLICK) { + info.removeAction(listOfActions.get(i)); + } + } + return; + } + + AccessibilityNodeInfoCompat.AccessibilityActionCompat action = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + AccessibilityNodeInfo.ACTION_CLICK, clickActionString); + info.addAction(action); + } + + private void maybeAddActionMoveToPosition( + View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) { + if (holder.isCurrentTile()) { + AccessibilityNodeInfoCompat.AccessibilityActionCompat action = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat(MOVE_TO_POSITION_ID, + host.getContext().getString( + R.string.accessibility_qs_edit_tile_start_move)); + info.addAction(action); + } + } + + private void maybeAddActionAddToPosition( + View host, AccessibilityNodeInfoCompat info, TileAdapter.Holder holder) { + if (holder.canAdd()) { + AccessibilityNodeInfoCompat.AccessibilityActionCompat action = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat(ADD_TO_POSITION_ID, + host.getContext().getString( + R.string.accessibility_qs_edit_tile_start_add)); + info.addAction(action); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java index f2495048bf26..7150e4350304 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java @@ -111,6 +111,7 @@ public class BatterySaverTile extends QSTileImpl<BooleanState> implements : mPowerSave ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; state.icon = mIcon; state.label = mContext.getString(R.string.battery_detail_switch_title); + state.secondaryLabel = ""; state.contentDescription = state.label; state.value = mPowerSave; state.expandedAccessibilityClassName = Switch.class.getName(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java index 077c7aacd7fb..33e98d836d94 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CellularTile.java @@ -118,7 +118,8 @@ public class CellularTile extends QSTileImpl<SignalState> { return; } String carrierName = mController.getMobileDataNetworkName(); - if (TextUtils.isEmpty(carrierName)) { + boolean isInService = mController.isMobileDataNetworkInService(); + if (TextUtils.isEmpty(carrierName) || !isInService) { carrierName = mContext.getString(R.string.mobile_data_disable_message_default_carrier); } AlertDialog dialog = new Builder(mContext) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java index 2f582727c766..acd2846fef3c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NightDisplayTile.java @@ -60,7 +60,7 @@ public class NightDisplayTile extends QSTileImpl<BooleanState> implements private static final String PATTERN_HOUR_MINUTE = "h:mm a"; private static final String PATTERN_HOUR_NINUTE_24 = "HH:mm"; - private final ColorDisplayManager mManager; + private ColorDisplayManager mManager; private final LocationController mLocationController; private NightDisplayListener mListener; private boolean mIsListening; @@ -105,6 +105,8 @@ public class NightDisplayTile extends QSTileImpl<BooleanState> implements mListener.setCallback(null); } + mManager = getHost().getUserContext().getSystemService(ColorDisplayManager.class); + // Make a new controller for the new user. mListener = new NightDisplayListener(mContext, newUserId, new Handler(Looper.myLooper())); if (mIsListening) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java index 7b83c20d4b86..f777553bb5fe 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UiModeNightTile.java @@ -50,16 +50,15 @@ public class UiModeNightTile extends QSTileImpl<QSTile.BooleanState> implements public static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a"); private final Icon mIcon = ResourceIcon.get( com.android.internal.R.drawable.ic_qs_ui_mode_night); - private final UiModeManager mUiModeManager; + private UiModeManager mUiModeManager; private final BatteryController mBatteryController; private final LocationController mLocationController; - @Inject public UiModeNightTile(QSHost host, ConfigurationController configurationController, BatteryController batteryController, LocationController locationController) { super(host); mBatteryController = batteryController; - mUiModeManager = mContext.getSystemService(UiModeManager.class); + mUiModeManager = host.getUserContext().getSystemService(UiModeManager.class); mLocationController = locationController; configurationController.observe(getLifecycle(), this); batteryController.observe(getLifecycle(), this); diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java index 476ec798a35f..8ec3db59117d 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java @@ -21,7 +21,6 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; @@ -42,6 +41,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.LongRunning; import com.android.systemui.settings.CurrentUserContextTracker; +import com.android.systemui.statusbar.phone.KeyguardDismissUtil; import java.io.IOException; import java.util.concurrent.Executor; @@ -69,10 +69,9 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis private static final String ACTION_STOP_NOTIF = "com.android.systemui.screenrecord.STOP_FROM_NOTIF"; private static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE"; - private static final String ACTION_DELETE = "com.android.systemui.screenrecord.DELETE"; private final RecordingController mController; - + private final KeyguardDismissUtil mKeyguardDismissUtil; private ScreenRecordingAudioSource mAudioSource; private boolean mShowTaps; private boolean mOriginalShowTaps; @@ -85,12 +84,13 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis @Inject public RecordingService(RecordingController controller, @LongRunning Executor executor, UiEventLogger uiEventLogger, NotificationManager notificationManager, - CurrentUserContextTracker userContextTracker) { + CurrentUserContextTracker userContextTracker, KeyguardDismissUtil keyguardDismissUtil) { mController = controller; mLongExecutor = executor; mUiEventLogger = uiEventLogger; mNotificationManager = notificationManager; mUserContextTracker = userContextTracker; + mKeyguardDismissUtil = keyguardDismissUtil; } /** @@ -170,33 +170,17 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis Intent shareIntent = new Intent(Intent.ACTION_SEND) .setType("video/mp4") .putExtra(Intent.EXTRA_STREAM, shareUri); - String shareLabel = getResources().getString(R.string.screenrecord_share_label); - - // Close quick shade - sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - - // Remove notification - mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); + mKeyguardDismissUtil.executeWhenUnlocked(() -> { + String shareLabel = getResources().getString(R.string.screenrecord_share_label); + startActivity(Intent.createChooser(shareIntent, shareLabel) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + // Remove notification + mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); + return false; + }, false); - startActivity(Intent.createChooser(shareIntent, shareLabel) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); - break; - case ACTION_DELETE: // Close quick shade sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); - - ContentResolver resolver = getContentResolver(); - Uri uri = Uri.parse(intent.getStringExtra(EXTRA_PATH)); - resolver.delete(uri, null, null); - - Toast.makeText( - this, - R.string.screenrecord_delete_description, - Toast.LENGTH_LONG).show(); - - // Remove notification - mNotificationManager.cancelAsUser(null, NOTIFICATION_VIEW_ID, currentUser); - Log.d(TAG, "Deleted recording " + uri); break; } return Service.START_STICKY; @@ -307,16 +291,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) .build(); - Notification.Action deleteAction = new Notification.Action.Builder( - Icon.createWithResource(this, R.drawable.ic_screenrecord), - getResources().getString(R.string.screenrecord_delete_label), - PendingIntent.getService( - this, - REQUEST_CODE, - getDeleteIntent(this, uri.toString()), - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE)) - .build(); - Bundle extras = new Bundle(); extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, getResources().getString(R.string.screenrecord_name)); @@ -330,7 +304,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis viewIntent, PendingIntent.FLAG_IMMUTABLE)) .addAction(shareAction) - .addAction(deleteAction) .setAutoCancel(true) .addExtras(extras); @@ -409,11 +382,6 @@ public class RecordingService extends Service implements MediaRecorder.OnInfoLis .putExtra(EXTRA_PATH, path); } - private static Intent getDeleteIntent(Context context, String path) { - return new Intent(context, RecordingService.class).setAction(ACTION_DELETE) - .putExtra(EXTRA_PATH, path); - } - @Override public void onInfo(MediaRecorder mr, int what, int extra) { Log.d(TAG, "Media recorder info: " + what); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java new file mode 100644 index 000000000000..3fd7f94514f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ActionProxyReceiver.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.screenshot; + +import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_EDIT; +import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_SHARE; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; + +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.statusbar.phone.StatusBar; + +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import javax.inject.Inject; + +/** + * Receiver to proxy the share or edit intent, used to clean up the notification and send + * appropriate signals to the system (ie. to dismiss the keyguard if necessary). + */ +public class ActionProxyReceiver extends BroadcastReceiver { + private static final String TAG = "ActionProxyReceiver"; + + private static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000; + private final StatusBar mStatusBar; + private final ActivityManagerWrapper mActivityManagerWrapper; + private final ScreenshotSmartActions mScreenshotSmartActions; + + @Inject + public ActionProxyReceiver(Optional<StatusBar> statusBar, + ActivityManagerWrapper activityManagerWrapper, + ScreenshotSmartActions screenshotSmartActions) { + mStatusBar = statusBar.orElse(null); + mActivityManagerWrapper = activityManagerWrapper; + mScreenshotSmartActions = screenshotSmartActions; + } + + @Override + public void onReceive(Context context, final Intent intent) { + Runnable startActivityRunnable = () -> { + try { + mActivityManagerWrapper.closeSystemWindows( + SYSTEM_DIALOG_REASON_SCREENSHOT).get( + CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (TimeoutException | InterruptedException | ExecutionException e) { + Log.e(TAG, "Unable to share screenshot", e); + return; + } + + PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); + ActivityOptions opts = ActivityOptions.makeBasic(); + opts.setDisallowEnterPictureInPictureWhileLaunching( + intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false)); + try { + actionIntent.send(context, 0, null, null, null, null, opts.toBundle()); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Pending intent canceled", e); + } + + }; + + if (mStatusBar != null) { + mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null, + true /* dismissShade */, true /* afterKeyguardGone */, + true /* deferred */); + } else { + startActivityRunnable.run(); + } + + if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { + String actionType = Intent.ACTION_EDIT.equals(intent.getAction()) + ? ACTION_TYPE_EDIT + : ACTION_TYPE_SHARE; + mScreenshotSmartActions.notifyScreenshotAction( + context, intent.getStringExtra(EXTRA_ID), actionType, false); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteImageInBackgroundTask.java deleted file mode 100644 index 8c4865510ed1..000000000000 --- a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteImageInBackgroundTask.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.screenshot; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.os.AsyncTask; - -/** - * An AsyncTask that deletes an image from the media store in the background. - */ -class DeleteImageInBackgroundTask extends AsyncTask<Uri, Void, Void> { - private Context mContext; - - DeleteImageInBackgroundTask(Context context) { - mContext = context; - } - - @Override - protected Void doInBackground(Uri... params) { - if (params.length != 1) return null; - - Uri screenshotUri = params[0]; - ContentResolver resolver = mContext.getContentResolver(); - resolver.delete(screenshotUri, null, null); - return null; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java new file mode 100644 index 000000000000..9028bb57c8e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/DeleteScreenshotReceiver.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.screenshot; + +import static com.android.systemui.screenshot.GlobalScreenshot.ACTION_TYPE_DELETE; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED; +import static com.android.systemui.screenshot.GlobalScreenshot.SCREENSHOT_URI_ID; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import com.android.systemui.dagger.qualifiers.Background; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +/** + * Removes the file at a provided URI. + */ +public class DeleteScreenshotReceiver extends BroadcastReceiver { + + private final ScreenshotSmartActions mScreenshotSmartActions; + private final Executor mBackgroundExecutor; + + @Inject + public DeleteScreenshotReceiver(ScreenshotSmartActions screenshotSmartActions, + @Background Executor backgroundExecutor) { + mScreenshotSmartActions = screenshotSmartActions; + mBackgroundExecutor = backgroundExecutor; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.hasExtra(SCREENSHOT_URI_ID)) { + return; + } + + // And delete the image from the media store + final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID)); + mBackgroundExecutor.execute(() -> { + ContentResolver resolver = context.getContentResolver(); + resolver.delete(uri, null, null); + }); + if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { + mScreenshotSmartActions.notifyScreenshotAction( + context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java index d6e1a16bc69e..c3c947bc40a8 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java @@ -21,8 +21,6 @@ import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT; - import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -30,13 +28,10 @@ import android.animation.ValueAnimator; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; -import android.app.ActivityOptions; import android.app.Notification; import android.app.PendingIntent; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; @@ -57,13 +52,11 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.PowerManager; import android.os.RemoteException; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import android.util.MathUtils; -import android.util.Slog; import android.view.Display; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -88,23 +81,15 @@ import android.widget.Toast; import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.QuickStepContract; -import com.android.systemui.statusbar.phone.StatusBar; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Singleton; -import dagger.Lazy; - /** * Class for handling device screen shots */ @@ -193,6 +178,7 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset private final UiEventLogger mUiEventLogger; private final Context mContext; + private final ScreenshotSmartActions mScreenshotSmartActions; private final WindowManager mWindowManager; private final WindowManager.LayoutParams mWindowLayoutParams; private final Display mDisplay; @@ -214,9 +200,9 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset private Animator mScreenshotAnimation; private Runnable mOnCompleteRunnable; private Animator mDismissAnimation; - private boolean mInDarkMode = false; - private boolean mDirectionLTR = true; - private boolean mOrientationPortrait = true; + private boolean mInDarkMode; + private boolean mDirectionLTR; + private boolean mOrientationPortrait; private float mCornerSizeX; private float mDismissDeltaY; @@ -245,15 +231,14 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } }; - /** - * @param context everything needs a context :( - */ @Inject public GlobalScreenshot( Context context, @Main Resources resources, + ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController screenshotNotificationsController, UiEventLogger uiEventLogger) { mContext = context; + mScreenshotSmartActions = screenshotSmartActions; mNotificationsController = screenshotNotificationsController; mUiEventLogger = uiEventLogger; @@ -320,6 +305,104 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset inoutInfo.touchableRegion.set(touchRegion); } + void takeScreenshotFullscreen(Consumer<Uri> finisher, Runnable onComplete) { + mOnCompleteRunnable = onComplete; + + mDisplay.getRealMetrics(mDisplayMetrics); + takeScreenshotInternal( + finisher, + new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); + } + + void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, + Insets visibleInsets, int taskId, int userId, ComponentName topComponent, + Consumer<Uri> finisher, Runnable onComplete) { + // TODO: use task Id, userId, topComponent for smart handler + + mOnCompleteRunnable = onComplete; + if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { + saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); + } else { + saveScreenshot(screenshot, finisher, + new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE, + true); + } + } + + /** + * Displays a screenshot selector + */ + @SuppressLint("ClickableViewAccessibility") + void takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete) { + dismissScreenshot("new screenshot requested", true); + mOnCompleteRunnable = onComplete; + + mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); + mScreenshotSelectorView.setOnTouchListener((v, event) -> { + ScreenshotSelectorView view = (ScreenshotSelectorView) v; + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + view.startSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_MOVE: + view.updateSelection((int) event.getX(), (int) event.getY()); + return true; + case MotionEvent.ACTION_UP: + view.setVisibility(View.GONE); + mWindowManager.removeView(mScreenshotLayout); + final Rect rect = view.getSelectionRect(); + if (rect != null) { + if (rect.width() != 0 && rect.height() != 0) { + // Need mScreenshotLayout to handle it after the view disappears + mScreenshotLayout.post(() -> takeScreenshotInternal(finisher, rect)); + } + } + + view.stopSelection(); + return true; + } + + return false; + }); + mScreenshotLayout.post(() -> { + mScreenshotSelectorView.setVisibility(View.VISIBLE); + mScreenshotSelectorView.requestFocus(); + }); + } + + /** + * Cancels screenshot request + */ + void stopScreenshot() { + // If the selector layer still presents on screen, we remove it and resets its state. + if (mScreenshotSelectorView.getSelectionRect() != null) { + mWindowManager.removeView(mScreenshotLayout); + mScreenshotSelectorView.stopSelection(); + } + } + + /** + * Clears current screenshot + */ + void dismissScreenshot(String reason, boolean immediate) { + Log.v(TAG, "clearing screenshot: " + reason); + mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); + mScreenshotLayout.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + if (!immediate) { + mDismissAnimation = createScreenshotDismissAnimation(); + mDismissAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + clearScreenshot(); + } + }); + mDismissAnimation.start(); + } else { + clearScreenshot(); + } + } + private void onConfigChanged(Configuration newConfig) { boolean needsUpdate = false; // dark mode @@ -408,15 +491,12 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } return mScreenshotLayout.onApplyWindowInsets(insets); }); - mScreenshotLayout.setOnKeyListener(new View.OnKeyListener() { - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - dismissScreenshot("back pressed", true); - return true; - } - return false; + mScreenshotLayout.setOnKeyListener((v, keyCode, event) -> { + if (keyCode == KeyEvent.KEYCODE_BACK) { + dismissScreenshot("back pressed", false); + return true; } + return false; }); // Get focus so that the key events go to the layout. mScreenshotLayout.setFocusableInTouchMode(true); @@ -471,61 +551,27 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } /** - * Updates the window focusability. If the window is already showing, then it updates the - * window immediately, otherwise the layout params will be applied when the window is next - * shown. - */ - private void setWindowFocusable(boolean focusable) { - if (focusable) { - mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; - } else { - mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; - } - if (mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.updateViewLayout(mScreenshotLayout, mWindowLayoutParams); - } - } - - /** - * Creates a new worker thread and saves the screenshot to the media store. - */ - private void saveScreenshotInWorkerThread( - Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener) { - SaveImageInBackgroundData data = new SaveImageInBackgroundData(); - data.image = mScreenBitmap; - data.finisher = finisher; - data.mActionsReadyListener = actionsReadyListener; - - if (mSaveInBgTask != null) { - // just log success/failure for the pre-existing screenshot - mSaveInBgTask.setActionsReadyListener(new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - logSuccessOnActionsReady(imageData); - } - }); - } - - mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data); - mSaveInBgTask.execute(); - } - - /** * Takes a screenshot of the current display and shows an animation. */ - private void takeScreenshot(Consumer<Uri> finisher, Rect crop) { + private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) { // copy the input Rect, since SurfaceControl.screenshot can mutate it Rect screenRect = new Rect(crop); int rot = mDisplay.getRotation(); int width = crop.width(); int height = crop.height(); - takeScreenshot(SurfaceControl.screenshot(crop, width, height, rot), finisher, screenRect, + saveScreenshot(SurfaceControl.screenshot(crop, width, height, rot), finisher, screenRect, Insets.NONE, true); } - private void takeScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, + private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, boolean showFlash) { - dismissScreenshot("new screenshot requested", true); + if (mScreenshotLayout.isAttachedToWindow()) { + // if we didn't already dismiss for another reason + if (mDismissAnimation == null || !mDismissAnimation.isRunning()) { + mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED); + } + dismissScreenshot("new screenshot requested", true); + } mScreenBitmap = screenshot; @@ -561,85 +607,6 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset startAnimation(finisher, screenRect, screenInsets, showFlash); } - void takeScreenshot(Consumer<Uri> finisher, Runnable onComplete) { - mOnCompleteRunnable = onComplete; - - mDisplay.getRealMetrics(mDisplayMetrics); - takeScreenshot( - finisher, - new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels)); - } - - void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, - Insets visibleInsets, int taskId, int userId, ComponentName topComponent, - Consumer<Uri> finisher, Runnable onComplete) { - // TODO: use task Id, userId, topComponent for smart handler - - mOnCompleteRunnable = onComplete; - if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { - takeScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false); - } else { - takeScreenshot(screenshot, finisher, - new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE, - true); - } - } - - /** - * Displays a screenshot selector - */ - @SuppressLint("ClickableViewAccessibility") - void takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete) { - dismissScreenshot("new screenshot requested", true); - mOnCompleteRunnable = onComplete; - - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - ScreenshotSelectorView view = (ScreenshotSelectorView) v; - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - view.startSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_MOVE: - view.updateSelection((int) event.getX(), (int) event.getY()); - return true; - case MotionEvent.ACTION_UP: - view.setVisibility(View.GONE); - mWindowManager.removeView(mScreenshotLayout); - final Rect rect = view.getSelectionRect(); - if (rect != null) { - if (rect.width() != 0 && rect.height() != 0) { - // Need mScreenshotLayout to handle it after the view disappears - mScreenshotLayout.post(() -> takeScreenshot(finisher, rect)); - } - } - - view.stopSelection(); - return true; - } - - return false; - } - }); - mScreenshotLayout.post(() -> { - mScreenshotSelectorView.setVisibility(View.VISIBLE); - mScreenshotSelectorView.requestFocus(); - }); - } - - /** - * Cancels screenshot request - */ - void stopScreenshot() { - // If the selector layer still presents on screen, we remove it and resets its state. - if (mScreenshotSelectorView.getSelectionRect() != null) { - mWindowManager.removeView(mScreenshotLayout); - mScreenshotSelectorView.stopSelection(); - } - } - /** * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on * failure). @@ -670,55 +637,70 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset }); } - private boolean isUserSetupComplete() { - return Settings.Secure.getInt(mContext.getContentResolver(), - SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + /** + * Starts the animation after taking the screenshot + */ + private void startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, + boolean showFlash) { + mScreenshotHandler.post(() -> { + if (!mScreenshotLayout.isAttachedToWindow()) { + mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); + } + mScreenshotAnimatedView.setImageDrawable( + createScreenDrawable(mScreenBitmap, screenInsets)); + setAnimatedViewSize(screenRect.width(), screenRect.height()); + // Show when the animation starts + mScreenshotAnimatedView.setVisibility(View.GONE); + + mScreenshotPreview.setImageDrawable(createScreenDrawable(mScreenBitmap, screenInsets)); + // make static preview invisible (from gone) so we can query its location on screen + mScreenshotPreview.setVisibility(View.INVISIBLE); + + mScreenshotHandler.post(() -> { + mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this); + + mScreenshotAnimation = + createScreenshotDropInAnimation(screenRect, showFlash); + + saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { + @Override + void onActionsReady(SavedImageData imageData) { + showUiOnActionsReady(imageData); + } + }); + + // Play the shutter sound to notify that we've taken a screenshot + mCameraSound.play(MediaActionSound.SHUTTER_CLICK); + + mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null); + mScreenshotPreview.buildLayer(); + mScreenshotAnimation.start(); + }); + }); } /** - * Clears current screenshot + * Creates a new worker thread and saves the screenshot to the media store. */ - void dismissScreenshot(String reason, boolean immediate) { - Log.v(TAG, "clearing screenshot: " + reason); - mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); - mScreenshotLayout.getViewTreeObserver().removeOnComputeInternalInsetsListener(this); - if (!immediate) { - mDismissAnimation = createScreenshotDismissAnimation(); - mDismissAnimation.addListener(new AnimatorListenerAdapter() { + private void saveScreenshotInWorkerThread( + Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener) { + SaveImageInBackgroundData data = new SaveImageInBackgroundData(); + data.image = mScreenBitmap; + data.finisher = finisher; + data.mActionsReadyListener = actionsReadyListener; + + if (mSaveInBgTask != null) { + // just log success/failure for the pre-existing screenshot + mSaveInBgTask.setActionsReadyListener(new ActionsReadyListener() { @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - clearScreenshot(); + void onActionsReady(SavedImageData imageData) { + logSuccessOnActionsReady(imageData); } }); - mDismissAnimation.start(); - } else { - clearScreenshot(); - } - } - - private void clearScreenshot() { - if (mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.removeView(mScreenshotLayout); } - // Clear any references to the bitmap - mScreenshotPreview.setImageDrawable(null); - mScreenshotAnimatedView.setImageDrawable(null); - mScreenshotAnimatedView.setVisibility(View.GONE); - mActionsContainerBackground.setVisibility(View.GONE); - mActionsContainer.setVisibility(View.GONE); - mBackgroundProtection.setAlpha(0f); - mDismissButton.setVisibility(View.GONE); - mScreenshotPreview.setVisibility(View.GONE); - mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null); - mScreenshotPreview.setContentDescription( - mContext.getResources().getString(R.string.screenshot_preview_description)); - mScreenshotLayout.setAlpha(1); - mDismissButton.setTranslationY(0); - mActionsContainer.setTranslationY(0); - mActionsContainerBackground.setTranslationY(0); - mScreenshotPreview.setTranslationY(0); + mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mScreenshotSmartActions, data); + mSaveInBgTask.execute(); } /** @@ -768,56 +750,6 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset } } - /** - * Starts the animation after taking the screenshot - */ - private void startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, - boolean showFlash) { - - // If power save is on, show a toast so there is some visual indication that a - // screenshot has been taken. - PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); - if (powerManager.isPowerSaveMode()) { - Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show(); - } - - mScreenshotHandler.post(() -> { - if (!mScreenshotLayout.isAttachedToWindow()) { - mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams); - } - mScreenshotAnimatedView.setImageDrawable( - createScreenDrawable(mScreenBitmap, screenInsets)); - setAnimatedViewSize(screenRect.width(), screenRect.height()); - // Show when the animation starts - mScreenshotAnimatedView.setVisibility(View.GONE); - - mScreenshotPreview.setImageDrawable(createScreenDrawable(mScreenBitmap, screenInsets)); - // make static preview invisible (from gone) so we can query its location on screen - mScreenshotPreview.setVisibility(View.INVISIBLE); - - mScreenshotHandler.post(() -> { - mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this); - - mScreenshotAnimation = - createScreenshotDropInAnimation(screenRect, showFlash); - - saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() { - @Override - void onActionsReady(SavedImageData imageData) { - showUiOnActionsReady(imageData); - } - }); - - // Play the shutter sound to notify that we've taken a screenshot - mCameraSound.play(MediaActionSound.SHUTTER_CLICK); - - mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null); - mScreenshotPreview.buildLayer(); - mScreenshotAnimation.start(); - }); - }); - } - private AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) { Rect previewBounds = new Rect(); mScreenshotPreview.getBoundsOnScreen(previewBounds); @@ -1070,6 +1002,31 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset return animSet; } + private void clearScreenshot() { + if (mScreenshotLayout.isAttachedToWindow()) { + mWindowManager.removeView(mScreenshotLayout); + } + + // Clear any references to the bitmap + mScreenshotPreview.setImageDrawable(null); + mScreenshotAnimatedView.setImageDrawable(null); + mScreenshotAnimatedView.setVisibility(View.GONE); + mActionsContainerBackground.setVisibility(View.GONE); + mActionsContainer.setVisibility(View.GONE); + mBackgroundProtection.setAlpha(0f); + mDismissButton.setVisibility(View.GONE); + mScreenshotPreview.setVisibility(View.GONE); + mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null); + mScreenshotPreview.setContentDescription( + mContext.getResources().getString(R.string.screenshot_preview_description)); + mScreenshotPreview.setOnClickListener(null); + mScreenshotLayout.setAlpha(1); + mDismissButton.setTranslationY(0); + mActionsContainer.setTranslationY(0); + mActionsContainerBackground.setTranslationY(0); + mScreenshotPreview.setTranslationY(0); + } + private void setAnimatedViewSize(int width, int height) { ViewGroup.LayoutParams layoutParams = mScreenshotAnimatedView.getLayoutParams(); layoutParams.width = width; @@ -1077,6 +1034,27 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset mScreenshotAnimatedView.setLayoutParams(layoutParams); } + /** + * Updates the window focusability. If the window is already showing, then it updates the + * window immediately, otherwise the layout params will be applied when the window is next + * shown. + */ + private void setWindowFocusable(boolean focusable) { + if (focusable) { + mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE; + } else { + mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE; + } + if (mScreenshotLayout.isAttachedToWindow()) { + mWindowManager.updateViewLayout(mScreenshotLayout, mWindowLayoutParams); + } + } + + private boolean isUserSetupComplete() { + return Settings.Secure.getInt(mContext.getContentResolver(), + SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; + } + /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ private boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds) { int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; @@ -1128,125 +1106,10 @@ public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInset if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) { // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need // to fill in the background of the drawable. - return new LayerDrawable(new Drawable[] { + return new LayerDrawable(new Drawable[]{ new ColorDrawable(Color.BLACK), insetDrawable}); } else { return insetDrawable; } } - - /** - * Receiver to proxy the share or edit intent, used to clean up the notification and send - * appropriate signals to the system (ie. to dismiss the keyguard if necessary). - */ - public static class ActionProxyReceiver extends BroadcastReceiver { - static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000; - private final StatusBar mStatusBar; - - @Inject - public ActionProxyReceiver(Optional<Lazy<StatusBar>> statusBarLazy) { - Lazy<StatusBar> statusBar = statusBarLazy.orElse(null); - mStatusBar = statusBar != null ? statusBar.get() : null; - } - - @Override - public void onReceive(Context context, final Intent intent) { - Runnable startActivityRunnable = () -> { - try { - ActivityManagerWrapper.getInstance().closeSystemWindows( - SYSTEM_DIALOG_REASON_SCREENSHOT).get( - CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } catch (TimeoutException | InterruptedException | ExecutionException e) { - Slog.e(TAG, "Unable to share screenshot", e); - return; - } - - PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); - if (intent.getBooleanExtra(EXTRA_CANCEL_NOTIFICATION, false)) { - ScreenshotNotificationsController.cancelScreenshotNotification(context); - } - ActivityOptions opts = ActivityOptions.makeBasic(); - opts.setDisallowEnterPictureInPictureWhileLaunching( - intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false)); - try { - actionIntent.send(context, 0, null, null, null, null, opts.toBundle()); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent canceled", e); - } - - }; - - if (mStatusBar != null) { - mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null, - true /* dismissShade */, true /* afterKeyguardGone */, - true /* deferred */); - } else { - startActivityRunnable.run(); - } - - if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { - String actionType = Intent.ACTION_EDIT.equals(intent.getAction()) - ? ACTION_TYPE_EDIT - : ACTION_TYPE_SHARE; - ScreenshotSmartActions.notifyScreenshotAction( - context, intent.getStringExtra(EXTRA_ID), actionType, false); - } - } - } - - /** - * Removes the notification for a screenshot after a share target is chosen. - */ - public static class TargetChosenReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - // Clear the notification only after the user has chosen a share action - ScreenshotNotificationsController.cancelScreenshotNotification(context); - } - } - - /** - * Removes the last screenshot. - */ - public static class DeleteScreenshotReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (!intent.hasExtra(SCREENSHOT_URI_ID)) { - return; - } - - // Clear the notification when the image is deleted - ScreenshotNotificationsController.cancelScreenshotNotification(context); - - // And delete the image from the media store - final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID)); - new DeleteImageInBackgroundTask(context).execute(uri); - if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) { - ScreenshotSmartActions.notifyScreenshotAction( - context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false); - } - } - } - - /** - * Executes the smart action tapped by the user in the notification. - */ - public static class SmartActionsReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); - String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE); - Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent()); - ActivityOptions opts = ActivityOptions.makeBasic(); - - try { - pendingIntent.send(context, 0, null, null, null, null, opts.toBundle()); - } catch (PendingIntent.CanceledException e) { - Log.e(TAG, "Pending intent canceled", e); - } - - ScreenshotSmartActions.notifyScreenshotAction( - context, intent.getStringExtra(EXTRA_ID), actionType, true); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 468b9b16addb..df1d78953f46 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -81,6 +81,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"; private final Context mContext; + private final ScreenshotSmartActions mScreenshotSmartActions; private final GlobalScreenshot.SaveImageInBackgroundData mParams; private final GlobalScreenshot.SavedImageData mImageData; private final String mImageFileName; @@ -90,8 +91,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { private final boolean mSmartActionsEnabled; private final Random mRandom = new Random(); - SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data) { + SaveImageInBackgroundTask(Context context, ScreenshotSmartActions screenshotSmartActions, + GlobalScreenshot.SaveImageInBackgroundData data) { mContext = context; + mScreenshotSmartActions = screenshotSmartActions; mImageData = new GlobalScreenshot.SavedImageData(); // Prepare all the output metadata @@ -141,7 +144,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { final Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); CompletableFuture<List<Notification.Action>> smartActionsFuture = - ScreenshotSmartActions.getSmartActionsFuture( + mScreenshotSmartActions.getSmartActionsFuture( mScreenshotId, uri, image, mSmartActionsProvider, mSmartActionsEnabled, getUserHandle(mContext)); @@ -199,7 +202,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { SystemUiDeviceConfigFlags.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS, 1000); smartActions.addAll(buildSmartActions( - ScreenshotSmartActions.getSmartActions( + mScreenshotSmartActions.getSmartActions( mScreenshotId, smartActionsFuture, timeoutMs, mSmartActionsProvider), mContext)); @@ -274,11 +277,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // by setting the (otherwise unused) request code to the current user id. int requestCode = context.getUserId(); - PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode, - new Intent(context, GlobalScreenshot.TargetChosenReceiver.class), - PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT); Intent sharingChooserIntent = - Intent.createChooser(sharingIntent, null, chooserAction.getIntentSender()) + Intent.createChooser(sharingIntent, null) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -288,7 +288,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Create a share action for the notification PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode, - new Intent(context, GlobalScreenshot.ActionProxyReceiver.class) + new Intent(context, ActionProxyReceiver.class) .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent) .putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true) .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) @@ -333,10 +333,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Create a edit action PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode, - new Intent(context, GlobalScreenshot.ActionProxyReceiver.class) + new Intent(context, ActionProxyReceiver.class) .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, pendingIntent) - .putExtra(GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION, - editIntent.getComponent() != null) .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, mSmartActionsEnabled) @@ -358,7 +356,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { // Create a delete action for the notification PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode, - new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class) + new Intent(context, DeleteScreenshotReceiver.class) .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()) .putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId) .putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, @@ -398,7 +396,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { String actionType = extras.getString( ScreenshotNotificationSmartActionsProvider.ACTION_TYPE, ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE); - Intent intent = new Intent(context, GlobalScreenshot.SmartActionsReceiver.class) + Intent intent = new Intent(context, SmartActionsReceiver.class) .putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, action.actionIntent) .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java index b5209bbbdd21..a48870240384 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionChip.java @@ -65,10 +65,10 @@ public class ScreenshotActionChip extends FrameLayout { } void setIcon(Icon icon, boolean tint) { - if (tint) { - icon.setTint(mIconColor); - } mIcon.setImageIcon(icon); + if (!tint) { + mIcon.setImageTintList(null); + } } void setText(CharSequence text) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java index 20fa991dcc1f..8535d5708a67 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotEvent.java @@ -56,7 +56,9 @@ public enum ScreenshotEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "screenshot interaction timed out") SCREENSHOT_INTERACTION_TIMEOUT(310), @UiEvent(doc = "screenshot explicitly dismissed") - SCREENSHOT_EXPLICIT_DISMISSAL(311); + SCREENSHOT_EXPLICIT_DISMISSAL(311), + @UiEvent(doc = "screenshot reentered for new screenshot") + SCREENSHOT_REENTERED(640); private final int mId; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java index 442b373b31be..633cdd6ca5ca 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotSmartActions.java @@ -39,14 +39,21 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import javax.inject.Inject; +import javax.inject.Singleton; + /** * Collects the static functions for retrieving and acting on smart actions. */ +@Singleton public class ScreenshotSmartActions { private static final String TAG = "ScreenshotSmartActions"; + @Inject + public ScreenshotSmartActions() {} + @VisibleForTesting - static CompletableFuture<List<Notification.Action>> getSmartActionsFuture( + CompletableFuture<List<Notification.Action>> getSmartActionsFuture( String screenshotId, Uri screenshotUri, Bitmap image, ScreenshotNotificationSmartActionsProvider smartActionsProvider, boolean smartActionsEnabled, UserHandle userHandle) { @@ -86,7 +93,7 @@ public class ScreenshotSmartActions { } @VisibleForTesting - static List<Notification.Action> getSmartActions(String screenshotId, + List<Notification.Action> getSmartActions(String screenshotId, CompletableFuture<List<Notification.Action>> smartActionsFuture, int timeoutMs, ScreenshotNotificationSmartActionsProvider smartActionsProvider) { long startTimeMs = SystemClock.uptimeMillis(); @@ -116,7 +123,7 @@ public class ScreenshotSmartActions { } } - static void notifyScreenshotOp(String screenshotId, + void notifyScreenshotOp(String screenshotId, ScreenshotNotificationSmartActionsProvider smartActionsProvider, ScreenshotNotificationSmartActionsProvider.ScreenshotOp op, ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus status, long durationMs) { @@ -127,7 +134,7 @@ public class ScreenshotSmartActions { } } - static void notifyScreenshotAction(Context context, String screenshotId, String action, + void notifyScreenshotAction(Context context, String screenshotId, String action, boolean isSmartAction) { try { ScreenshotNotificationSmartActionsProvider provider = diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java new file mode 100644 index 000000000000..217235b16ecf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsReceiver.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.screenshot; + +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_INTENT; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ACTION_TYPE; +import static com.android.systemui.screenshot.GlobalScreenshot.EXTRA_ID; + +import android.app.ActivityOptions; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.util.Slog; + +import javax.inject.Inject; + + +/** + * Executes the smart action tapped by the user in the notification. + */ +public class SmartActionsReceiver extends BroadcastReceiver { + private static final String TAG = "SmartActionsReceiver"; + + private final ScreenshotSmartActions mScreenshotSmartActions; + + @Inject + SmartActionsReceiver(ScreenshotSmartActions screenshotSmartActions) { + mScreenshotSmartActions = screenshotSmartActions; + } + + @Override + public void onReceive(Context context, Intent intent) { + PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT); + String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE); + Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent()); + ActivityOptions opts = ActivityOptions.makeBasic(); + + try { + pendingIntent.send(context, 0, null, null, null, null, opts.toBundle()); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Pending intent canceled", e); + } + + mScreenshotSmartActions.notifyScreenshotAction( + context, intent.getStringExtra(EXTRA_ID), actionType, true); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 9f8a9bb4a432..a043f0f1e50c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -61,7 +61,7 @@ public class TakeScreenshotService extends Service { @Override public void onReceive(Context context, Intent intent) { if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction()) && mScreenshot != null) { - mScreenshot.dismissScreenshot("close system dialogs", true); + mScreenshot.dismissScreenshot("close system dialogs", false); } } }; @@ -102,7 +102,7 @@ public class TakeScreenshotService extends Service { switch (msg.what) { case WindowManager.TAKE_SCREENSHOT_FULLSCREEN: - mScreenshot.takeScreenshot(uriConsumer, onComplete); + mScreenshot.takeScreenshotFullscreen(uriConsumer, onComplete); break; case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION: mScreenshot.takeScreenshotPartial(uriConsumer, onComplete); diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContentResolverProvider.kt b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContentResolverProvider.kt new file mode 100644 index 000000000000..9d05843b42bf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContentResolverProvider.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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.settings + +import android.content.ContentResolver + +interface CurrentUserContentResolverProvider { + + val currentUserContentResolver: ContentResolver +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt index 825a7f3dbadb..d7c4caaa4f9d 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/CurrentUserContextTracker.kt @@ -16,6 +16,7 @@ package com.android.systemui.settings +import android.content.ContentResolver import android.content.Context import android.os.UserHandle import androidx.annotation.VisibleForTesting @@ -31,7 +32,7 @@ import java.lang.IllegalStateException class CurrentUserContextTracker internal constructor( private val sysuiContext: Context, broadcastDispatcher: BroadcastDispatcher -) { +) : CurrentUserContentResolverProvider { private val userTracker: CurrentUserTracker private var initialized = false @@ -44,6 +45,9 @@ class CurrentUserContextTracker internal constructor( return _curUserContext!! } + override val currentUserContentResolver: ContentResolver + get() = currentUserContext.contentResolver + init { userTracker = object : CurrentUserTracker(broadcastDispatcher) { override fun onUserSwitched(newUserId: Int) { @@ -54,8 +58,8 @@ class CurrentUserContextTracker internal constructor( fun initialize() { initialized = true - _curUserContext = makeUserContext(userTracker.currentUserId) userTracker.startTracking() + _curUserContext = makeUserContext(userTracker.currentUserId) } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java b/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java index 2c5c3ceb6e66..eb5bd5c01a78 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java +++ b/packages/SystemUI/src/com/android/systemui/settings/dagger/SettingsModule.java @@ -19,10 +19,12 @@ package com.android.systemui.settings.dagger; import android.content.Context; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.settings.CurrentUserContentResolverProvider; import com.android.systemui.settings.CurrentUserContextTracker; import javax.inject.Singleton; +import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -30,7 +32,7 @@ import dagger.Provides; * Dagger Module for classes found within the com.android.systemui.settings package. */ @Module -public interface SettingsModule { +public abstract class SettingsModule { /** * Provides and initializes a CurrentUserContextTracker @@ -45,4 +47,9 @@ public interface SettingsModule { tracker.initialize(); return tracker; } + + @Binds + @Singleton + abstract CurrentUserContentResolverProvider bindCurrentUserContentResolverTracker( + CurrentUserContextTracker tracker); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index a1444532bd5e..7e1dc6634cec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -140,7 +140,7 @@ public class KeyguardIndicationController implements StateListener, * Creates a new KeyguardIndicationController and registers callbacks. */ @Inject - KeyguardIndicationController(Context context, + public KeyguardIndicationController(Context context, WakeLock.Builder wakeLockBuilder, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, @@ -523,8 +523,7 @@ public class KeyguardIndicationController implements StateListener, }); } - @VisibleForTesting - String computePowerIndication() { + protected String computePowerIndication() { if (mPowerCharged) { return mContext.getResources().getString(R.string.keyguard_charged); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 739d30c2a707..8cd82cc77530 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -21,6 +21,7 @@ import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_LOCKSCREEN_W import static com.android.systemui.statusbar.phone.StatusBar.SHOW_LOCKSCREEN_MEDIA_ARTWORK; import android.annotation.MainThread; +import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; import android.content.Context; @@ -57,6 +58,7 @@ import com.android.systemui.statusbar.dagger.StatusBarModule; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.phone.BiometricUnlockController; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.LockscreenWallpaper; @@ -234,8 +236,17 @@ public class NotificationMediaManager implements Dumpable { NotificationVisibility visibility, boolean removedByUser, int reason) { - onNotificationRemoved(entry.getKey()); - mediaDataManager.onNotificationRemoved(entry.getKey()); + removeEntry(entry); + } + }); + + // Pending entries are never inflated, and will never generate a call to onEntryRemoved(). + // This can happen when notifications are added and canceled before inflation. Add this + // separate listener for cleanup, since media inflation occurs onPendingEntryAdded(). + notificationEntryManager.addCollectionListener(new NotifCollectionListener() { + @Override + public void onEntryCleanUp(@NonNull NotificationEntry entry) { + removeEntry(entry); } }); @@ -248,6 +259,11 @@ public class NotificationMediaManager implements Dumpable { mPropertiesChangedListener); } + private void removeEntry(NotificationEntry entry) { + onNotificationRemoved(entry.getKey()); + mMediaDataManager.onNotificationRemoved(entry.getKey()); + } + /** * Check if a state should be considered actively playing * @param state a PlaybackState diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 423f85f2ddd0..bd65ef06f3a9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -282,7 +282,7 @@ public final class NotificationEntry extends ListEntry { + " doesn't match existing key " + mKey); } - mRanking = ranking; + mRanking = ranking.withAudiblyAlertedInfo(mRanking); } /* diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 582e3e5b6c34..a7d83b3b2774 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -37,6 +37,8 @@ import android.widget.RemoteViews; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageMessageConsumer; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.media.MediaDataManagerKt; +import com.android.systemui.media.MediaFeatureFlag; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.SmartReplyController; @@ -71,6 +73,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder public static final String TAG = "NotifContentInflater"; private boolean mInflateSynchronously = false; + private final boolean mIsMediaInQS; private final NotificationRemoteInputManager mRemoteInputManager; private final NotifRemoteViewCache mRemoteViewCache; private final Lazy<SmartReplyConstants> mSmartReplyConstants; @@ -85,12 +88,14 @@ public class NotificationContentInflater implements NotificationRowContentBinder Lazy<SmartReplyConstants> smartReplyConstants, Lazy<SmartReplyController> smartReplyController, ConversationNotificationProcessor conversationProcessor, + MediaFeatureFlag mediaFeatureFlag, @Background Executor bgExecutor) { mRemoteViewCache = remoteViewCache; mRemoteInputManager = remoteInputManager; mSmartReplyConstants = smartReplyConstants; mSmartReplyController = smartReplyController; mConversationProcessor = conversationProcessor; + mIsMediaInQS = mediaFeatureFlag.getEnabled(); mBgExecutor = bgExecutor; } @@ -135,7 +140,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, callback, - mRemoteInputManager.getRemoteViewsOnClickHandler()); + mRemoteInputManager.getRemoteViewsOnClickHandler(), + mIsMediaInQS); if (mInflateSynchronously) { task.onPostExecute(task.doInBackground()); } else { @@ -711,6 +717,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private RemoteViews.OnClickHandler mRemoteViewClickHandler; private CancellationSignal mCancellationSignal; private final ConversationNotificationProcessor mConversationProcessor; + private final boolean mIsMediaInQS; private AsyncInflationTask( Executor bgExecutor, @@ -726,7 +733,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, - RemoteViews.OnClickHandler remoteViewClickHandler) { + RemoteViews.OnClickHandler remoteViewClickHandler, + boolean isMediaFlagEnabled) { mEntry = entry; mRow = row; mSmartReplyConstants = smartReplyConstants; @@ -742,6 +750,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewClickHandler = remoteViewClickHandler; mCallback = callback; mConversationProcessor = conversationProcessor; + mIsMediaInQS = isMediaFlagEnabled; entry.setInflationTask(this); } @@ -765,7 +774,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder packageContext = new RtlEnabledContext(packageContext); } Notification notification = sbn.getNotification(); - if (notification.isMediaNotification()) { + if (notification.isMediaNotification() && !(mIsMediaInQS + && MediaDataManagerKt.isMediaNotification(sbn))) { MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext, packageContext); processor.processNotification(notification, recoveredBuilder); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java index f543db74d91a..25a0ea515ea3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationConversationInfo.java @@ -42,6 +42,8 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -539,12 +541,21 @@ public class NotificationConversationInfo extends LinearLayout implements && Settings.Global.getInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0) == 1; + Drawable person = mIconFactory.getBaseIconDrawable(mShortcutInfo); + if (person == null) { + person = mContext.getDrawable(R.drawable.ic_person).mutate(); + TypedArray ta = mContext.obtainStyledAttributes(new int[]{android.R.attr.colorAccent}); + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + person.setTint(colorAccent); + } + PriorityOnboardingDialogController controller = mBuilderProvider.get() .setContext(mUserContext) .setView(onboardingView) .setIgnoresDnd(ignoreDnd) .setShowsAsBubble(showAsBubble) - .setIcon(mIconFactory.getBaseIconDrawable(mShortcutInfo)) + .setIcon(person) .setBadge(mIconFactory.getAppBadge( mPackageName, UserHandle.getUserId(mSbn.getUid()))) .setOnSettingsClick(mOnConversationSettingsClickListener) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt index 56238d0a1b82..4699ace90bd3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt @@ -41,8 +41,8 @@ import com.android.systemui.statusbar.notification.row.StackScrollerDecorView import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.children -import com.android.systemui.util.takeUntil import com.android.systemui.util.foldToSparseArray +import com.android.systemui.util.takeUntil import javax.inject.Inject /** @@ -166,6 +166,9 @@ class NotificationSectionsManager @Inject internal constructor( peopleHubSubscription?.unsubscribe() peopleHubSubscription = null peopleHeaderView = reinflateView(peopleHeaderView, layoutInflater, R.layout.people_strip) + .apply { + setOnHeaderClickListener(View.OnClickListener { onPeopleHeaderClick() }) + } if (ENABLE_SNOOZED_CONVERSATION_HUB) { peopleHubSubscription = peopleHubViewAdapter.bindView(peopleHubViewBoundary) } @@ -519,6 +522,15 @@ class NotificationSectionsManager @Inject internal constructor( Intent.FLAG_ACTIVITY_SINGLE_TOP) } + private fun onPeopleHeaderClick() { + val intent = Intent(Settings.ACTION_CONVERSATION_SETTINGS) + activityStarter.startActivity( + intent, + true, + true, + Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + private fun onClearGentleNotifsClick(v: View) { onClearSilentNotifsClickListener?.onClick(v) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt index 8f77a1d776e4..b13e7fb839ff 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt @@ -93,6 +93,8 @@ class PeopleHubView(context: Context, attrs: AttributeSet) : } } + fun setOnHeaderClickListener(listener: OnClickListener) = label.setOnClickListener(listener) + private inner class PersonDataListenerImpl(val avatarView: ImageView) : DataListener<PersonViewModel?> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 541c7845a5d3..744733597db4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -29,6 +29,7 @@ import com.android.systemui.R; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.notification.NotificationUtils; +import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.FooterView; @@ -687,15 +688,27 @@ public class StackScrollAlgorithm { AmbientState ambientState) { int childCount = algorithmState.visibleChildren.size(); float childrenOnTop = 0.0f; + + int topHunIndex = -1; + for (int i = 0; i < childCount; i++) { + ExpandableView child = algorithmState.visibleChildren.get(i); + if (child instanceof ActivatableNotificationView + && (child.isAboveShelf() || child.showingPulsing())) { + topHunIndex = i; + break; + } + } + for (int i = childCount - 1; i >= 0; i--) { childrenOnTop = updateChildZValue(i, childrenOnTop, - algorithmState, ambientState); + algorithmState, ambientState, i == topHunIndex); } } protected float updateChildZValue(int i, float childrenOnTop, StackScrollAlgorithmState algorithmState, - AmbientState ambientState) { + AmbientState ambientState, + boolean shouldElevateHun) { ExpandableView child = algorithmState.visibleChildren.get(i); ExpandableViewState childViewState = child.getViewState(); int zDistanceBetweenElements = ambientState.getZDistanceBetweenElements(); @@ -713,8 +726,7 @@ public class StackScrollAlgorithm { } childViewState.zTranslation = baseZ + childrenOnTop * zDistanceBetweenElements; - } else if (child == ambientState.getTrackedHeadsUpRow() - || (i == 0 && (child.isAboveShelf() || child.showingPulsing()))) { + } else if (shouldElevateHun) { // In case this is a new view that has never been measured before, we don't want to // elevate if we are currently expanded more then the notification int shelfHeight = ambientState.getShelf() == null ? 0 : diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java index db9956a4074f..efd6767e66a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java @@ -57,10 +57,10 @@ public class AutoTileManager implements UserAwareController { private UserHandle mCurrentUser; private boolean mInitialized; - private final Context mContext; - private final QSTileHost mHost; - private final Handler mHandler; - private final AutoAddTracker mAutoTracker; + protected final Context mContext; + protected final QSTileHost mHost; + protected final Handler mHandler; + protected final AutoAddTracker mAutoTracker; private final HotspotController mHotspotController; private final DataSaverController mDataSaverController; private final ManagedProfileController mManagedProfileController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java index 304fe0090e77..00a932cb1e8a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/EdgeBackGestureHandler.java @@ -231,7 +231,6 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa } } - Dependency.get(ProtoTracer.class).add(this); mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, ViewConfiguration.getLongPressTimeout()); @@ -286,6 +285,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa */ public void onNavBarAttached() { mIsAttached = true; + Dependency.get(ProtoTracer.class).add(this); mOverviewProxyService.addCallback(mQuickSwitchListener); updateIsEnabled(); startTracking(); @@ -296,6 +296,7 @@ public class EdgeBackGestureHandler extends CurrentUserTracker implements Displa */ public void onNavBarDetached() { mIsAttached = false; + Dependency.get(ProtoTracer.class).remove(this); mOverviewProxyService.removeCallback(mQuickSwitchListener); updateIsEnabled(); stopTracking(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java index 16b5a2389ec6..78742f1d2580 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/FloatingRotationButton.java @@ -33,6 +33,8 @@ import com.android.systemui.R; import com.android.systemui.statusbar.policy.KeyButtonDrawable; import com.android.systemui.statusbar.policy.KeyButtonView; +import java.util.function.Consumer; + /** Containing logic for the rotation button on the physical left bottom corner of the screen. */ public class FloatingRotationButton implements RotationButton { @@ -48,6 +50,7 @@ public class FloatingRotationButton implements RotationButton { private boolean mCanShow = true; private RotationButtonController mRotationButtonController; + private Consumer<Boolean> mVisibilityChangedCallback; FloatingRotationButton(Context context) { mContext = context; @@ -68,6 +71,11 @@ public class FloatingRotationButton implements RotationButton { } @Override + public void setVisibilityChangedCallback(Consumer<Boolean> visibilityChangedCallback) { + mVisibilityChangedCallback = visibilityChangedCallback; + } + + @Override public View getCurrentView() { return mKeyButtonView; } @@ -107,6 +115,16 @@ public class FloatingRotationButton implements RotationButton { mKeyButtonDrawable.resetAnimation(); mKeyButtonDrawable.startAnimation(); } + mKeyButtonView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View view, int i, int i1, int i2, int i3, int i4, int i5, + int i6, int i7) { + if (mIsShowing && mVisibilityChangedCallback != null) { + mVisibilityChangedCallback.accept(true); + } + mKeyButtonView.removeOnLayoutChangeListener(this); + } + }); return true; } @@ -117,6 +135,9 @@ public class FloatingRotationButton implements RotationButton { } mWindowManager.removeViewImmediate(mKeyButtonView); mIsShowing = false; + if (mVisibilityChangedCallback != null) { + mVisibilityChangedCallback.accept(false); + } return true; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java index 27daf8615a31..e60293c9d347 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java @@ -192,6 +192,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback private int mLayoutDirection; private boolean mForceNavBarHandleOpaque; + private boolean mIsCurrentUserSetup; /** @see android.view.WindowInsetsController#setSystemBarsAppearance(int) */ private @Appearance int mAppearance; @@ -311,6 +312,10 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback @Override public void onNavBarButtonAlphaChanged(float alpha, boolean animate) { + if (!mIsCurrentUserSetup) { + // If the current user is not yet setup, then don't update any button alphas + return; + } ButtonDispatcher buttonDispatcher = null; boolean forceVisible = false; if (QuickStepContract.isSwipeUpMode(mNavBarMode)) { @@ -349,14 +354,6 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback } }; - private final ContextButtonListener mRotationButtonListener = (button, visible) -> { - if (visible) { - // If the button will actually become visible and the navbar is about to hide, - // tell the statusbar to keep it around for longer - mAutoHideController.touchAutoHide(); - } - }; - private final Runnable mAutoDim = () -> getBarTransitions().setAutoDim(true); private final ContentObserver mAssistContentObserver = new ContentObserver( @@ -383,6 +380,14 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback } }; + private final DeviceProvisionedController.DeviceProvisionedListener mUserSetupListener = + new DeviceProvisionedController.DeviceProvisionedListener() { + @Override + public void onUserSetupChanged() { + mIsCurrentUserSetup = mDeviceProvisionedController.isCurrentUserSetup(); + } + }; + @Inject public NavigationBarFragment(AccessibilityManagerWrapper accessibilityManagerWrapper, DeviceProvisionedController deviceProvisionedController, MetricsLogger metricsLogger, @@ -450,6 +455,9 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback /* defaultValue = */ true); DeviceConfig.addOnPropertiesChangedListener( DeviceConfig.NAMESPACE_SYSTEMUI, mHandler::post, mOnPropertiesChangedListener); + + mIsCurrentUserSetup = mDeviceProvisionedController.isCurrentUserSetup(); + mDeviceProvisionedController.addCallback(mUserSetupListener); } @Override @@ -458,6 +466,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback mNavigationModeController.removeListener(this); mAccessibilityManagerWrapper.removeCallback(mAccessibilityListener); mContentResolver.unregisterContentObserver(mAssistContentObserver); + mDeviceProvisionedController.removeCallback(mUserSetupListener); DeviceConfig.removeOnPropertiesChangedListener(mOnPropertiesChangedListener); } @@ -504,8 +513,6 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback // Currently there is no accelerometer sensor on non-default display. if (mIsOnDefaultDisplay) { - mNavigationBarView.getRotateSuggestionButton().setListener(mRotationButtonListener); - final RotationButtonController rotationButtonController = mNavigationBarView.getRotationButtonController(); rotationButtonController.addRotationCallback(mRotationWatcher); @@ -591,6 +598,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback .registerDisplayListener(this, new Handler(Looper.getMainLooper())); mOrientationHandle = new QuickswitchOrientedNavHandle(getContext()); + mOrientationHandle.setId(R.id.secondary_home_handle); getBarTransitions().addDarkIntensityListener(mOrientationHandleIntensityListener); mOrientationParams = new WindowManager.LayoutParams(0, 0, @@ -1297,6 +1305,7 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback if (mAutoHideController != null) { mAutoHideController.setNavigationBar(mAutoHideUiElement); } + mNavigationBarView.setAutoHideController(autoHideController); } private boolean isTransientShown() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java index 1eab427b4155..5bb3c836586c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarView.java @@ -113,12 +113,9 @@ public class NavigationBarView extends FrameLayout implements int mNavigationIconHints = 0; private int mNavBarMode; - private Rect mHomeButtonBounds = new Rect(); - private Rect mBackButtonBounds = new Rect(); - private Rect mRecentsButtonBounds = new Rect(); - private Rect mRotationButtonBounds = new Rect(); - private final Region mActiveRegion = new Region(); - private int[] mTmpPosition = new int[2]; + private final Region mTmpRegion = new Region(); + private final int[] mTmpPosition = new int[2]; + private Rect mTmpBounds = new Rect(); private KeyButtonDrawable mBackIcon; private KeyButtonDrawable mHomeDefaultIcon; @@ -130,6 +127,7 @@ public class NavigationBarView extends FrameLayout implements private boolean mDeadZoneConsuming = false; private final NavigationBarTransitions mBarTransitions; private final OverviewProxyService mOverviewProxyService; + private AutoHideController mAutoHideController; // performs manual animation in sync with layout transitions private final NavTransitionListener mTransitionListener = new NavTransitionListener(); @@ -255,24 +253,23 @@ public class NavigationBarView extends FrameLayout implements private final OnComputeInternalInsetsListener mOnComputeInternalInsetsListener = info -> { // When the nav bar is in 2-button or 3-button mode, or when IME is visible in fully // gestural mode, the entire nav bar should be touchable. - if (!mEdgeBackGestureHandler.isHandlingGestures() || mImeVisible) { + if (!mEdgeBackGestureHandler.isHandlingGestures()) { info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_FRAME); return; } info.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION); - ButtonDispatcher imeSwitchButton = getImeSwitchButton(); - if (imeSwitchButton.getVisibility() == VISIBLE) { - // If the IME is not up, but the ime switch button is visible, then make sure that - // button is touchable - int[] loc = new int[2]; - View buttonView = imeSwitchButton.getCurrentView(); - buttonView.getLocationInWindow(loc); - info.touchableRegion.set(loc[0], loc[1], loc[0] + buttonView.getWidth(), - loc[1] + buttonView.getHeight()); - return; + info.touchableRegion.set(getButtonLocations(false /* includeFloatingRotationButton */, + false /* inScreen */)); + }; + + private final Consumer<Boolean> mRotationButtonListener = (visible) -> { + if (visible) { + // If the button will actually become visible and the navbar is about to hide, + // tell the statusbar to keep it around for longer + mAutoHideController.touchAutoHide(); } - info.touchableRegion.setEmpty(); + notifyActiveTouchRegions(); }; public NavigationBarView(Context context, AttributeSet attrs) { @@ -305,7 +302,8 @@ public class NavigationBarView extends FrameLayout implements mFloatingRotationButton = new FloatingRotationButton(context); mRotationButtonController = new RotationButtonController(context, R.style.RotateButtonCCWStart90, - isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton); + isGesturalMode ? mFloatingRotationButton : rotateSuggestionButton, + mRotationButtonListener); mConfiguration = new Configuration(); mTmpLastConfiguration = new Configuration(); @@ -353,6 +351,10 @@ public class NavigationBarView extends FrameLayout implements }); } + public void setAutoHideController(AutoHideController autoHideController) { + mAutoHideController = autoHideController; + } + public NavigationBarTransitions getBarTransitions() { return mBarTransitions; } @@ -709,6 +711,7 @@ public class NavigationBarView extends FrameLayout implements getHomeButton().setVisibility(disableHome ? View.INVISIBLE : View.VISIBLE); getRecentsButton().setVisibility(disableRecent ? View.INVISIBLE : View.VISIBLE); getHomeHandle().setVisibility(disableHomeHandle ? View.INVISIBLE : View.VISIBLE); + notifyActiveTouchRegions(); } @VisibleForTesting @@ -927,42 +930,52 @@ public class NavigationBarView extends FrameLayout implements protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - mActiveRegion.setEmpty(); - updateButtonLocation(getBackButton(), mBackButtonBounds, true); - updateButtonLocation(getHomeButton(), mHomeButtonBounds, false); - updateButtonLocation(getRecentsButton(), mRecentsButtonBounds, false); - updateButtonLocation(getRotateSuggestionButton(), mRotationButtonBounds, true); - // TODO: Handle button visibility changes - mOverviewProxyService.onActiveNavBarRegionChanges(mActiveRegion); + notifyActiveTouchRegions(); mRecentsOnboarding.setNavBarHeight(getMeasuredHeight()); } - private void updateButtonLocation(ButtonDispatcher button, Rect buttonBounds, - boolean isActive) { + /** + * Notifies the overview service of the active touch regions. + */ + public void notifyActiveTouchRegions() { + mOverviewProxyService.onActiveNavBarRegionChanges( + getButtonLocations(true /* includeFloatingRotationButton */, true /* inScreen */)); + } + + private Region getButtonLocations(boolean includeFloatingRotationButton, + boolean inScreenSpace) { + mTmpRegion.setEmpty(); + updateButtonLocation(getBackButton(), inScreenSpace); + updateButtonLocation(getHomeButton(), inScreenSpace); + updateButtonLocation(getRecentsButton(), inScreenSpace); + updateButtonLocation(getImeSwitchButton(), inScreenSpace); + updateButtonLocation(getAccessibilityButton(), inScreenSpace); + if (includeFloatingRotationButton && mFloatingRotationButton.isVisible()) { + updateButtonLocation(mFloatingRotationButton.getCurrentView(), inScreenSpace); + } else { + updateButtonLocation(getRotateSuggestionButton(), inScreenSpace); + } + return mTmpRegion; + } + + private void updateButtonLocation(ButtonDispatcher button, boolean inScreenSpace) { View view = button.getCurrentView(); - if (view == null) { - buttonBounds.setEmpty(); + if (view == null || !button.isVisible()) { return; } - // Temporarily reset the translation back to origin to get the position in window - final float posX = view.getTranslationX(); - final float posY = view.getTranslationY(); - view.setTranslationX(0); - view.setTranslationY(0); - - if (isActive) { - view.getLocationOnScreen(mTmpPosition); - buttonBounds.set(mTmpPosition[0], mTmpPosition[1], - mTmpPosition[0] + view.getMeasuredWidth(), - mTmpPosition[1] + view.getMeasuredHeight()); - mActiveRegion.op(buttonBounds, Op.UNION); + updateButtonLocation(view, inScreenSpace); + } + + private void updateButtonLocation(View view, boolean inScreenSpace) { + if (inScreenSpace) { + view.getBoundsOnScreen(mTmpBounds); + } else { + view.getLocationInWindow(mTmpPosition); + mTmpBounds.set(mTmpPosition[0], mTmpPosition[1], + mTmpPosition[0] + view.getWidth(), + mTmpPosition[1] + view.getHeight()); } - view.getLocationInWindow(mTmpPosition); - buttonBounds.set(mTmpPosition[0], mTmpPosition[1], - mTmpPosition[0] + view.getMeasuredWidth(), - mTmpPosition[1] + view.getMeasuredHeight()); - view.setTranslationX(posX); - view.setTranslationY(posY); + mTmpRegion.op(mTmpBounds, Op.UNION); } private void updateOrientationViews() { @@ -1223,6 +1236,7 @@ public class NavigationBarView extends FrameLayout implements dumpButton(pw, "rcnt", getRecentsButton()); dumpButton(pw, "rota", getRotateSuggestionButton()); dumpButton(pw, "a11y", getAccessibilityButton()); + dumpButton(pw, "ime", getImeSwitchButton()); pw.println(" }"); pw.println(" mScreenOn: " + mScreenOn); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 64202d221b2d..799e16cc6d8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -2612,6 +2612,7 @@ public class NotificationPanelViewController extends PanelViewController { super.onClosingFinished(); resetHorizontalPanelPosition(); setClosingWithAlphaFadeout(false); + mMediaHierarchyManager.closeGuts(); } private void setClosingWithAlphaFadeout(boolean closing) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java index 5bb8fab8a62e..4e71a7ea8dae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java @@ -47,6 +47,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; +import com.android.systemui.privacy.PrivacyItem; +import com.android.systemui.privacy.PrivacyItemController; +import com.android.systemui.privacy.PrivacyType; import com.android.systemui.qs.tiles.DndTile; import com.android.systemui.qs.tiles.RotationLockTile; import com.android.systemui.screenrecord.RecordingController; @@ -70,6 +73,9 @@ import com.android.systemui.statusbar.policy.ZenModeController; import com.android.systemui.util.RingerModeTracker; import com.android.systemui.util.time.DateFormatUtil; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; import java.util.Locale; import java.util.concurrent.Executor; @@ -87,13 +93,13 @@ public class PhoneStatusBarPolicy ZenModeController.Callback, DeviceProvisionedListener, KeyguardStateController.Callback, + PrivacyItemController.Callback, LocationController.LocationChangeCallback, RecordingController.RecordingStateChangeCallback { private static final String TAG = "PhoneStatusBarPolicy"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - static final int LOCATION_STATUS_ICON_ID = - com.android.internal.R.drawable.perm_group_location; + static final int LOCATION_STATUS_ICON_ID = PrivacyType.TYPE_LOCATION.getIconId(); private final String mSlotCast; private final String mSlotHotspot; @@ -107,6 +113,8 @@ public class PhoneStatusBarPolicy private final String mSlotHeadset; private final String mSlotDataSaver; private final String mSlotLocation; + private final String mSlotMicrophone; + private final String mSlotCamera; private final String mSlotSensorsOff; private final String mSlotScreenRecord; private final int mDisplayId; @@ -132,6 +140,7 @@ public class PhoneStatusBarPolicy private final DeviceProvisionedController mProvisionedController; private final KeyguardStateController mKeyguardStateController; private final LocationController mLocationController; + private final PrivacyItemController mPrivacyItemController; private final Executor mUiBgExecutor; private final SensorPrivacyController mSensorPrivacyController; private final RecordingController mRecordingController; @@ -162,7 +171,8 @@ public class PhoneStatusBarPolicy RecordingController recordingController, @Nullable TelecomManager telecomManager, @DisplayId int displayId, @Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil, - RingerModeTracker ringerModeTracker) { + RingerModeTracker ringerModeTracker, + PrivacyItemController privacyItemController) { mIconController = iconController; mCommandQueue = commandQueue; mBroadcastDispatcher = broadcastDispatcher; @@ -181,6 +191,7 @@ public class PhoneStatusBarPolicy mProvisionedController = deviceProvisionedController; mKeyguardStateController = keyguardStateController; mLocationController = locationController; + mPrivacyItemController = privacyItemController; mSensorPrivacyController = sensorPrivacyController; mRecordingController = recordingController; mUiBgExecutor = uiBgExecutor; @@ -200,6 +211,8 @@ public class PhoneStatusBarPolicy mSlotHeadset = resources.getString(com.android.internal.R.string.status_bar_headset); mSlotDataSaver = resources.getString(com.android.internal.R.string.status_bar_data_saver); mSlotLocation = resources.getString(com.android.internal.R.string.status_bar_location); + mSlotMicrophone = resources.getString(com.android.internal.R.string.status_bar_microphone); + mSlotCamera = resources.getString(com.android.internal.R.string.status_bar_camera); mSlotSensorsOff = resources.getString(com.android.internal.R.string.status_bar_sensors_off); mSlotScreenRecord = resources.getString( com.android.internal.R.string.status_bar_screen_record); @@ -271,6 +284,22 @@ public class PhoneStatusBarPolicy mResources.getString(R.string.accessibility_data_saver_on)); mIconController.setIconVisibility(mSlotDataSaver, false); + + // privacy items + String microphoneString = mResources.getString(PrivacyType.TYPE_MICROPHONE.getNameId()); + String microphoneDesc = mResources.getString( + R.string.ongoing_privacy_chip_content_multiple_apps, microphoneString); + mIconController.setIcon(mSlotMicrophone, PrivacyType.TYPE_MICROPHONE.getIconId(), + microphoneDesc); + mIconController.setIconVisibility(mSlotMicrophone, false); + + String cameraString = mResources.getString(PrivacyType.TYPE_CAMERA.getNameId()); + String cameraDesc = mResources.getString( + R.string.ongoing_privacy_chip_content_multiple_apps, cameraString); + mIconController.setIcon(mSlotCamera, PrivacyType.TYPE_CAMERA.getIconId(), + cameraDesc); + mIconController.setIconVisibility(mSlotCamera, false); + mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID, mResources.getString(R.string.accessibility_location_active)); mIconController.setIconVisibility(mSlotLocation, false); @@ -294,6 +323,7 @@ public class PhoneStatusBarPolicy mNextAlarmController.addCallback(mNextAlarmCallback); mDataSaver.addCallback(this); mKeyguardStateController.addCallback(this); + mPrivacyItemController.addCallback(this); mSensorPrivacyController.addCallback(mSensorPrivacyListener); mLocationController.addCallback(this); mRecordingController.addCallback(this); @@ -609,13 +639,52 @@ public class PhoneStatusBarPolicy mIconController.setIconVisibility(mSlotDataSaver, isDataSaving); } + @Override // PrivacyItemController.Callback + public void onPrivacyItemsChanged(List<PrivacyItem> privacyItems) { + updatePrivacyItems(privacyItems); + } + + private void updatePrivacyItems(List<PrivacyItem> items) { + boolean showCamera = false; + boolean showMicrophone = false; + boolean showLocation = false; + for (PrivacyItem item : items) { + if (item == null /* b/124234367 */) { + if (DEBUG) { + Log.e(TAG, "updatePrivacyItems - null item found"); + StringWriter out = new StringWriter(); + mPrivacyItemController.dump(null, new PrintWriter(out), null); + Log.e(TAG, out.toString()); + } + continue; + } + switch (item.getPrivacyType()) { + case TYPE_CAMERA: + showCamera = true; + break; + case TYPE_LOCATION: + showLocation = true; + break; + case TYPE_MICROPHONE: + showMicrophone = true; + break; + } + } + + mIconController.setIconVisibility(mSlotCamera, showCamera); + mIconController.setIconVisibility(mSlotMicrophone, showMicrophone); + if (mPrivacyItemController.getAllIndicatorsAvailable()) { + mIconController.setIconVisibility(mSlotLocation, showLocation); + } + } + @Override public void onLocationActiveChanged(boolean active) { - updateLocation(); + if (!mPrivacyItemController.getAllIndicatorsAvailable()) updateLocationFromController(); } // Updates the status view based on the current state of location requests. - private void updateLocation() { + private void updateLocationFromController() { if (mLocationController.isLocationActive()) { mIconController.setIconVisibility(mSlotLocation, true); } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java index 2580c0e77013..281207ba2c36 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButton.java @@ -20,9 +20,12 @@ import android.view.View; import com.android.systemui.statusbar.policy.KeyButtonDrawable; +import java.util.function.Consumer; + /** Interface of a rotation button that interacts {@link RotationButtonController}. */ interface RotationButton { void setRotationButtonController(RotationButtonController rotationButtonController); + void setVisibilityChangedCallback(Consumer<Boolean> visibilityChangedCallback); View getCurrentView(); boolean show(); boolean hide(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java index 59b10e416b03..dbf5aa7481e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationButtonController.java @@ -117,7 +117,8 @@ public class RotationButtonController { return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0; } - RotationButtonController(Context context, @StyleRes int style, RotationButton rotationButton) { + RotationButtonController(Context context, @StyleRes int style, RotationButton rotationButton, + Consumer<Boolean> visibilityChangedCallback) { mContext = context; mRotationButton = rotationButton; mRotationButton.setRotationButtonController(this); @@ -131,6 +132,7 @@ public class RotationButtonController { mTaskStackListener = new TaskStackListenerImpl(); mRotationButton.setOnClickListener(this::onRotateSuggestionClick); mRotationButton.setOnHoverListener(this::onRotateSuggestionHover); + mRotationButton.setVisibilityChangedCallback(visibilityChangedCallback); } void registerListeners() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java index bd9675280b0b..687c2238197b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/RotationContextButton.java @@ -26,6 +26,8 @@ import android.view.View; import com.android.systemui.statusbar.policy.KeyButtonDrawable; +import java.util.function.Consumer; + /** Containing logic for the rotation button in nav bar. */ public class RotationContextButton extends ContextualButton implements NavigationModeController.ModeChangedListener, RotationButton { @@ -44,6 +46,18 @@ public class RotationContextButton extends ContextualButton implements } @Override + public void setVisibilityChangedCallback(Consumer<Boolean> visibilityChangedCallback) { + setListener(new ContextButtonListener() { + @Override + public void onVisibilityChanged(ContextualButton button, boolean visible) { + if (visibilityChangedCallback != null) { + visibilityChangedCallback.accept(visible); + } + } + }); + } + + @Override public void setVisibility(int visibility) { super.setVisibility(visibility); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java index 5a7c5c9b5ebc..f65f97a3a450 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java @@ -271,7 +271,7 @@ public class StatusIconContainer extends AlphaOptimizedLinearLayout { float contentStart = getPaddingStart(); int childCount = getChildCount(); // Underflow === don't show content until that index - if (DEBUG) android.util.Log.d(TAG, "calculateIconTranslations: start=" + translationX + if (DEBUG) Log.d(TAG, "calculateIconTranslations: start=" + translationX + " width=" + width + " underflow=" + mNeedsUnderflow); // Collect all of the states which want to be visible diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index 673549ab589f..e5a46797d035 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -79,6 +79,13 @@ public interface BatteryController extends DemoMode, Dumpable, default void setReverseState(boolean isReverse) {} /** + * Returns {@code true} if extreme battery saver is on. + */ + default boolean isExtremeSaverOn() { + return false; + } + + /** * A listener that will be notified whenever a change in battery level or power save mode has * occurred. */ @@ -92,6 +99,9 @@ public interface BatteryController extends DemoMode, Dumpable, default void onReverseChanged(boolean isReverse, int level, String name) { } + + default void onExtremeBatterySaverChanged(boolean isExtreme) { + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 7f3516194298..d30f01a658f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -58,7 +58,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final EnhancedEstimates mEstimates; - private final BroadcastDispatcher mBroadcastDispatcher; + protected final BroadcastDispatcher mBroadcastDispatcher; protected final ArrayList<BatteryController.BatteryStateChangeCallback> mChangeCallbacks = new ArrayList<>(); private final ArrayList<EstimateFetchCompletion> mFetchCallbacks = new ArrayList<>(); @@ -73,6 +73,7 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC private boolean mCharged; protected boolean mPowerSave; private boolean mAodPowerSave; + protected boolean mWirelessCharging; private boolean mTestmode = false; @VisibleForTesting boolean mHasReceivedBattery = false; @@ -164,6 +165,8 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC BatteryManager.BATTERY_STATUS_UNKNOWN); mCharged = status == BatteryManager.BATTERY_STATUS_FULL; mCharging = mCharged || status == BatteryManager.BATTERY_STATUS_CHARGING; + mWirelessCharging = mCharging && intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) + == BatteryManager.BATTERY_PLUGGED_WIRELESS; fireBatteryLevelChanged(); } else if (action.equals(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)) { @@ -219,6 +222,11 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC } @Override + public boolean isWirelessCharging() { + return mWirelessCharging; + } + + @Override public void getEstimatedTimeRemainingString(EstimateFetchCompletion completion) { // Need to fetch or refresh the estimate, but it may involve binder calls so offload the // work diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index d43dd232e7c5..120a0e3abba4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -81,6 +81,7 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C private boolean mClockVisibleByUser = true; private boolean mAttached; + private boolean mScreenReceiverRegistered; private Calendar mCalendar; private String mClockFormatString; private SimpleDateFormat mClockFormat; @@ -212,6 +213,14 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + if (mScreenReceiverRegistered) { + mScreenReceiverRegistered = false; + mBroadcastDispatcher.unregisterReceiver(mScreenReceiver); + if (mSecondsHandler != null) { + mSecondsHandler.removeCallbacks(mSecondTick); + mSecondsHandler = null; + } + } if (mAttached) { mBroadcastDispatcher.unregisterReceiver(mIntentReceiver); mAttached = false; @@ -362,12 +371,14 @@ public class Clock extends TextView implements DemoMode, Tunable, CommandQueue.C mSecondsHandler.postAtTime(mSecondTick, SystemClock.uptimeMillis() / 1000 * 1000 + 1000); } + mScreenReceiverRegistered = true; IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); mBroadcastDispatcher.registerReceiver(mScreenReceiver, filter); } } else { if (mSecondsHandler != null) { + mScreenReceiverRegistered = false; mBroadcastDispatcher.unregisterReceiver(mScreenReceiver); mSecondsHandler.removeCallbacks(mSecondTick); mSecondsHandler = null; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java index 07de388598b7..82ad00ad7c6d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.java @@ -20,6 +20,7 @@ import static com.android.systemui.statusbar.notification.row.NotificationRowCon import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Notification; import android.content.Context; import android.content.res.Resources; import android.database.ContentObserver; @@ -106,9 +107,9 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { public void updateNotification(@NonNull String key, boolean alert) { super.updateNotification(key, alert); - AlertEntry alertEntry = getHeadsUpEntry(key); - if (alert && alertEntry != null) { - setEntryPinned((HeadsUpEntry) alertEntry, shouldHeadsUpBecomePinned(alertEntry.mEntry)); + HeadsUpEntry headsUpEntry = getHeadsUpEntry(key); + if (alert && headsUpEntry != null) { + setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry)); } } @@ -359,6 +360,11 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { return false; } + private static boolean isOngoingCallNotif(NotificationEntry entry) { + return entry.getSbn().isOngoing() && Notification.CATEGORY_CALL.equals( + entry.getSbn().getNotification().category); + } + /** * This represents a notification and how long it is in a heads up mode. It also manages its * lifecycle automatically when created. @@ -391,6 +397,15 @@ public abstract class HeadsUpManager extends AlertingNotificationManager { return 1; } + boolean selfCall = isOngoingCallNotif(mEntry); + boolean otherCall = isOngoingCallNotif(headsUpEntry.mEntry); + + if (selfCall && !otherCall) { + return -1; + } else if (!selfCall && otherCall) { + return 1; + } + if (remoteInputActive && !headsUpEntry.remoteInputActive) { return -1; } else if (!remoteInputActive && headsUpEntry.remoteInputActive) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java index 23d03a4b225a..e1ff9a236b02 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonDrawable.java @@ -172,6 +172,24 @@ public class KeyButtonDrawable extends Drawable { } @Override + public boolean setVisible(boolean visible, boolean restart) { + boolean changed = super.setVisible(visible, restart); + if (changed) { + // End any existing animations when the visibility changes + jumpToCurrentState(); + } + return changed; + } + + @Override + public void jumpToCurrentState() { + super.jumpToCurrentState(); + if (mAnimatedDrawable != null) { + mAnimatedDrawable.jumpToCurrentState(); + } + } + + @Override public void setAlpha(int alpha) { mState.mAlpha = alpha; mIconPaint.setAlpha(alpha); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java index 2d8784dc41bd..9e1485dba8bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyButtonRipple.java @@ -225,6 +225,16 @@ public class KeyButtonRipple extends Drawable { } @Override + public boolean setVisible(boolean visible, boolean restart) { + boolean changed = super.setVisible(visible, restart); + if (changed) { + // End any existing animations when the visibility changes + jumpToCurrentState(); + } + return changed; + } + + @Override public void jumpToCurrentState() { endAnimations("jumpToCurrentState", false /* cancel */); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java index 2563f19245e0..49be648755c3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java @@ -417,6 +417,10 @@ public class MobileSignalController extends SignalController< return (mServiceState != null && mServiceState.isEmergencyOnly()); } + public boolean isInService() { + return Utils.isInService(mServiceState); + } + private boolean isRoaming() { // During a carrier change, roaming indications need to be supressed. if (isCarrierNetworkChangeActive()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java index 95a97729936b..1dbb228f58b5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java @@ -37,6 +37,7 @@ public interface NetworkController extends CallbackController<SignalCallback>, D DataUsageController getMobileDataController(); DataSaverController getDataSaverController(); String getMobileDataNetworkName(); + boolean isMobileDataNetworkInService(); int getNumberSubscriptions(); boolean hasVoiceCallingFeature(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java index 32c4aec39923..df00a4f743ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkControllerImpl.java @@ -446,6 +446,12 @@ public class NetworkControllerImpl extends BroadcastReceiver } @Override + public boolean isMobileDataNetworkInService() { + MobileSignalController controller = getDataController(); + return controller != null && controller.isInService(); + } + + @Override public int getNumberSubscriptions() { return mMobileSignalControllers.size(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index 309d4b04ebbf..c5a35eaf3e6c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -29,7 +29,6 @@ import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; import android.net.IConnectivityManager; import android.net.Network; -import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.os.Handler; import android.os.RemoteException; @@ -66,12 +65,8 @@ public class SecurityControllerImpl extends CurrentUserTracker implements Securi private static final String TAG = "SecurityController"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - private static final NetworkRequest REQUEST = new NetworkRequest.Builder() - .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) - .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) - .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) - .setUids(null) - .build(); + private static final NetworkRequest REQUEST = + new NetworkRequest.Builder().clearCapabilities().build(); private static final int NO_NETWORK = -1; private static final String VPN_BRANDED_META_DATA = "com.android.systemui.IS_BRANDED"; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java index 5257ce4c6bd9..4ae96651b570 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java @@ -84,7 +84,7 @@ public class WifiSignalController extends R.bool.config_showWifiIndicatorWhenEnabled); boolean wifiVisible = mCurrentState.enabled && ( (mCurrentState.connected && mCurrentState.inetCondition == 1) - || !mHasMobileDataFeature || mWifiTracker.isDefaultNetwork + || !mHasMobileDataFeature || mCurrentState.isDefault || visibleWhenEnabled); String wifiDesc = mCurrentState.connected ? mCurrentState.ssid : null; boolean ssidPresent = wifiVisible && mCurrentState.ssid != null; @@ -107,6 +107,7 @@ public class WifiSignalController extends public void fetchInitialState() { mWifiTracker.fetchInitialState(); mCurrentState.enabled = mWifiTracker.enabled; + mCurrentState.isDefault = mWifiTracker.isDefaultNetwork; mCurrentState.connected = mWifiTracker.connected; mCurrentState.ssid = mWifiTracker.ssid; mCurrentState.rssi = mWifiTracker.rssi; @@ -121,6 +122,7 @@ public class WifiSignalController extends public void handleBroadcast(Intent intent) { mWifiTracker.handleBroadcast(intent); mCurrentState.enabled = mWifiTracker.enabled; + mCurrentState.isDefault = mWifiTracker.isDefaultNetwork; mCurrentState.connected = mWifiTracker.connected; mCurrentState.ssid = mWifiTracker.ssid; mCurrentState.rssi = mWifiTracker.rssi; @@ -131,6 +133,7 @@ public class WifiSignalController extends private void handleStatusUpdated() { mCurrentState.statusLabel = mWifiTracker.statusLabel; + mCurrentState.isDefault = mWifiTracker.isDefaultNetwork; notifyListenersIfNecessary(); } @@ -156,6 +159,7 @@ public class WifiSignalController extends static class WifiState extends SignalController.State { String ssid; boolean isTransient; + boolean isDefault; String statusLabel; @Override @@ -164,6 +168,7 @@ public class WifiSignalController extends WifiState state = (WifiState) s; ssid = state.ssid; isTransient = state.isTransient; + isDefault = state.isDefault; statusLabel = state.statusLabel; } @@ -172,6 +177,7 @@ public class WifiSignalController extends super.toString(builder); builder.append(",ssid=").append(ssid) .append(",isTransient=").append(isTransient) + .append(",isDefault=").append(isDefault) .append(",statusLabel=").append(statusLabel); } @@ -183,6 +189,7 @@ public class WifiSignalController extends WifiState other = (WifiState) o; return Objects.equals(other.ssid, ssid) && other.isTransient == isTransient + && other.isDefault == isDefault && TextUtils.equals(other.statusLabel, statusLabel); } } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt index 016f4de724b6..2a5424ce4ef7 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/PhysicsAnimator.kt @@ -20,6 +20,7 @@ import android.os.Looper import android.util.ArrayMap import android.util.Log import android.view.View +import androidx.dynamicanimation.animation.AnimationHandler import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.FlingAnimation import androidx.dynamicanimation.animation.FloatPropertyCompat @@ -124,6 +125,12 @@ class PhysicsAnimator<T> private constructor (target: T) { private var defaultFling: FlingConfig = globalDefaultFling /** + * AnimationHandler to use if it need custom AnimationHandler, if this is null, it will use + * the default AnimationHandler in the DynamicAnimation. + */ + private var customAnimationHandler: AnimationHandler? = null + + /** * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add * just one permanent update and end listener to the DynamicAnimations. @@ -447,6 +454,14 @@ class PhysicsAnimator<T> private constructor (target: T) { this.defaultFling = defaultFling } + /** + * Set the custom AnimationHandler for all aniatmion in this animator. Set this with null for + * restoring to default AnimationHandler. + */ + fun setCustomAnimationHandler(handler: AnimationHandler) { + this.customAnimationHandler = handler + } + /** Starts the animations! */ fun start() { startAction() @@ -501,10 +516,13 @@ class PhysicsAnimator<T> private constructor (target: T) { // springs) on this property before flinging. cancel(animatedProperty) + // Apply the custom animation handler if it not null + val flingAnim = getFlingAnimation(animatedProperty, target) + flingAnim.animationHandler = + customAnimationHandler ?: flingAnim.animationHandler + // Apply the configuration and start the animation. - getFlingAnimation(animatedProperty, target) - .also { flingConfig.applyToAnimation(it) } - .start() + flingAnim.also { flingConfig.applyToAnimation(it) }.start() } } @@ -516,6 +534,21 @@ class PhysicsAnimator<T> private constructor (target: T) { if (flingConfig == null) { // Apply the configuration and start the animation. val springAnim = getSpringAnimation(animatedProperty, target) + + // If customAnimationHander is exist and has not been set to the animation, + // it should set here. + if (customAnimationHandler != null && + springAnim.animationHandler != customAnimationHandler) { + // Cancel the animation before set animation handler + if (springAnim.isRunning) { + cancel(animatedProperty) + } + // Apply the custom animation handler if it not null + springAnim.animationHandler = + customAnimationHandler ?: springAnim.animationHandler + } + + // Apply the configuration and start the animation. springConfig.applyToAnimation(springAnim) animationStartActions.add(springAnim::start) } else { @@ -570,10 +603,13 @@ class PhysicsAnimator<T> private constructor (target: T) { } } + // Apply the custom animation handler if it not null + val springAnim = getSpringAnimation(animatedProperty, target) + springAnim.animationHandler = + customAnimationHandler ?: springAnim.animationHandler + // Apply the configuration and start the spring animation. - getSpringAnimation(animatedProperty, target) - .also { springConfig.applyToAnimation(it) } - .start() + springAnim.also { springConfig.applyToAnimation(it) }.start() } } }) diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java b/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java index 2224c9cce40a..450336a6b73b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java +++ b/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java @@ -60,8 +60,8 @@ public class AsyncSensorManager extends SensorManager private final List<SensorManagerPlugin> mPlugins; @Inject - public AsyncSensorManager(Context context, PluginManager pluginManager) { - this(context.getSystemService(SensorManager.class), pluginManager, null); + public AsyncSensorManager(SensorManager sensorManager, PluginManager pluginManager) { + this(sensorManager, pluginManager, null); } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 3455ff47de8d..6fe11ed1792b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -935,6 +935,7 @@ public class VolumeDialogImpl implements VolumeDialog, protected void onStateChangedH(State state) { if (D.BUG) Log.d(TAG, "onStateChangedH() state: " + state.toString()); if (mState != null && state != null + && mState.ringerModeInternal != -1 && mState.ringerModeInternal != state.ringerModeInternal && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK)); diff --git a/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java b/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java index 926f653153ee..dd4ea578dafe 100644 --- a/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java +++ b/packages/SystemUI/src/com/android/systemui/wm/DisplayImeController.java @@ -228,12 +228,22 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mHandler.post(() -> { final Point lastSurfacePosition = mImeSourceControl != null ? mImeSourceControl.getSurfacePosition() : null; + final boolean positionChanged = + !activeControl.getSurfacePosition().equals(lastSurfacePosition); + final boolean leashChanged = + !haveSameLeash(mImeSourceControl, activeControl); mImeSourceControl = activeControl; - if (!activeControl.getSurfacePosition().equals(lastSurfacePosition) - && mAnimation != null) { - startAnimation(mImeShowing, true /* forceRestart */); - } else if (!mImeShowing) { - removeImeSurface(); + if (mAnimation != null) { + if (positionChanged) { + startAnimation(mImeShowing, true /* forceRestart */); + } + } else { + if (leashChanged) { + applyVisibilityToLeash(); + } + if (!mImeShowing) { + removeImeSurface(); + } } }); } @@ -241,6 +251,20 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + private void applyVisibilityToLeash() { + SurfaceControl leash = mImeSourceControl.getLeash(); + if (leash != null) { + SurfaceControl.Transaction t = mTransactionPool.acquire(); + if (mImeShowing) { + t.show(leash); + } else { + t.hide(leash); + } + t.apply(); + mTransactionPool.release(t); + } + } + @Override public void showInsets(int types, boolean fromIme) { if ((types & WindowInsets.Type.ime()) == 0) { @@ -259,6 +283,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged mHandler.post(() -> startAnimation(false /* show */, false /* forceRestart */)); } + @Override + public void topFocusedWindowChanged(String packageName) { + // no-op + } + /** * Sends the local visibility state back to window manager. Needed for legacy adjustForIme. */ @@ -487,4 +516,20 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged return IInputMethodManager.Stub.asInterface( ServiceManager.getService(Context.INPUT_METHOD_SERVICE)); } + + private static boolean haveSameLeash(InsetsSourceControl a, InsetsSourceControl b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + if (a.getLeash() == b.getLeash()) { + return true; + } + if (a.getLeash() == null || b.getLeash() == null) { + return false; + } + return a.getLeash().isSameSurface(b.getLeash()); + } } |