diff options
37 files changed, 857 insertions, 308 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index cd27fdf9c947..749b537ea364 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -290,6 +290,12 @@ public class BubbleStackView extends FrameLayout /** Whether we're in the middle of dragging the stack around by touch. */ private boolean mIsDraggingStack = false; + /** + * The pointer index of the ACTION_DOWN event we received prior to an ACTION_UP. We'll ignore + * touches from other pointer indices. + */ + private int mPointerIndexDown = -1; + /** Description of current animation controller state. */ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("Stack view state:"); @@ -2220,6 +2226,18 @@ public class BubbleStackView extends FrameLayout @Override public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() != MotionEvent.ACTION_DOWN && ev.getActionIndex() != mPointerIndexDown) { + // Ignore touches from additional pointer indices. + return false; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mPointerIndexDown = ev.getActionIndex(); + } else if (ev.getAction() == MotionEvent.ACTION_UP + || ev.getAction() == MotionEvent.ACTION_CANCEL) { + mPointerIndexDown = -1; + } + boolean dispatched = super.dispatchTouchEvent(ev); // If a new bubble arrives while the collapsed stack is being dragged, it will be positioned diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index 1a730c39dcd3..127c5dd54d72 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -39,9 +39,8 @@ class MediaCarouselController @Inject constructor( private val mediaHostStatesManager: MediaHostStatesManager, private val activityStarter: ActivityStarter, @Main executor: DelayableExecutor, - mediaManager: MediaDataCombineLatest, + mediaManager: MediaDataFilter, configurationController: ConfigurationController, - mediaDataManager: MediaDataManager, falsingManager: FalsingManager ) { /** @@ -148,7 +147,7 @@ class MediaCarouselController @Inject constructor( mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, - executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation, + executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation, falsingManager) isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL inflateSettingsButton() @@ -249,6 +248,7 @@ class MediaCarouselController @Inject constructor( 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) { @@ -256,16 +256,18 @@ class MediaCarouselController @Inject constructor( } else { mediaContent.addView(existingPlayer.view?.player) } - } else 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 + } 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) updatePageIndicator() mediaCarouselScrollHandler.onPlayersChanged() mediaCarousel.requiresRemeasuring = true diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt index 8c9cb1b240bf..dafc52ad8025 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt @@ -23,6 +23,7 @@ import android.media.session.MediaSession /** State of a media view. */ data class MediaData( + val userId: Int, val initialized: Boolean = false, val backgroundColor: Int, /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt index 11cbc482459a..0904ebccd414 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt @@ -58,6 +58,17 @@ class MediaDataCombineLatest @Inject constructor( } /** + * Get a map of all non-null data entries + */ + fun getData(): Map<String, MediaData> { + return entries.filter { + (key, pair) -> pair.first != null + }.mapValues { + (key, pair) -> pair.first!! + } + } + + /** * Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData]. */ fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener) diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt new file mode 100644 index 000000000000..662831e4a445 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt @@ -0,0 +1,153 @@ +/* + * 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.util.Log +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.qualifiers.Main +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" + +/** + * 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. + */ +@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() + + // The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager + private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + + init { + userTracker = object : CurrentUserTracker(broadcastDispatcher) { + override fun onUserSwitched(newUserId: Int) { + // Post this so we can be sure lockscreenUserManager already got the broadcast + executor.execute { handleUserSwitched(newUserId) } + } + } + userTracker.startTracking() + dataSource.addListener(this) + } + + override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { + if (!lockscreenUserManager.isCurrentProfile(data.userId)) { + return + } + + if (oldKey != null) { + mediaEntries.remove(oldKey) + } + mediaEntries.put(key, data) + + // Notify listeners + val listenersCopy = listeners.toSet() + listenersCopy.forEach { + it.onMediaDataLoaded(key, oldKey, data) + } + } + + override fun onMediaDataRemoved(key: String) { + mediaEntries.remove(key)?.let { + // Only notify listeners if something actually changed + val listenersCopy = listeners.toSet() + listenersCopy.forEach { + it.onMediaDataRemoved(key) + } + } + } + + @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() + // Clear the list first, to make sure callbacks from listeners if we have any entries + // are up to date + mediaEntries.clear() + keyCopy.forEach { + Log.d(TAG, "Removing $it after user change") + listenersCopy.forEach { listener -> + listener.onMediaDataRemoved(it) + } + } + + dataSource.getData().forEach { (key, data) -> + if (lockscreenUserManager.isCurrentProfile(data.userId)) { + Log.d(TAG, "Re-adding $key after user change") + mediaEntries.put(key, data) + listenersCopy.forEach { listener -> + listener.onMediaDataLoaded(key, null, data) + } + } + } + } + + /** + * Invoked when the user has dismissed the media carousel + */ + fun onSwipeToDismiss() { + val mediaKeys = mediaEntries.keys.toSet() + mediaKeys.forEach { + mediaDataManager.setTimedOut(it, timedOut = true) + } + } + + /** + * Are there any media notifications active? + */ + fun hasActiveMedia() = mediaEntries.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() + } + + /** + * 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) +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index d6b6660b778c..8cb93bfc6d4d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -67,7 +67,7 @@ private const val DEFAULT_LUMINOSITY = 0.25f private const val LUMINOSITY_THRESHOLD = 0.05f private const val SATURATION_MULTIPLIER = 0.8f -private val LOADING = MediaData(false, 0, null, null, null, null, null, +private val LOADING = MediaData(-1, false, 0, null, null, null, null, null, emptyList(), emptyList(), "INVALID", null, null, null, true, null) fun isMediaNotification(sbn: StatusBarNotification): Boolean { @@ -116,15 +116,6 @@ class MediaDataManager( broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context)) - private val userChangeReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (Intent.ACTION_USER_SWITCHED == intent.action) { - // Remove all controls, regardless of state - clearData() - } - } - } - private val appChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { @@ -152,9 +143,6 @@ class MediaDataManager( mediaResumeListener.setManager(this) addListener(mediaResumeListener) - val userFilter = IntentFilter(Intent.ACTION_USER_SWITCHED) - broadcastDispatcher.registerReceiver(userChangeReceiver, userFilter, null, UserHandle.ALL) - val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) @@ -169,7 +157,6 @@ class MediaDataManager( fun destroy() { context.unregisterReceiver(appChangeReceiver) - broadcastDispatcher.unregisterReceiver(userChangeReceiver) } fun onNotificationAdded(key: String, sbn: StatusBarNotification) { @@ -190,20 +177,6 @@ class MediaDataManager( } } - private fun clearData() { - // Called on user change. Remove all current MediaData objects and inform listeners - val listenersCopy = listeners.toSet() - val keyCopy = mediaEntries.keys.toMutableList() - // Clear the list first, to make sure callbacks from listeners if we have any entries - // are up to date - mediaEntries.clear() - keyCopy.forEach { - listenersCopy.forEach { listener -> - listener.onMediaDataRemoved(it) - } - } - } - private fun removeAllForPackage(packageName: String) { Assert.isMainThread() val listenersCopy = listeners.toSet() @@ -224,6 +197,7 @@ class MediaDataManager( } fun addResumptionControls( + userId: Int, desc: MediaDescription, action: Runnable, token: MediaSession.Token, @@ -238,7 +212,8 @@ class MediaDataManager( mediaEntries.put(packageName, resumeData) } backgroundExecutor.execute { - loadMediaDataInBgForResumption(desc, action, token, appName, appIntent, packageName) + loadMediaDataInBgForResumption(userId, desc, action, token, appName, appIntent, + packageName) } } @@ -282,7 +257,7 @@ class MediaDataManager( * This will make the player not active anymore, hiding it from QQS and Keyguard. * @see MediaData.active */ - private fun setTimedOut(token: String, timedOut: Boolean) { + internal fun setTimedOut(token: String, timedOut: Boolean) { mediaEntries[token]?.let { if (it.active == !timedOut) { return @@ -293,6 +268,7 @@ class MediaDataManager( } private fun loadMediaDataInBgForResumption( + userId: Int, desc: MediaDescription, resumeAction: Runnable, token: MediaSession.Token, @@ -307,7 +283,7 @@ class MediaDataManager( return } - Log.d(TAG, "adding track from browser: $desc") + Log.d(TAG, "adding track for $userId from browser: $desc") // Album art var artworkBitmap = desc.iconBitmap @@ -323,7 +299,7 @@ class MediaDataManager( val mediaAction = getResumeMediaAction(resumeAction) foregroundExecutor.execute { - onMediaDataLoaded(packageName, null, MediaData(true, bgColor, appName, + onMediaDataLoaded(packageName, null, MediaData(userId, true, bgColor, appName, null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0), packageName, token, appIntent, device = null, active = false, resumeAction = resumeAction, resumption = true, notificationKey = packageName, @@ -439,10 +415,11 @@ class MediaDataManager( val resumeAction: Runnable? = mediaEntries[key]?.resumeAction val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true val active = mediaEntries[key]?.active ?: true - onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist, - song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token, - notif.contentIntent, null, active, resumeAction = resumeAction, - notificationKey = key, hasCheckedForResume = hasCheckedForResume)) + 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)) } } @@ -564,18 +541,6 @@ class MediaDataManager( } } - /** - * Are there any media notifications active? - */ - fun hasActiveMedia() = mediaEntries.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 (useMediaResumption) mediaEntries.isNotEmpty() else hasActiveMedia() - fun setMediaResumptionEnabled(isEnabled: Boolean) { if (useMediaResumption == isEnabled) { return @@ -596,16 +561,6 @@ class MediaDataManager( } } - /** - * Invoked when the user has dismissed the media carousel - */ - fun onSwipeToDismiss() { - val mediaKeys = mediaEntries.keys.toSet() - mediaKeys.forEach { - setTimedOut(it, timedOut = true) - } - } - interface Listener { /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 570de635ea1d..3598719fcb3a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -14,8 +14,7 @@ import javax.inject.Inject class MediaHost @Inject constructor( private val state: MediaHostStateHolder, private val mediaHierarchyManager: MediaHierarchyManager, - private val mediaDataManager: MediaDataManager, - private val mediaDataManagerCombineLatest: MediaDataCombineLatest, + private val mediaDataFilter: MediaDataFilter, private val mediaHostStatesManager: MediaHostStatesManager ) : MediaHostState by state { lateinit var hostView: UniqueObjectHostView @@ -80,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 - mediaDataManagerCombineLatest.addListener(listener) + mediaDataFilter.addListener(listener) updateViewVisibility() } override fun onViewDetachedFromWindow(v: View?) { - mediaDataManagerCombineLatest.removeListener(listener) + mediaDataFilter.removeListener(listener) } }) @@ -114,9 +113,9 @@ class MediaHost @Inject constructor( private fun updateViewVisibility() { visible = if (showsOnlyActiveMedia) { - mediaDataManager.hasActiveMedia() + mediaDataFilter.hasActiveMedia() } else { - mediaDataManager.hasAnyMedia() + mediaDataFilter.hasAnyMedia() } val newVisibility = if (visible) View.VISIBLE else View.GONE if (newVisibility != hostView.visibility) { diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt index 0cc1e7bb1b56..4ec746fcb153 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt @@ -56,7 +56,7 @@ class MediaResumeListener @Inject constructor( private lateinit var mediaDataManager: MediaDataManager private var mediaBrowser: ResumeMediaBrowser? = null - private var currentUserId: Int + private var currentUserId: Int = context.userId private val userChangeReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { @@ -65,7 +65,6 @@ class MediaResumeListener @Inject constructor( } else if (Intent.ACTION_USER_SWITCHED == intent.action) { currentUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1) loadSavedComponents() - loadMediaResumptionControls() } } } @@ -89,13 +88,12 @@ class MediaResumeListener @Inject constructor( } Log.d(TAG, "Adding resume controls $desc") - mediaDataManager.addResumptionControls(desc, resumeAction, token, appName.toString(), - appIntent, component.packageName) + mediaDataManager.addResumptionControls(currentUserId, desc, resumeAction, token, + appName.toString(), appIntent, component.packageName) } } init { - currentUserId = context.userId if (useMediaResumption) { val unlockFilter = IntentFilter() unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED) @@ -118,6 +116,8 @@ 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() diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt index 9a134dbe0264..8662aacfdab2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt @@ -54,7 +54,32 @@ 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. + 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() + if (DEBUG) Log.d(TAG, "migrating key $oldKey to $key, for resumption") + } 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) { @@ -71,7 +96,7 @@ class MediaTimeoutListener @Inject constructor( ) : MediaController.Callback() { var timedOut = false - private var playing: Boolean? = null + var playing: Boolean? = null // Resume controls may have null token private val mediaController = if (data.token != null) { @@ -83,7 +108,9 @@ class MediaTimeoutListener @Inject constructor( init { mediaController?.registerCallback(this) - onPlaybackStateChanged(mediaController?.playbackState) + // 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) } fun destroy() { @@ -91,8 +118,12 @@ class MediaTimeoutListener @Inject constructor( } override fun onPlaybackStateChanged(state: PlaybackState?) { + processState(state, dispatchEvents = true) + } + + private fun processState(state: PlaybackState?, dispatchEvents: Boolean) { if (DEBUG) { - Log.v(TAG, "onPlaybackStateChanged: $state") + Log.v(TAG, "processState: $state") } val isPlaying = state != null && isPlayingState(state.state) @@ -116,12 +147,16 @@ class MediaTimeoutListener @Inject constructor( Log.v(TAG, "Execute timeout for $key") } timedOut = true - timeoutCallback(key, timedOut) + if (dispatchEvents) { + timeoutCallback(key, timedOut) + } }, PAUSED_MEDIA_TIMEOUT) } else { expireMediaTimeout(key, "playback started - $state, $key") timedOut = false - timeoutCallback(key, timedOut) + if (dispatchEvents) { + timeoutCallback(key, timedOut) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index 83cfdd5f2699..38817d7b579e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -268,7 +268,6 @@ class MediaViewController @Inject constructor( fun attach(transitionLayout: TransitionLayout) { this.transitionLayout = transitionLayout layoutController.attach(transitionLayout) - ensureAllMeasurements() if (currentEndLocation == -1) { return } @@ -414,13 +413,16 @@ class MediaViewController @Inject constructor( * Clear all existing measurements and refresh the state to match the view. */ fun refreshState() { - if (!firstRefresh) { - // Let's clear all of our measurements and recreate them! - viewStates.clear() - setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress, - applyImmediately = true) + // Let's clear all of our measurements and recreate them! + viewStates.clear() + if (firstRefresh) { + // This is the first bind, let's ensure we pre-cache all measurements. Otherwise + // We'll just load these on demand. + ensureAllMeasurements() + firstRefresh = false } - firstRefresh = false + setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress, + applyImmediately = true) } } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java index 2980f11b3cbc..ead17867844a 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java @@ -18,16 +18,16 @@ package com.android.systemui.pip; import android.animation.AnimationHandler; 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; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Interpolators; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -76,7 +76,6 @@ public class PipAnimationController { || direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN; } - private final Interpolator mFastOutSlowInInterpolator; private final PipSurfaceTransactionHelper mSurfaceTransactionHelper; private PipTransitionAnimator mCurrentAnimator; @@ -90,8 +89,6 @@ public class PipAnimationController { @Inject PipAnimationController(Context context, PipSurfaceTransactionHelper helper) { - mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, - com.android.internal.R.interpolator.fast_out_slow_in); mSurfaceTransactionHelper = helper; } @@ -113,10 +110,11 @@ public class PipAnimationController { } @SuppressWarnings("unchecked") - PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds) { + PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds, + Rect sourceHintRect) { if (mCurrentAnimator == null) { mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds)); + PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect)); } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA && mCurrentAnimator.isRunning()) { // If we are still animating the fade into pip, then just move the surface and ensure @@ -131,7 +129,7 @@ public class PipAnimationController { } else { mCurrentAnimator.cancel(); mCurrentAnimator = setupPipTransitionAnimator( - PipTransitionAnimator.ofBounds(leash, startBounds, endBounds)); + PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect)); } return mCurrentAnimator; } @@ -142,7 +140,7 @@ public class PipAnimationController { private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) { animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper); - animator.setInterpolator(mFastOutSlowInInterpolator); + animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); animator.setFloatValues(FRACTION_START, FRACTION_END); animator.setAnimationHandler(mSfAnimationHandlerThreadLocal.get()); return animator; @@ -341,6 +339,7 @@ public class PipAnimationController { @Override void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { getSurfaceTransactionHelper() + .resetScale(tx, leash, getDestinationBounds()) .crop(tx, leash, getDestinationBounds()) .round(tx, leash, shouldApplyCornerRadius()); tx.show(leash); @@ -356,35 +355,46 @@ public class PipAnimationController { } static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash, - Rect startValue, Rect endValue) { + Rect startValue, Rect endValue, Rect sourceHintRect) { + // Just for simplicity we'll interpolate between the source rect hint insets and empty + // insets to calculate the window crop + final Rect initialStartValue = new Rect(startValue); + final Rect sourceHintRectInsets = sourceHintRect != null + ? new Rect(sourceHintRect.left - startValue.left, + sourceHintRect.top - startValue.top, + startValue.right - sourceHintRect.right, + startValue.bottom - sourceHintRect.bottom) + : null; + final Rect sourceInsets = new Rect(0, 0, 0, 0); + // construct new Rect instances in case they are recycled return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS, endValue, new Rect(startValue), new Rect(endValue)) { - private final Rect mTmpRect = new Rect(); - - private int getCastedFractionValue(float start, float end, float fraction) { - return (int) (start * (1 - fraction) + end * fraction + .5f); - } + private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); + private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); @Override void applySurfaceControlTransaction(SurfaceControl leash, SurfaceControl.Transaction tx, float fraction) { final Rect start = getStartValue(); final Rect end = getEndValue(); - mTmpRect.set( - getCastedFractionValue(start.left, end.left, fraction), - getCastedFractionValue(start.top, end.top, fraction), - getCastedFractionValue(start.right, end.right, fraction), - getCastedFractionValue(start.bottom, end.bottom, fraction)); - setCurrentValue(mTmpRect); + Rect bounds = mRectEvaluator.evaluate(fraction, start, end); + setCurrentValue(bounds); if (inScaleTransition()) { if (isOutPipDirection(getTransitionDirection())) { - getSurfaceTransactionHelper().scale(tx, leash, end, mTmpRect); + getSurfaceTransactionHelper().scale(tx, leash, end, bounds); } else { - getSurfaceTransactionHelper().scale(tx, leash, start, mTmpRect); + getSurfaceTransactionHelper().scale(tx, leash, start, bounds); } } else { - getSurfaceTransactionHelper().crop(tx, leash, mTmpRect); + if (sourceHintRectInsets != null) { + Rect insets = mInsetsEvaluator.evaluate(fraction, sourceInsets, + sourceHintRectInsets); + getSurfaceTransactionHelper().scaleAndCrop(tx, leash, initialStartValue, + bounds, insets); + } else { + getSurfaceTransactionHelper().scale(tx, leash, start, bounds); + } } tx.apply(); } @@ -400,11 +410,11 @@ public class PipAnimationController { @Override void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) { - if (!inScaleTransition()) return; // NOTE: intentionally does not apply the transaction here. // this end transaction should get executed synchronously with the final // WindowContainerTransaction in task organizer - getSurfaceTransactionHelper().resetScale(tx, leash, getDestinationBounds()) + 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 0d3a16ec1028..8bbd15babf19 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java @@ -289,6 +289,24 @@ public class PipBoundsHandler { } /** + * Updatest the display info and display layout on rotation change. This is needed even when we + * aren't in PIP because the rotation layout is used to calculate the proper insets for the + * next enter animation into PIP. + */ + public void onDisplayRotationChangedNotInPip(int toRotation) { + // Update the display layout, note that we have to do this on every rotation even if we + // aren't in PIP since we need to update the display layout to get the right resources + mDisplayLayout.rotateTo(mContext.getResources(), toRotation); + + // Populate the new {@link #mDisplayInfo}. + // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation, + // therefore, the width/height may require a swap first. + // Moving forward, we should get the new dimensions after rotation from DisplayLayout. + mDisplayInfo.rotation = toRotation; + updateDisplayInfoIfNeeded(); + } + + /** * Updates the display info, calculating and returning the new stack and movement bounds in the * new orientation of the device if necessary. * diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java index fc41d2ea8862..65ea887259be 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java @@ -44,6 +44,7 @@ public class PipSurfaceTransactionHelper implements ConfigurationController.Conf private final float[] mTmpFloat9 = new float[9]; private final RectF mTmpSourceRectF = new RectF(); private final RectF mTmpDestinationRectF = new RectF(); + private final Rect mTmpDestinationRect = new Rect(); @Inject public PipSurfaceTransactionHelper(Context context, ConfigurationController configController) { @@ -90,7 +91,30 @@ public class PipSurfaceTransactionHelper implements ConfigurationController.Conf mTmpDestinationRectF.set(destinationBounds); mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) - .setPosition(leash, destinationBounds.left, destinationBounds.top); + .setPosition(leash, mTmpDestinationRectF.left, mTmpDestinationRectF.top); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds, Rect insets) { + mTmpSourceRectF.set(sourceBounds); + mTmpDestinationRect.set(sourceBounds); + mTmpDestinationRect.inset(insets); + // Scale by the shortest edge and offset such that the top/left of the scaled inset source + // rect aligns with the top/left of the destination bounds + final float scale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + final float left = destinationBounds.left - insets.left * scale; + final float top = destinationBounds.top - insets.top * scale; + mTmpTransform.setScale(scale, scale); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setWindowCrop(leash, mTmpDestinationRect) + .setPosition(leash, left, top); return this; } diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java index c8a1ca02fdfb..0141dee04086 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java +++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java @@ -143,8 +143,10 @@ public class PipTaskOrganizer extends TaskOrganizer implements case MSG_RESIZE_ANIMATE: { Rect currentBounds = (Rect) args.arg2; Rect toBounds = (Rect) args.arg3; + Rect sourceHintRect = (Rect) args.arg4; int duration = args.argi2; - animateResizePip(currentBounds, toBounds, args.argi1 /* direction */, duration); + animateResizePip(currentBounds, toBounds, sourceHintRect, + args.argi1 /* direction */, duration); if (updateBoundsCallback != null) { updateBoundsCallback.accept(toBounds); } @@ -307,7 +309,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements public void onTransactionReady(int id, SurfaceControl.Transaction t) { t.apply(); scheduleAnimateResizePip(mLastReportedBounds, destinationBounds, - direction, animationDurationMs, null /* updateBoundsCallback */); + null /* sourceHintRect */, direction, animationDurationMs, + null /* updateBoundsCallback */); mInPip = false; } }); @@ -380,7 +383,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) { - scheduleAnimateResizePip(currentBounds, destinationBounds, + final Rect sourceHintRect = getValidSourceHintRect(info, currentBounds); + scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect, TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration, null /* updateBoundsCallback */); } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) { @@ -391,6 +395,21 @@ public class PipTaskOrganizer extends TaskOrganizer implements } } + /** + * Returns the source hint rect if it is valid (if provided and is contained by the current + * task bounds). + */ + private Rect getValidSourceHintRect(ActivityManager.RunningTaskInfo info, Rect sourceBounds) { + final Rect sourceHintRect = info.pictureInPictureParams != null + && info.pictureInPictureParams.hasSourceBoundsHint() + ? info.pictureInPictureParams.getSourceRectHint() + : null; + if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) { + return sourceHintRect; + } + return null; + } + private void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) { // If we are fading the PIP in, then we should move the pip to the final location as // soon as possible, but set the alpha immediately since the transaction can take a @@ -611,13 +630,13 @@ public class PipTaskOrganizer extends TaskOrganizer implements Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred"); return; } - scheduleAnimateResizePip(mLastReportedBounds, toBounds, + scheduleAnimateResizePip(mLastReportedBounds, toBounds, null /* sourceHintRect */, TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback); } private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds, - @PipAnimationController.TransitionDirection int direction, int durationMs, - Consumer<Rect> updateBoundsCallback) { + Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, + int durationMs, Consumer<Rect> updateBoundsCallback) { if (!mInPip) { // TODO: tend to use shouldBlockResizeRequest here as well but need to consider // the fact that when in exitPip, scheduleAnimateResizePip is executed in the window @@ -629,6 +648,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements args.arg1 = updateBoundsCallback; args.arg2 = currentBounds; args.arg3 = destinationBounds; + args.arg4 = sourceHintRect; args.argi1 = direction; args.argi2 = durationMs; mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args)); @@ -732,7 +752,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements } final Rect destinationBounds = new Rect(originalBounds); destinationBounds.offset(xOffset, yOffset); - animateResizePip(originalBounds, destinationBounds, TRANSITION_DIRECTION_SAME, durationMs); + animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */, + TRANSITION_DIRECTION_SAME, durationMs); } private void resizePip(Rect destinationBounds) { @@ -838,7 +859,8 @@ public class PipTaskOrganizer extends TaskOrganizer implements return WINDOWING_MODE_UNDEFINED; } - private void animateResizePip(Rect currentBounds, Rect destinationBounds, + + private void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction, int durationMs) { if (Looper.myLooper() != mUpdateHandler.getLooper()) { throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of " @@ -850,7 +872,7 @@ public class PipTaskOrganizer extends TaskOrganizer implements return; } mPipAnimationController - .getAnimator(mLeash, currentBounds, destinationBounds) + .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect) .setTransitionDirection(direction) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(durationMs) 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 40a86b78d3ad..7d35416a8d1d 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipManager.java @@ -36,7 +36,6 @@ import android.util.Log; import android.util.Pair; import android.view.DisplayInfo; import android.view.IPinnedStackController; -import android.view.SurfaceControl; import android.window.WindowContainerTransaction; import com.android.systemui.Dependency; @@ -96,7 +95,9 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) -> { if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isDeferringEnterPipAnimation()) { - // Skip if we aren't in PIP or haven't actually entered PIP yet + // Skip if we aren't in PIP or haven't actually entered PIP yet. We still need to update + // the display layout in the bounds handler in this case. + mPipBoundsHandler.onDisplayRotationChangedNotInPip(toRotation); return; } // If there is an animation running (ie. from a shelf offset), then ensure that we calculate @@ -174,7 +175,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { - if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() + if (task.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_PINNED) { return; } 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 a4edacecfd91..1ca53f907994 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipResizeGestureHandler.java @@ -55,7 +55,9 @@ import com.android.systemui.pip.PipBoundsHandler; import com.android.systemui.pip.PipTaskOrganizer; 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; /** @@ -94,7 +96,7 @@ public class PipResizeGestureHandler { private final Rect mTmpBottomLeftCorner = new Rect(); private final Rect mTmpBottomRightCorner = new Rect(); private final Rect mDisplayBounds = new Rect(); - private final Supplier<Rect> mMovementBoundsSupplier; + private final Function<Rect, Rect> mMovementBoundsSupplier; private final Runnable mUpdateMovementBoundsRunnable; private int mDelta; @@ -113,7 +115,7 @@ public class PipResizeGestureHandler { public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, - PipTaskOrganizer pipTaskOrganizer, Supplier<Rect> movementBoundsSupplier, + PipTaskOrganizer pipTaskOrganizer, Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, SysUiState sysUiState) { mContext = context; mDisplayId = context.getDisplayId(); @@ -244,10 +246,15 @@ public class PipResizeGestureHandler { return mTmpRegion.contains(x, y); } + public boolean willStartResizeGesture(MotionEvent ev) { + return mEnableUserResize && isInValidSysUiState() + && isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY()); + } + private void setCtrlType(int x, int y) { final Rect currentPipBounds = mMotionHelper.getBounds(); - Rect movementBounds = mMovementBoundsSupplier.get(); + Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); mDisplayBounds.set(movementBounds.left, movementBounds.top, movementBounds.right + currentPipBounds.width(), @@ -353,6 +360,16 @@ public class PipResizeGestureHandler { mMinSize.set(minX, minY); } + public void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); + pw.println(innerPrefix + "mIsAttached=" + mIsAttached); + pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); + pw.println(innerPrefix + "mEnableUserResize=" + mEnableUserResize); + pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); + } + class SysUiInputEventReceiver extends BatchedInputEventReceiver { SysUiInputEventReceiver(InputChannel channel, Looper looper) { super(channel, looper, Choreographer.getSfInstance()); 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 3e06c1eb1ca3..b6e4e1628c20 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java @@ -644,12 +644,12 @@ public class PipTouchHandler { } MotionEvent ev = (MotionEvent) inputEvent; - if (!mTouchState.isDragging() - && !mMagnetizedPip.getObjectStuckToTarget() - && !mMotionHelper.isAnimating() - && mPipResizeGestureHandler.isWithinTouchRegion( - (int) ev.getRawX(), (int) ev.getRawY())) { + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN + && mPipResizeGestureHandler.willStartResizeGesture(ev)) { + // Initialize the touch state for the gesture, but immediately reset to invalidate the + // gesture mTouchState.onTouchEvent(ev); + mTouchState.reset(); return true; } @@ -1032,8 +1032,11 @@ public class PipTouchHandler { isMenuExpanded && willResizeMenu() ? mExpandedShortestEdgeSize : 0); } - private Rect getMovementBounds() { - return mMovementBounds; + private Rect getMovementBounds(Rect curBounds) { + Rect movementBounds = new Rect(); + mSnapAlgorithm.getMovementBounds(curBounds, mInsetBounds, + movementBounds, mIsImeShowing ? mImeHeight : 0); + return movementBounds; } /** @@ -1065,6 +1068,9 @@ public class PipTouchHandler { pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets); mTouchState.dump(pw, innerPrefix); mMotionHelper.dump(pw, innerPrefix); + if (mPipResizeGestureHandler != null) { + mPipResizeGestureHandler.dump(pw, innerPrefix); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java index 628223630af7..4b2c27321035 100644 --- a/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java +++ b/packages/SystemUI/src/com/android/systemui/pip/tv/PipManager.java @@ -716,7 +716,7 @@ public class PipManager implements BasePipManager, PipTaskOrganizer.PipTransitio @Override public void onActivityRestartAttempt(RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { - if (!wasVisible || task.configuration.windowConfiguration.getWindowingMode() + if (task.configuration.windowConfiguration.getWindowingMode() != WINDOWING_MODE_PINNED) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 5628a24f40ef..739d30c2a707 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -100,12 +100,7 @@ public class NotificationMediaManager implements Dumpable { PAUSED_MEDIA_STATES.add(PlaybackState.STATE_STOPPED); PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED); PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR); - } - private static final HashSet<Integer> INACTIVE_MEDIA_STATES = new HashSet<>(); - static { - INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_NONE); - INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_STOPPED); - INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_ERROR); + PAUSED_MEDIA_STATES.add(PlaybackState.STATE_CONNECTING); } private final NotificationEntryManager mEntryManager; @@ -262,15 +257,6 @@ public class NotificationMediaManager implements Dumpable { return !PAUSED_MEDIA_STATES.contains(state); } - /** - * Check if a state should be considered active (playing or paused) - * @param state a PlaybackState - * @return true if active - */ - public static boolean isActiveState(int state) { - return !INACTIVE_MEDIA_STATES.contains(state); - } - public void setUpWithPresenter(NotificationPresenter presenter) { mPresenter = presenter; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt index b7f317b38743..c63781cb110a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt @@ -64,6 +64,7 @@ private const val DEVICE_NAME = "DEVICE_NAME" private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" +private const val USER_ID = 0 @SmallTest @RunWith(AndroidTestingRunner::class) @@ -180,7 +181,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindWhenUnattached() { - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, null, null, device, true, null) player.bind(state) assertThat(player.isPlaying()).isFalse() @@ -189,7 +190,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindText() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null) player.bind(state) assertThat(appName.getText()).isEqualTo(APP) @@ -200,7 +201,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindBackgroundColor() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null) player.bind(state) val list = ArgumentCaptor.forClass(ColorStateList::class.java) @@ -211,7 +212,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindDevice() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null) player.bind(state) assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME) @@ -223,7 +224,7 @@ public class MediaControlPanelTest : SysuiTestCase() { seamless.id = 1 seamlessFallback.id = 2 player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, true, null) player.bind(state) verify(expandedSet).setVisibility(seamless.id, View.GONE) @@ -235,7 +236,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindNullDevice() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null) player.bind(state) assertThat(seamless.isEnabled()).isTrue() @@ -246,7 +247,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindDeviceResumptionPlayer() { player.attach(holder) - val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null, resumption = true) player.bind(state) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java index 9fdd9ad744ff..5d4693d3ccf8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java @@ -52,6 +52,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { private static final String ARTIST = "ARTIST"; private static final String TITLE = "TITLE"; private static final String DEVICE_NAME = "DEVICE_NAME"; + private static final int USER_ID = 0; private MediaDataCombineLatest mManager; @@ -78,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { mManager.addListener(mListener); - mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, + mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, true, null, false, KEY, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME); diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt new file mode 100644 index 000000000000..afb64a7649b4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt @@ -0,0 +1,216 @@ +/* + * 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.graphics.Color +import androidx.test.filters.SmallTest +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import java.util.concurrent.Executor + +private const val KEY = "TEST_KEY" +private const val KEY_ALT = "TEST_KEY_2" +private const val USER_MAIN = 0 +private const val USER_GUEST = 10 +private const val APP = "APP" +private const val BG_COLOR = Color.RED +private const val PACKAGE = "PKG" +private const val ARTIST = "ARTIST" +private const val TITLE = "TITLE" +private const val DEVICE_NAME = "DEVICE_NAME" + +private fun <T> eq(value: T): T = Mockito.eq(value) ?: value +private fun <T> any(): T = Mockito.any() + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaDataFilterTest : SysuiTestCase() { + + @Mock + private lateinit var combineLatest: MediaDataCombineLatest + @Mock + private lateinit var listener: MediaDataManager.Listener + @Mock + private lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock + private lateinit var mediaResumeListener: MediaResumeListener + @Mock + private lateinit var mediaDataManager: MediaDataManager + @Mock + private lateinit var lockscreenUserManager: NotificationLockscreenUserManager + @Mock + private lateinit var executor: Executor + + private lateinit var mediaDataFilter: MediaDataFilter + private lateinit var dataMain: MediaData + private lateinit var dataGuest: MediaData + private val device = MediaDeviceData(true, null, DEVICE_NAME) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + mediaDataFilter = MediaDataFilter(combineLatest, broadcastDispatcher, mediaResumeListener, + mediaDataManager, lockscreenUserManager, executor) + mediaDataFilter.addListener(listener) + + // Start all tests as main user + setUser(USER_MAIN) + + // Set up test media data + dataMain = MediaData(USER_MAIN, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), + emptyList(), PACKAGE, null, null, device, true, null) + + dataGuest = MediaData(USER_GUEST, true, BG_COLOR, APP, null, ARTIST, TITLE, null, + emptyList(), emptyList(), PACKAGE, null, null, device, true, null) + } + + private fun setUser(id: Int) { + `when`(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false) + `when`(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true) + mediaDataFilter.handleUserSwitched(id) + } + + @Test + fun testOnDataLoadedForCurrentUser_callsListener() { + // GIVEN a media for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // THEN we should tell the listener + verify(listener).onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain)) + } + + @Test + fun testOnDataLoadedForGuest_doesNotCallListener() { + // GIVEN a media for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + + // THEN we should NOT tell the listener + verify(listener, never()).onMediaDataLoaded(any(), any(), any()) + } + + @Test + fun testOnRemovedForCurrent_callsListener() { + // GIVEN a media was removed for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should tell the listener + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnRemovedForGuest_doesNotCallListener() { + // GIVEN a media was removed for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should NOT tell the listener + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_removesOldUserControls() { + // GIVEN that we have a media loaded for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should remove the main user's media + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_addsNewUserControls() { + // GIVEN that we had some media for both users + val dataMap = mapOf(KEY to dataMain, KEY_ALT to dataGuest) + `when`(combineLatest.getData()).thenReturn(dataMap) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should add back the guest user media + verify(listener).onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest)) + + // but not the main user's + verify(listener, never()).onMediaDataLoaded(eq(KEY), any(), eq(dataMain)) + } + + @Test + fun testHasAnyMedia() { + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + assertThat(mediaDataFilter.hasAnyMedia()).isTrue() + } + + @Test + fun testHasActiveMedia() { + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() + val data = dataMain.copy(active = true) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + assertThat(mediaDataFilter.hasActiveMedia()).isTrue() + } + + @Test + fun testHasAnyMedia_onlyCurrentUser() { + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest) + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + } + + @Test + fun testHasActiveMedia_onlyCurrentUser() { + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() + val data = dataGuest.copy(active = true) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + assertThat(mediaDataFilter.hasActiveMedia()).isFalse() + } + + @Test + fun testOnNotificationRemoved_doesntHaveMedia() { + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + assertThat(mediaDataFilter.hasAnyMedia()).isFalse() + } + + @Test + fun testOnSwipeToDismiss_setsTimedOut() { + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onSwipeToDismiss() + + verify(mediaDataManager).setTimedOut(eq(KEY), eq(true)) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt index e56bbabfdc0b..6761b282b26a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt @@ -35,6 +35,7 @@ private const val PACKAGE_NAME = "com.android.systemui" private const val APP_NAME = "SystemUI" private const val SESSION_ARTIST = "artist" private const val SESSION_TITLE = "title" +private const val USER_ID = 0 private fun <T> anyObject(): T { return Mockito.anyObject<T>() @@ -91,28 +92,15 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test - fun testHasActiveMedia() { - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - val data = mock(MediaData::class.java) - - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - - whenever(data.active).thenReturn(true) - assertThat(mediaDataManager.hasActiveMedia()).isTrue() - } - - @Test - fun testOnSwipeToDismiss_deactivatesMedia() { - val data = MediaData(initialized = true, backgroundColor = 0, app = null, appIcon = null, - artist = null, song = null, artwork = null, actions = emptyList(), + fun testSetTimedOut_deactivatesMedia() { + val data = MediaData(userId = USER_ID, initialized = true, backgroundColor = 0, app = null, + appIcon = null, artist = null, song = null, artwork = null, actions = emptyList(), actionsToShowInCompact = emptyList(), packageName = "INVALID", token = null, clickIntent = null, device = null, active = true, resumeAction = null) mediaDataManager.onNotificationAdded(KEY, mediaNotification) mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - mediaDataManager.onSwipeToDismiss() + mediaDataManager.setTimedOut(KEY, timedOut = true) assertThat(data.active).isFalse() } @@ -141,37 +129,6 @@ class MediaDataManagerTest : SysuiTestCase() { assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) assertThat(listener.data!!.active).isTrue() - - // Swiping away makes the notification not active - mediaDataManager.onSwipeToDismiss() - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - - // And when a notification is updated - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) - assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) - - // MediaData should still be inactive - assertThat(mediaDataManager.hasActiveMedia()).isFalse() - } - - @Test - fun testHasAnyMedia_whenAddingMedia() { - assertThat(mediaDataManager.hasAnyMedia()).isFalse() - val data = mock(MediaData::class.java) - - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - assertThat(mediaDataManager.hasAnyMedia()).isTrue() - } - - @Test - fun testOnNotificationRemoved_doesntHaveMedia() { - val data = mock(MediaData::class.java) - mediaDataManager.onNotificationAdded(KEY, mediaNotification) - mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = data) - mediaDataManager.onNotificationRemoved(KEY) - assertThat(mediaDataManager.hasAnyMedia()).isFalse() } @Test @@ -212,8 +169,8 @@ class MediaDataManagerTest : SysuiTestCase() { setTitle(SESSION_TITLE) build() } - mediaDataManager.addResumptionControls(desc, Runnable {}, session.sessionToken, APP_NAME, - pendingIntent, PACKAGE_NAME) + mediaDataManager.addResumptionControls(USER_ID, desc, Runnable {}, session.sessionToken, + APP_NAME, pendingIntent, PACKAGE_NAME) assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) // THEN the media data indicates that it is for resumption diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt index 6c7f2e8d7925..fc22eeb3ea68 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt @@ -58,6 +58,7 @@ private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" private const val DEVICE_NAME = "DEVICE_NAME" +private const val USER_ID = 0 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value @@ -118,7 +119,7 @@ public class MediaDeviceManagerTest : SysuiTestCase() { setSmallIcon(android.R.drawable.ic_media_pause) setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken())) } - mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null, + mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, device = null, active = true, resumeAction = null) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt index 916fd0fe11b7..7a8e4f7e9b85 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt @@ -32,7 +32,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyLong +import org.mockito.ArgumentMatchers.anyString import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito @@ -48,6 +50,7 @@ private const val PACKAGE = "PKG" private const val SESSION_KEY = "SESSION_KEY" private const val SESSION_ARTIST = "SESSION_ARTIST" private const val SESSION_TITLE = "SESSION_TITLE" +private const val USER_ID = 0 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value private fun <T> anyObject(): T { @@ -93,7 +96,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { setPlaybackState(playbackBuilder.build()) } session.setActive(true) - mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null, + mediaData = MediaData(USER_ID, true, 0, PACKAGE, null, null, SESSION_TITLE, null, emptyList(), emptyList(), PACKAGE, session.sessionToken, clickIntent = null, device = null, active = true, resumeAction = null) } @@ -118,6 +121,7 @@ class MediaTimeoutListenerTest : SysuiTestCase() { mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) verify(mediaController).registerCallback(capture(mediaCallbackCaptor)) verify(executor).executeDelayed(capture(timeoutCaptor), anyLong()) + verify(timeoutCallback, never()).invoke(anyString(), anyBoolean()) } @Test @@ -133,6 +137,24 @@ class MediaTimeoutListenerTest : SysuiTestCase() { } @Test + fun testOnMediaDataLoaded_migratesKeys() { + // From not playing + mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData) + clearInvocations(mediaController) + + // To playing + val playingState = mock(android.media.session.PlaybackState::class.java) + `when`(playingState.state).thenReturn(PlaybackState.STATE_PLAYING) + `when`(mediaController.playbackState).thenReturn(playingState) + mediaTimeoutListener.onMediaDataLoaded("NEWKEY", KEY, mediaData) + verify(mediaController).unregisterCallback(anyObject()) + verify(mediaController).registerCallback(anyObject()) + + // Enqueues callback + verify(executor).execute(anyObject()) + } + + @Test fun testOnPlaybackStateChanged_schedulesTimeout_whenPaused() { // Assuming we're registered testOnMediaDataLoaded_registersPlaybackListener() diff --git a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java index b7a2633d0d36..536cae4380c4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java @@ -82,7 +82,7 @@ public class PipAnimationControllerTest extends SysuiTestCase { @Test public void getAnimator_withBounds_returnBoundsAnimator() { final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, new Rect(), new Rect()); + .getAnimator(mLeash, new Rect(), new Rect(), null); assertEquals("Expect ANIM_TYPE_BOUNDS animation", animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS); @@ -94,12 +94,12 @@ public class PipAnimationControllerTest extends SysuiTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue1); + .getAnimator(mLeash, startValue, endValue1, null); oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); oldAnimator.start(); final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue2); + .getAnimator(mLeash, startValue, endValue2, null); assertEquals("getAnimator with same type returns same animator", oldAnimator, newAnimator); @@ -129,7 +129,7 @@ public class PipAnimationControllerTest extends SysuiTestCase { final Rect endValue1 = new Rect(100, 100, 200, 200); final Rect endValue2 = new Rect(200, 200, 300, 300); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue1); + .getAnimator(mLeash, startValue, endValue1, null); animator.updateEndValue(endValue2); @@ -141,7 +141,7 @@ public class PipAnimationControllerTest extends SysuiTestCase { final Rect startValue = new Rect(0, 0, 100, 100); final Rect endValue = new Rect(100, 100, 200, 200); final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController - .getAnimator(mLeash, startValue, endValue); + .getAnimator(mLeash, startValue, endValue, null); animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new); animator.setPipAnimationCallback(mPipAnimationCallback); diff --git a/packages/Tethering/jarjar-rules.txt b/packages/Tethering/jarjar-rules.txt index 2d3108a0bff1..591861f5b837 100644 --- a/packages/Tethering/jarjar-rules.txt +++ b/packages/Tethering/jarjar-rules.txt @@ -3,7 +3,7 @@ # If there are files in that filegroup that are not covered below, the classes in the # module will be overwritten by the ones in the framework. rule com.android.internal.util.** com.android.networkstack.tethering.util.@1 -rule android.net.LocalLog* com.android.networkstack.tethering.LocalLog@1 +rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1 rule android.net.shared.Inet4AddressUtils* com.android.networkstack.tethering.shared.Inet4AddressUtils@1 diff --git a/packages/Tethering/tests/unit/jarjar-rules.txt b/packages/Tethering/tests/unit/jarjar-rules.txt index 1ea56cdf1a3d..ec2d2b02004e 100644 --- a/packages/Tethering/tests/unit/jarjar-rules.txt +++ b/packages/Tethering/tests/unit/jarjar-rules.txt @@ -8,4 +8,4 @@ rule com.android.internal.util.State* com.android.networkstack.tethering.util.St rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1 rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1 -rule android.net.LocalLog* com.android.networkstack.tethering.LocalLog@1 +rule android.util.LocalLog* com.android.networkstack.tethering.util.LocalLog@1 diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index b5c173c91a53..6eab0221b7ab 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -1597,7 +1597,8 @@ public class AppOpsService extends IAppOpsService.Stub { packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); packageUpdateFilter.addDataScheme("package"); - mContext.registerReceiver(mOnPackageUpdatedReceiver, packageUpdateFilter); + mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL, + packageUpdateFilter, null, null); synchronized (this) { for (int uidNum = mUidStates.size() - 1; uidNum >= 0; uidNum--) { @@ -1640,7 +1641,7 @@ public class AppOpsService extends IAppOpsService.Stub { final IntentFilter packageSuspendFilter = new IntentFilter(); packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED); packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED); - mContext.registerReceiver(new BroadcastReceiver() { + mContext.registerReceiverAsUser(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST); @@ -1664,7 +1665,7 @@ public class AppOpsService extends IAppOpsService.Stub { } } } - }, packageSuspendFilter); + }, UserHandle.ALL, packageSuspendFilter, null, null); final IntentFilter packageAddedFilter = new IntentFilter(); packageAddedFilter.addAction(Intent.ACTION_PACKAGE_ADDED); diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java index 25bbfa02fa05..3a4dfaf9bfcd 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java +++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java @@ -45,6 +45,7 @@ import com.android.internal.R; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -318,16 +319,6 @@ class BluetoothRouteProvider { btRoute.route = builder.build(); } - private void clearActiveRoutes() { - if (DEBUG) { - Log.d(TAG, "Clearing active routes"); - } - for (BluetoothRouteInfo btRoute : mActiveRoutes) { - setRouteConnectionState(btRoute, STATE_DISCONNECTED); - } - mActiveRoutes.clear(); - } - private void addActiveRoute(BluetoothRouteInfo btRoute) { if (DEBUG) { Log.d(TAG, "Adding active route: " + btRoute.route); @@ -348,18 +339,34 @@ class BluetoothRouteProvider { } } - private void findAndSetActiveHearingAidDevices() { + private void clearActiveRoutesWithType(int type) { if (DEBUG) { - Log.d(TAG, "Setting active hearing aid devices"); + Log.d(TAG, "Clearing active routes with type. type=" + type); + } + Iterator<BluetoothRouteInfo> iter = mActiveRoutes.iterator(); + while (iter.hasNext()) { + BluetoothRouteInfo btRoute = iter.next(); + if (btRoute.route.getType() == type) { + iter.remove(); + setRouteConnectionState(btRoute, STATE_DISCONNECTED); + } } + } - BluetoothHearingAid hearingAidProfile = mHearingAidProfile; - if (hearingAidProfile == null) { - return; + private void addActiveHearingAidDevices(BluetoothDevice device) { + if (DEBUG) { + Log.d(TAG, "Setting active hearing aid devices. device=" + device); } - List<BluetoothDevice> activeDevices = hearingAidProfile.getActiveDevices(); + + // Let the given device be the first active device + BluetoothRouteInfo activeBtRoute = mBluetoothRoutes.get(device.getAddress()); + addActiveRoute(activeBtRoute); + + // A bluetooth route with the same route ID should be added. for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { - if (activeDevices.contains(btRoute.btDevice)) { + if (TextUtils.equals(btRoute.route.getId(), activeBtRoute.route.getId()) + && !TextUtils.equals(btRoute.btDevice.getAddress(), + activeBtRoute.btDevice.getAddress())) { addActiveRoute(btRoute); } } @@ -465,16 +472,16 @@ class BluetoothRouteProvider { public void onReceive(Context context, Intent intent, BluetoothDevice device) { switch (intent.getAction()) { case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED: - clearActiveRoutes(); + clearActiveRoutesWithType(MediaRoute2Info.TYPE_BLUETOOTH_A2DP); if (device != null) { addActiveRoute(mBluetoothRoutes.get(device.getAddress())); } notifyBluetoothRoutesUpdated(); break; case BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED: - clearActiveDevices(); + clearActiveRoutesWithType(MediaRoute2Info.TYPE_HEARING_AID); if (device != null) { - findAndSetActiveHearingAidDevices(); + addActiveHearingAidDevices(device); } notifyBluetoothRoutesUpdated(); break; diff --git a/services/core/java/com/android/server/textclassifier/IconsContentProvider.java b/services/core/java/com/android/server/textclassifier/IconsContentProvider.java index 9b3176d9df67..183e920e4620 100644 --- a/services/core/java/com/android/server/textclassifier/IconsContentProvider.java +++ b/services/core/java/com/android/server/textclassifier/IconsContentProvider.java @@ -27,6 +27,7 @@ import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor.AutoCloseOutputStream; import android.os.UserHandle; import android.util.Log; +import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.server.textclassifier.IconsUriHelper.ResourceInfo; @@ -34,6 +35,7 @@ import com.android.server.textclassifier.IconsUriHelper.ResourceInfo; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.util.Arrays; /** * A content provider that is used to access icons returned from the TextClassifier service. @@ -46,32 +48,40 @@ import java.io.OutputStream; public final class IconsContentProvider extends ContentProvider { private static final String TAG = "IconsContentProvider"; + private static final String MIME_TYPE = "image/png"; + + private final PipeDataWriter<Pair<ResourceInfo, Integer>> mWriter = + (writeSide, uri, mimeType, bundle, args) -> { + try (OutputStream out = new AutoCloseOutputStream(writeSide)) { + final ResourceInfo res = args.first; + final int userId = args.second; + final Drawable drawable = Icon.createWithResource(res.packageName, res.id) + .loadDrawableAsUser(getContext(), userId); + getBitmap(drawable).compress(Bitmap.CompressFormat.PNG, 100, out); + } catch (Exception e) { + Log.e(TAG, "Error retrieving icon for uri: " + uri, e); + } + }; @Override public ParcelFileDescriptor openFile(Uri uri, String mode) { + final ResourceInfo res = IconsUriHelper.getInstance().getResourceInfo(uri); + if (res == null) { + Log.e(TAG, "No icon found for uri: " + uri); + return null; + } + try { - final ResourceInfo res = IconsUriHelper.getInstance().getResourceInfo(uri); - final Drawable drawable = Icon.createWithResource(res.packageName, res.id) - .loadDrawableAsUser(getContext(), UserHandle.getCallingUserId()); - final byte[] data = getBitmapData(drawable); - final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); - final ParcelFileDescriptor readSide = pipe[0]; - final ParcelFileDescriptor writeSide = pipe[1]; - try (OutputStream out = new AutoCloseOutputStream(writeSide)) { - out.write(data); - return readSide; - } - } catch (IOException | RuntimeException e) { - Log.e(TAG, "Error retrieving icon for uri: " + uri, e); + final Pair<ResourceInfo, Integer> args = new Pair(res, UserHandle.getCallingUserId()); + return openPipeHelper(uri, MIME_TYPE, /* bundle= */ null, args, mWriter); + } catch (IOException e) { + Log.e(TAG, "Error opening pipe helper for icon at uri: " + uri, e); } + return null; } - /** - * Returns the bitmap data for the specified drawable. - */ - @VisibleForTesting - public static byte[] getBitmapData(Drawable drawable) { + private static Bitmap getBitmap(Drawable drawable) { if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) { throw new IllegalStateException("The icon is zero-sized"); } @@ -85,16 +95,24 @@ public final class IconsContentProvider extends ContentProvider { drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - final byte[] byteArray = stream.toByteArray(); - bitmap.recycle(); - return byteArray; + return bitmap; + } + + /** + * Returns true if the drawables are considered the same. + */ + @VisibleForTesting + public static boolean sameIcon(Drawable one, Drawable two) { + final ByteArrayOutputStream stream1 = new ByteArrayOutputStream(); + getBitmap(one).compress(Bitmap.CompressFormat.PNG, 100, stream1); + final ByteArrayOutputStream stream2 = new ByteArrayOutputStream(); + getBitmap(two).compress(Bitmap.CompressFormat.PNG, 100, stream2); + return Arrays.equals(stream1.toByteArray(), stream2.toByteArray()); } @Override public String getType(Uri uri) { - return "image/png"; + return MIME_TYPE; } @Override diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java index 407b9fcbca74..6bfcf0c75b83 100644 --- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java @@ -1504,12 +1504,21 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { } void removeTask(Task task, boolean killProcess, boolean removeFromRecents, String reason) { - task.removeTaskActivitiesLocked(reason); - cleanUpRemovedTaskLocked(task, killProcess, removeFromRecents); - mService.getLockTaskController().clearLockedTask(task); - mService.getTaskChangeNotificationController().notifyTaskStackChanged(); - if (task.isPersistable) { - mService.notifyTaskPersisterLocked(null, true); + if (task.mInRemoveTask) { + // Prevent recursion. + return; + } + task.mInRemoveTask = true; + try { + task.performClearTask(reason); + cleanUpRemovedTaskLocked(task, killProcess, removeFromRecents); + mService.getLockTaskController().clearLockedTask(task); + mService.getTaskChangeNotificationController().notifyTaskStackChanged(); + if (task.isPersistable) { + mService.notifyTaskPersisterLocked(null, true); + } + } finally { + task.mInRemoveTask = false; } } @@ -2177,7 +2186,7 @@ public class ActivityStackSupervisor implements RecentTasks.Callbacks { // split-screen in split-screen. mService.getTaskChangeNotificationController() .notifyActivityDismissingDockedStack(); - taskDisplayArea.onSplitScreenModeDismissed(task.getStack()); + taskDisplayArea.onSplitScreenModeDismissed((ActivityStack) task); taskDisplayArea.mDisplayContent.ensureActivitiesVisible(null, 0, PRESERVE_WINDOWS, true /* notifyClients */); } diff --git a/services/core/java/com/android/server/wm/InputMonitor.java b/services/core/java/com/android/server/wm/InputMonitor.java index 8734b5efa45d..3e88566449fe 100644 --- a/services/core/java/com/android/server/wm/InputMonitor.java +++ b/services/core/java/com/android/server/wm/InputMonitor.java @@ -19,10 +19,13 @@ package com.android.server.wm; import static android.os.Process.myPid; import static android.os.Process.myUid; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; +import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.INPUT_CONSUMER_NAVIGATION; import static android.view.WindowManager.INPUT_CONSUMER_PIP; import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION; import static android.view.WindowManager.INPUT_CONSUMER_WALLPAPER; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS; @@ -477,12 +480,11 @@ final class InputMonitor { mService.getRecentsAnimationController(); final boolean shouldApplyRecentsInputConsumer = recentsAnimationController != null && recentsAnimationController.shouldApplyInputConsumer(w.mActivityRecord); - if (inputChannel == null || inputWindowHandle == null || w.mRemoved - || (w.cantReceiveTouchInput() && !shouldApplyRecentsInputConsumer)) { + if (inputWindowHandle == null || w.mRemoved) { if (w.mWinAnimator.hasSurface()) { mInputTransaction.setInputWindowInfo( - w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), - mInvalidInputWindow); + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + mInvalidInputWindow); } // Skip this window because it cannot possibly receive input. return; @@ -491,9 +493,23 @@ final class InputMonitor { final int flags = w.mAttrs.flags; final int privateFlags = w.mAttrs.privateFlags; final int type = w.mAttrs.type; - final boolean hasFocus = w.isFocused(); final boolean isVisible = w.isVisibleLw(); + // Assign an InputInfo with type to the overlay window which can't receive input event. + // This is used to omit Surfaces from occlusion detection. + if (inputChannel == null + || (w.cantReceiveTouchInput() && !shouldApplyRecentsInputConsumer)) { + if (!w.mWinAnimator.hasSurface()) { + return; + } + populateOverlayInputInfo(inputWindowHandle, w.getName(), type, isVisible); + mInputTransaction.setInputWindowInfo( + w.mWinAnimator.mSurfaceController.getClientViewRootSurface(), + inputWindowHandle); + return; + } + + final boolean hasFocus = w.isFocused(); if (mAddRecentsAnimationInputConsumerHandle && shouldApplyRecentsInputConsumer) { if (recentsAnimationController.updateInputConsumerForApp( mRecentsAnimationInputConsumer.mWindowHandle, hasFocus)) { @@ -555,6 +571,28 @@ final class InputMonitor { } } + // This would reset InputWindowHandle fields to prevent it could be found by input event. + // We need to check if any new field of InputWindowHandle could impact the result. + private static void populateOverlayInputInfo(final InputWindowHandle inputWindowHandle, + final String name, final int type, final boolean isVisible) { + inputWindowHandle.name = name; + inputWindowHandle.layoutParamsType = type; + inputWindowHandle.dispatchingTimeoutNanos = + WindowManagerService.DEFAULT_INPUT_DISPATCHING_TIMEOUT_NANOS; + inputWindowHandle.visible = isVisible; + inputWindowHandle.canReceiveKeys = false; + inputWindowHandle.hasFocus = false; + inputWindowHandle.ownerPid = myPid(); + inputWindowHandle.ownerUid = myUid(); + inputWindowHandle.inputFeatures = INPUT_FEATURE_NO_INPUT_CHANNEL; + inputWindowHandle.scaleFactor = 1; + inputWindowHandle.layoutParamsFlags = + FLAG_NOT_TOUCH_MODAL | FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE; + inputWindowHandle.portalToDisplayId = INVALID_DISPLAY; + inputWindowHandle.touchableRegion.setEmpty(); + inputWindowHandle.setTouchableRegionCrop(null); + } + /** * Helper function to generate an InputInfo with type SECURE_SYSTEM_OVERLAY. This input * info will not have an input channel or be touchable, but is used to omit Surfaces @@ -564,16 +602,7 @@ final class InputMonitor { static void setTrustedOverlayInputInfo(SurfaceControl sc, SurfaceControl.Transaction t, int displayId, String name) { InputWindowHandle inputWindowHandle = new InputWindowHandle(null, displayId); - inputWindowHandle.name = name; - inputWindowHandle.layoutParamsType = TYPE_SECURE_SYSTEM_OVERLAY; - inputWindowHandle.dispatchingTimeoutNanos = -1; - inputWindowHandle.visible = true; - inputWindowHandle.canReceiveKeys = false; - inputWindowHandle.hasFocus = false; - inputWindowHandle.ownerPid = myPid(); - inputWindowHandle.ownerUid = myUid(); - inputWindowHandle.inputFeatures = INPUT_FEATURE_NO_INPUT_CHANNEL; - inputWindowHandle.scaleFactor = 1; + populateOverlayInputInfo(inputWindowHandle, name, TYPE_SECURE_SYSTEM_OVERLAY, true); t.setInputWindowInfo(sc, inputWindowHandle); } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 748244e1a5c2..c664a841fc1f 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -435,6 +435,13 @@ class Task extends WindowContainer<WindowContainer> { static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1; private int mForceHiddenFlags = 0; + // TODO(b/160201781): Revisit double invocation issue in Task#removeChild. + /** + * Skip {@link ActivityStackSupervisor#removeTask(Task, boolean, boolean, String)} execution if + * {@code true} to prevent double traversal of {@link #mChildren} in a loop. + */ + boolean mInRemoveTask; + // When non-null, this is a transaction that will get applied on the next frame returned after // a relayout is requested from the client. While this is only valid on a leaf task; since the // transaction can effect an ancestor task, this also needs to keep track of the ancestor task @@ -1496,11 +1503,8 @@ class Task extends WindowContainer<WindowContainer> { return autoRemoveRecents || (!hasChild() && !getHasBeenVisible()); } - /** - * Completely remove all activities associated with an existing - * task starting at a specified index. - */ - private void performClearTaskAtIndexLocked(String reason) { + /** Completely remove all activities associated with an existing task. */ + void performClearTask(String reason) { // Broken down into to cases to avoid object create due to capturing mStack. if (getStack() == null) { forAllActivities((r) -> { @@ -1524,7 +1528,7 @@ class Task extends WindowContainer<WindowContainer> { */ void performClearTaskLocked() { mReuseTask = true; - performClearTaskAtIndexLocked("clear-task-all"); + performClearTask("clear-task-all"); mReuseTask = false; } @@ -1585,11 +1589,6 @@ class Task extends WindowContainer<WindowContainer> { return false; } - void removeTaskActivitiesLocked(String reason) { - // Just remove the entire task. - performClearTaskAtIndexLocked(reason); - } - String lockTaskAuthToString() { switch (mLockTaskAuth) { case LOCK_TASK_AUTH_DONT_LOCK: return "LOCK_TASK_AUTH_DONT_LOCK"; diff --git a/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java b/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java index 72580a3b98c2..a787c321fc66 100644 --- a/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/textclassifier/IconsContentProviderTest.java @@ -50,8 +50,7 @@ public final class IconsContentProviderTest { final Drawable actual = Icon.createWithContentUri(uri).loadDrawable(context); assertThat(actual).isNotNull(); - assertThat(IconsContentProvider.getBitmapData(actual)) - .isEqualTo(IconsContentProvider.getBitmapData(expected)); + assertThat(IconsContentProvider.sameIcon(actual, expected)).isTrue(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index 1415c506a1c9..9d88ada5a90c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -28,6 +28,8 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.clearInvocations; import android.graphics.Point; @@ -158,4 +160,30 @@ public class TaskTests extends WindowTestsBase { assertEquals(activity1, task1.isInTask(activity1)); assertNull(task1.isInTask(activity2)); } + + @Test + public void testRemoveChildForOverlayTask() { + final Task task = createTaskStackOnDisplay(mDisplayContent); + final int taskId = task.mTaskId; + final ActivityRecord activity1 = + WindowTestUtils.createActivityRecordInTask(mDisplayContent, task); + final ActivityRecord activity2 = + WindowTestUtils.createActivityRecordInTask(mDisplayContent, task); + final ActivityRecord activity3 = + WindowTestUtils.createActivityRecordInTask(mDisplayContent, task); + activity1.setTaskOverlay(true); + activity2.setTaskOverlay(true); + activity3.setTaskOverlay(true); + + assertEquals(3, task.getChildCount()); + assertTrue(task.onlyHasTaskOverlayActivities(true)); + + task.removeChild(activity1); + + verify(task.mStackSupervisor).removeTask(any(), anyBoolean(), anyBoolean(), anyString()); + assertEquals(2, task.getChildCount()); + task.forAllActivities((r) -> { + assertTrue(r.finishing); + }); + } } diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java index 42b0c608822e..314e95229d29 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java @@ -61,11 +61,12 @@ public class NetworkStagedRollbackTest { private static final TestApp NETWORK_STACK = new TestApp("NetworkStack", getNetworkStackPackageName(), -1, false, findNetworkStackApk()); - private static File findNetworkStackApk() { + private static File[] findNetworkStackApk() { for (String name : NETWORK_STACK_APK_NAMES) { final File apk = new File("/system/priv-app/" + name + "/" + name + ".apk"); if (apk.isFile()) { - return apk; + final File dir = new File("/system/priv-app/" + name); + return dir.listFiles((d, f) -> f.startsWith(name)); } } throw new RuntimeException("Can't find NetworkStackApk"); |