Stop timer when notification is not visible

This combines ag/8888937 and ag/9019277 into one CL, plus fixes another issue
where the timer could restart if the user had removed a notification and then
restarted the stream

Fixes: 138261464
Test: manual, atest NotificationMediaTemplateViewWrapperTest

Also checked the changes on the master branch since that had very
obvious issues when locking / unlocking the phone before

Change-Id: I6a0bbc675d33a5c7d4ce7f0884aec19606bff4fa
diff --git a/core/java/com/android/internal/widget/MediaNotificationView.java b/core/java/com/android/internal/widget/MediaNotificationView.java
index e7d240a..9bb4501 100644
--- a/core/java/com/android/internal/widget/MediaNotificationView.java
+++ b/core/java/com/android/internal/widget/MediaNotificationView.java
@@ -26,6 +26,8 @@
 import android.widget.ImageView;
 import android.widget.RemoteViews;
 
+import java.util.ArrayList;
+
 /**
  * A TextView that can float around an image on the end.
  *
@@ -42,6 +44,7 @@
     private View mMainColumn;
     private View mMediaContent;
     private int mImagePushIn;
+    private ArrayList<VisibilityChangeListener> mListeners;
 
     public MediaNotificationView(Context context) {
         this(context, null);
@@ -168,4 +171,50 @@
         mMainColumn = findViewById(com.android.internal.R.id.notification_main_column);
         mMediaContent = findViewById(com.android.internal.R.id.notification_media_content);
     }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        super.onVisibilityAggregated(isVisible);
+        if (mListeners != null) {
+            for (int i = 0; i < mListeners.size(); i++) {
+                mListeners.get(i).onAggregatedVisibilityChanged(isVisible);
+            }
+        }
+    }
+
+    /**
+     * Add a listener to receive updates on the visibility of this view
+     *
+     * @param listener The listener to add.
+     */
+    public void addVisibilityListener(VisibilityChangeListener listener) {
+        if (mListeners == null) {
+            mListeners = new ArrayList<>();
+        }
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
+    }
+
+    /**
+     * Remove the specified listener
+     *
+     * @param listener The listener to remove.
+     */
+    public void removeVisibilityListener(VisibilityChangeListener listener) {
+        if (mListeners != null) {
+            mListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Interface for receiving updates when the view's visibility changes
+     */
+    public interface VisibilityChangeListener {
+        /**
+         * Method called when the visibility of this view has changed
+         * @param isVisible true if the view is now visible
+         */
+        void onAggregatedVisibilityChanged(boolean isVisible);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index a8327f6..ea31be4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -1367,7 +1367,9 @@
         if (isChildInGroup()) {
             mTranslationWhenRemoved += getNotificationParent().getTranslationY();
         }
-        mPrivateLayout.setRemoved();
+        for (NotificationContentView l : mLayouts) {
+            l.setRemoved();
+        }
     }
 
     public boolean wasChildInGroupWhenRemoved() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 90f6324..0c5b27b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1542,6 +1542,15 @@
         if (mHeadsUpRemoteInput != null) {
             mHeadsUpRemoteInput.setRemoved();
         }
+        if (mExpandedWrapper != null) {
+            mExpandedWrapper.setRemoved();
+        }
+        if (mContractedWrapper != null) {
+            mContractedWrapper.setRemoved();
+        }
+        if (mHeadsUpWrapper != null) {
+            mHeadsUpWrapper.setRemoved();
+        }
     }
 
     public void setContentHeightAnimating(boolean animating) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
index 20e8b73..1116106 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
@@ -39,6 +39,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.widget.MediaNotificationView;
 import com.android.systemui.Dependency;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.TransformableView;
@@ -67,6 +68,7 @@
     private View mSeekBarView;
     private Context mContext;
     private MetricsLogger mMetricsLogger;
+    private boolean mIsViewVisible;
 
     @VisibleForTesting
     protected SeekBar.OnSeekBarChangeListener mSeekListener =
@@ -88,11 +90,46 @@
         }
     };
 
+    private MediaNotificationView.VisibilityChangeListener mVisibilityListener =
+            new MediaNotificationView.VisibilityChangeListener() {
+        @Override
+        public void onAggregatedVisibilityChanged(boolean isVisible) {
+            mIsViewVisible = isVisible;
+            if (isVisible && mMediaController != null) {
+                // Restart timer if we're currently playing and didn't already have one going
+                PlaybackState state = mMediaController.getPlaybackState();
+                if (state != null && state.getState() == PlaybackState.STATE_PLAYING
+                        && mSeekBarTimer == null && mSeekBarView != null
+                        && mSeekBarView.getVisibility() != View.GONE) {
+                    startTimer();
+                }
+            } else {
+                clearTimer();
+            }
+        }
+    };
+
+    private View.OnAttachStateChangeListener mAttachStateListener =
+            new View.OnAttachStateChangeListener() {
+        @Override
+        public void onViewAttachedToWindow(View v) {
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View v) {
+            mIsViewVisible = false;
+        }
+    };
+
     private MediaController.Callback mMediaCallback = new MediaController.Callback() {
         @Override
         public void onSessionDestroyed() {
             clearTimer();
             mMediaController.unregisterCallback(this);
+            if (mView instanceof MediaNotificationView) {
+                ((MediaNotificationView) mView).removeVisibilityListener(mVisibilityListener);
+                mView.removeOnAttachStateChangeListener(mAttachStateListener);
+            }
         }
 
         @Override
@@ -126,10 +163,17 @@
         mContext = ctx;
         mMediaManager = Dependency.get(NotificationMediaManager.class);
         mMetricsLogger = Dependency.get(MetricsLogger.class);
+
+        if (mView instanceof MediaNotificationView) {
+            MediaNotificationView mediaView = (MediaNotificationView) mView;
+            mediaView.addVisibilityListener(mVisibilityListener);
+            mView.addOnAttachStateChangeListener(mAttachStateListener);
+        }
     }
 
     private void resolveViews() {
         mActions = mView.findViewById(com.android.internal.R.id.media_actions);
+        mIsViewVisible = mView.isShown();
 
         final MediaSession.Token token = mRow.getEntry().notification.getNotification().extras
                 .getParcelable(Notification.EXTRA_MEDIA_SESSION);
@@ -208,24 +252,37 @@
 
     private void startTimer() {
         clearTimer();
-        mSeekBarTimer = new Timer(true /* isDaemon */);
-        mSeekBarTimer.schedule(new TimerTask() {
-            @Override
-            public void run() {
-                mHandler.post(mOnUpdateTimerTick);
-            }
-        }, 0, PROGRESS_UPDATE_INTERVAL);
+        if (mIsViewVisible) {
+            mSeekBarTimer = new Timer(true /* isDaemon */);
+            mSeekBarTimer.schedule(new TimerTask() {
+                @Override
+                public void run() {
+                    mHandler.post(mOnUpdateTimerTick);
+                }
+            }, 0, PROGRESS_UPDATE_INTERVAL);
+        }
     }
 
     private void clearTimer() {
         if (mSeekBarTimer != null) {
-            // TODO: also trigger this when the notification panel is collapsed
             mSeekBarTimer.cancel();
             mSeekBarTimer.purge();
             mSeekBarTimer = null;
         }
     }
 
+    @Override
+    public void setRemoved() {
+        clearTimer();
+        if (mMediaController != null) {
+            mMediaController.unregisterCallback(mMediaCallback);
+        }
+        if (mView instanceof MediaNotificationView) {
+            ((MediaNotificationView) mView).removeVisibilityListener(mVisibilityListener);
+            mView.removeOnAttachStateChangeListener(mAttachStateListener);
+        }
+    }
+
     private boolean canSeekMedia(@Nullable PlaybackState state) {
         if (state == null) {
             return false;
@@ -261,7 +318,6 @@
         public void run() {
             if (mMediaController != null && mSeekBar != null) {
                 PlaybackState playbackState = mMediaController.getPlaybackState();
-
                 if (playbackState != null) {
                     updatePlaybackUi(playbackState);
                 } else {
@@ -274,6 +330,10 @@
     };
 
     private void updatePlaybackUi(PlaybackState state) {
+        if (mSeekBar == null || mSeekBarElapsedTime == null) {
+            return;
+        }
+
         long position = state.getPosition();
         mSeekBar.setProgress((int) position);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index 47906a7..3950003 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -261,6 +261,12 @@
         mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
     }
 
+    /**
+     * Called to indicate this view is removed
+     */
+    public void setRemoved() {
+    }
+
     public int getCustomBackgroundColor() {
         // Parent notifications should always use the normal background color
         return mRow.isSummaryWithChildren() ? 0 : mBackgroundColor;