Move some of the media error handling code to car-media-common

Fixes: 165084717
Test: manual
Change-Id: I5caf97530ccfec43d421cc139d4cd020df180aeb
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 33d7f0d..258963f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -35,30 +35,7 @@
     <!-- Title to display when in playback view. [CHAR LIMIT=50] -->
     <string name="fragment_playback_title">Now Playing</string>
 
-    <!-- MediaActivity default error message [CHAR LIMIT=100] -->
-    <string name="default_error_message">Something’s wrong. Try later.</string>
-    <!-- Error message set when the application state is invalid to fulfill the request. [CHAR LIMIT=100] -->
-    <string name="error_code_app_error">Can’t do that right now</string>
-    <!-- Error message set when the request is not supported by the application. [CHAR LIMIT=100] -->
-    <string name="error_code_not_supported">This app can’t do that</string>
-    <!-- Error message set when the request cannot be performed because authentication has expired. [CHAR LIMIT=100] -->
-    <string name="error_code_authentication_expired">Sign in to use this app</string>
-    <!-- Error message set when a premium account is required for the request to succeed. [CHAR LIMIT=100] -->
-    <string name="error_code_premium_account_required">Premium access required</string>
-    <!-- Error message set when too many concurrent streams are detected. [CHAR LIMIT=100] -->
-    <string name="error_code_concurrent_stream_limit">Listening on too many devices</string>
-    <!-- Error message set when the content is blocked due to parental controls. [CHAR LIMIT=100] -->
-    <string name="error_code_parental_control_restricted">That content is blocked</string>
-    <!-- Error message set when the content is blocked due to being regionally unavailable. [CHAR LIMIT=100] -->
-    <string name="error_code_not_available_in_region">Can’t get that content here</string>
-    <!-- Error message set when the requested content is already playing. [CHAR LIMIT=100] -->
-    <string name="error_code_content_already_playing">Already playing that content</string>
-    <!-- Error message set when the application cannot skip any more songs because skip limit is reached. [CHAR LIMIT=100] -->
-    <string name="error_code_skip_limit_reached">Can’t skip any more tracks</string>
-    <!-- Error message set when the action is interrupted due to some external event. [CHAR LIMIT=100] -->
-    <string name="error_code_action_aborted">Couldn’t finish. Try again.</string>
-    <!-- Error message set when the playback navigation (previous, next) is not possible because the queue was exhausted. [CHAR LIMIT=100] -->
-    <string name="error_code_end_of_queue">Nothing else is queued up</string>
+
     <!-- Title string for the service used to bind to the current media source -->
     <string name="service_notification_title">Connecting to media</string>
     <!-- Title of the sound settings menu item. Will be displayed if the button is in the overflow menu. [CHAR_LIMIT=50] -->
diff --git a/src/com/android/car/media/BrowseViewController.java b/src/com/android/car/media/BrowseViewController.java
index fe2d8e8..106c151 100644
--- a/src/com/android/car/media/BrowseViewController.java
+++ b/src/com/android/car/media/BrowseViewController.java
@@ -43,6 +43,7 @@
 import com.android.car.media.browse.BrowseAdapter;
 import com.android.car.media.common.GridSpacingItemDecoration;
 import com.android.car.media.common.MediaItemMetadata;
+import com.android.car.media.common.browse.BrowsedMediaItems;
 import com.android.car.media.common.browse.MediaBrowserViewModel;
 import com.android.car.media.common.source.MediaSource;
 import com.android.car.media.widgets.AppBarController;
@@ -563,18 +564,7 @@
         }
     }
 
-    /**
-     * Filters the items that are valid for the root (tabs) or the current node. Returns null when
-     * the given list is null to preserve its error signal.
-     */
-    @Nullable
-    private List<MediaItemMetadata> filterItems(boolean forRoot,
-            @Nullable List<MediaItemMetadata> items) {
-        if (items == null) return null;
-        Predicate<MediaItemMetadata> predicate = forRoot ? MediaItemMetadata::isBrowsable
-                : item -> (item.isPlayable() || item.isBrowsable());
-        return items.stream().filter(predicate).collect(Collectors.toList());
-    }
+
 
     private void onItemsUpdate(boolean forRoot, FutureData<List<MediaItemMetadata>> futureData) {
 
@@ -608,7 +598,8 @@
 
         stopLoadingIndicator();
 
-        List<MediaItemMetadata> items = filterItems(forRoot, futureData.getData());
+        List<MediaItemMetadata> items =
+                BrowsedMediaItems.filterItems(forRoot, futureData.getData());
         if (forRoot) {
             boolean browseTreeHasChildren = items != null && !items.isEmpty();
             if (Log.isLoggable(TAG, Log.INFO)) {
diff --git a/src/com/android/car/media/ErrorScreenController.java b/src/com/android/car/media/ErrorScreenController.java
new file mode 100644
index 0000000..bd62f81
--- /dev/null
+++ b/src/com/android/car/media/ErrorScreenController.java
@@ -0,0 +1,41 @@
+package com.android.car.media;
+
+import android.app.PendingIntent;
+import android.car.content.pm.CarPackageManager;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.media.common.PlaybackErrorViewController;
+import com.android.car.media.common.source.MediaSource;
+
+/**
+ * A view controller that displays the playback state error iif there is no browse tree.
+ */
+public class ErrorScreenController extends ViewControllerBase {
+
+    private final PlaybackErrorViewController mPlaybackErrorViewController;
+
+    ErrorScreenController(FragmentActivity activity,
+            CarPackageManager carPackageManager, ViewGroup container) {
+        super(activity, carPackageManager, container, R.layout.fragment_error);
+
+        mPlaybackErrorViewController = new PlaybackErrorViewController(mContent);
+    }
+
+    @Override
+    void onMediaSourceChanged(@Nullable MediaSource mediaSource) {
+        super.onMediaSourceChanged(mediaSource);
+
+        mAppBarController.setListener(new BasicAppBarListener());
+        mAppBarController.setTitle(getAppBarDefaultTitle(mediaSource));
+
+        mPlaybackErrorViewController.hideErrorNoAnim();
+    }
+
+    public void setError(String message, String label, PendingIntent pendingIntent,
+            boolean distractionOptimized) {
+        mPlaybackErrorViewController.setError(message, label, pendingIntent, distractionOptimized);
+    }
+}
diff --git a/src/com/android/car/media/ErrorViewController.java b/src/com/android/car/media/ErrorViewController.java
deleted file mode 100644
index f67e22c..0000000
--- a/src/com/android/car/media/ErrorViewController.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package com.android.car.media;
-
-import android.app.PendingIntent;
-import android.car.content.pm.CarPackageManager;
-import android.car.drivingstate.CarUxRestrictions;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.Nullable;
-import androidx.fragment.app.FragmentActivity;
-
-import com.android.car.apps.common.UxrButton;
-import com.android.car.apps.common.UxrTextView;
-import com.android.car.apps.common.util.ViewUtils;
-import com.android.car.media.common.source.MediaSource;
-
-/**
- * A view controller that displays the playback state error iif there is no browse tree.
- */
-public class ErrorViewController extends ViewControllerBase {
-    private final String TAG = "ErrorViewController";
-
-    // mErrorMessageView is defined explicitly as a UxrTextView instead of a TextView to
-    // provide clarity as it may be misleading to assume that mErrorMessageView extends all TextView
-    // methods. In addition, it increases discoverability of runtime issues that may occur.
-    private final UxrTextView mErrorMessageView;
-    private final UxrButton mErrorButton;
-
-
-    ErrorViewController(FragmentActivity activity,
-            CarPackageManager carPackageManager, ViewGroup container) {
-        super(activity, carPackageManager, container, R.layout.fragment_error);
-
-        mErrorMessageView = mContent.findViewById(R.id.error_message);
-        mErrorButton = mContent.findViewById(R.id.error_button);
-    }
-
-    @Override
-    void onMediaSourceChanged(@Nullable MediaSource mediaSource) {
-        super.onMediaSourceChanged(mediaSource);
-
-        mAppBarController.setListener(new BasicAppBarListener());
-        mAppBarController.setTitle(getAppBarDefaultTitle(mediaSource));
-
-        ViewUtils.hideViewAnimated(mErrorMessageView, 0);
-        ViewUtils.hideViewAnimated(mErrorButton, 0);
-    }
-
-    public void setError(String message, String label, PendingIntent pendingIntent,
-            boolean isDistractionOptimized) {
-        mErrorMessageView.setText(message);
-
-        // Only show the error button if the error is actionable.
-        if (label != null && pendingIntent != null) {
-            mErrorButton.setText(label);
-
-            mErrorButton.setUxRestrictions(isDistractionOptimized
-                    ? CarUxRestrictions.UX_RESTRICTIONS_BASELINE
-                    : CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP);
-
-            mErrorButton.setOnClickListener(v -> {
-                try {
-                    pendingIntent.send();
-                } catch (PendingIntent.CanceledException e) {
-                    if (Log.isLoggable(TAG, Log.ERROR)) {
-                        Log.e(TAG, "Pending intent canceled");
-                    }
-                }
-            });
-            mErrorButton.setVisibility(View.VISIBLE);
-        } else {
-            mErrorButton.setVisibility(View.GONE);
-        }
-
-        ViewUtils.showViewAnimated(mErrorMessageView, mFadeDuration);
-        ViewUtils.showViewAnimated(mErrorButton, mFadeDuration);
-    }
-}
diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java
index a0a4f2c..0c02e56 100644
--- a/src/com/android/car/media/MediaActivity.java
+++ b/src/com/android/car/media/MediaActivity.java
@@ -31,7 +31,6 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.os.Bundle;
-import android.support.v4.media.session.PlaybackStateCompat;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Size;
@@ -54,7 +53,7 @@
 import com.android.car.apps.common.util.VectorMath;
 import com.android.car.apps.common.util.CarPackageManagerUtils;
 import com.android.car.apps.common.util.ViewUtils;
-import com.android.car.media.common.MediaConstants;
+import com.android.car.media.common.PlaybackErrorsHelper;
 import com.android.car.media.common.MediaItemMetadata;
 import com.android.car.media.common.MinimizedPlaybackControlBar;
 import com.android.car.media.common.playback.PlaybackViewModel;
@@ -63,7 +62,6 @@
 import com.android.car.ui.AlertDialogBuilder;
 import com.android.car.ui.utils.CarUxRestrictionsUtil;
 
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Stack;
@@ -90,7 +88,7 @@
     private ViewGroup mBrowseContainer;
     private ViewGroup mPlaybackContainer;
     private ViewGroup mErrorContainer;
-    private ErrorViewController mErrorController;
+    private ErrorScreenController mErrorController;
     private ViewGroup mSearchContainer;
 
     private Toast mToast;
@@ -133,30 +131,6 @@
         FATAL_ERROR
     }
 
-    private static final Map<Integer, Integer> ERROR_CODE_MESSAGES_MAP;
-
-    static {
-        Map<Integer, Integer> map = new HashMap<>();
-        map.put(PlaybackStateCompat.ERROR_CODE_APP_ERROR, R.string.error_code_app_error);
-        map.put(PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED, R.string.error_code_not_supported);
-        map.put(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
-                R.string.error_code_authentication_expired);
-        map.put(PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
-                R.string.error_code_premium_account_required);
-        map.put(PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT,
-                R.string.error_code_concurrent_stream_limit);
-        map.put(PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
-                R.string.error_code_parental_control_restricted);
-        map.put(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION,
-                R.string.error_code_not_available_in_region);
-        map.put(PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING,
-                R.string.error_code_content_already_playing);
-        map.put(PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED,
-                R.string.error_code_skip_limit_reached);
-        map.put(PlaybackStateCompat.ERROR_CODE_ACTION_ABORTED, R.string.error_code_action_aborted);
-        map.put(PlaybackStateCompat.ERROR_CODE_END_OF_QUEUE, R.string.error_code_end_of_queue);
-        ERROR_CODE_MESSAGES_MAP = Collections.unmodifiableMap(map);
-    }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -247,100 +221,62 @@
 
     private void handlePlaybackState(PlaybackViewModel.PlaybackStateWrapper state,
             boolean ignoreSameState) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG,
-                    "handlePlaybackState(); state change: " + (mCurrentPlaybackStateWrapper != null
-                            ? mCurrentPlaybackStateWrapper.getState() : null) + " -> " + (
-                            state != null ? state.getState() : null));
-
-        }
-
-        // TODO(arnaudberry) rethink interactions between customized layouts and dynamic visibility.
-        mCanShowMiniPlaybackControls = (state != null) && state.shouldDisplay();
-        updateMiniPlaybackControls(true);
-
-        if (state == null) {
-            mCurrentPlaybackStateWrapper = null;
-            return;
-        }
-
-        String displayedMessage = getDisplayedMessage(state);
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            Log.d(TAG, "Displayed error message: [" + displayedMessage + "]");
-        }
-        if (ignoreSameState && mCurrentPlaybackStateWrapper != null
-                && mCurrentPlaybackStateWrapper.getState() == state.getState()
-                && TextUtils.equals(displayedMessage,
-                getDisplayedMessage(mCurrentPlaybackStateWrapper))) {
-            if (Log.isLoggable(TAG, Log.DEBUG)) {
-                Log.d(TAG, "Ignore same playback state.");
-            }
-            return;
-        }
-
-        mCurrentPlaybackStateWrapper = state;
-
-        maybeCancelToast();
-        maybeCancelDialog();
-
-        Bundle extras = state.getExtras();
-        PendingIntent intent = extras == null ? null : extras.getParcelable(
-                MediaConstants.ERROR_RESOLUTION_ACTION_INTENT);
-        String label = extras == null ? null : extras.getString(
-                MediaConstants.ERROR_RESOLUTION_ACTION_LABEL);
-
-        boolean isFatalError = false;
-        if (!TextUtils.isEmpty(displayedMessage)) {
-            if (mBrowseController.browseTreeHasChildren()) {
-                if (intent != null && !isUxRestricted()) {
-                    showDialog(intent, displayedMessage, label, getString(android.R.string.cancel));
-                } else {
-                    showToast(displayedMessage);
-                }
-            } else {
-                boolean isDistractionOptimized = intent == null
-                        ? false
-                        : CarPackageManagerUtils.isDistractionOptimized(mCarPackageManager, intent);
-                getErrorController().setError(displayedMessage, label, intent,
-                        isDistractionOptimized);
-                isFatalError = true;
-            }
-        }
-        if (isFatalError) {
-            changeMode(Mode.FATAL_ERROR);
-        } else if (mMode == Mode.FATAL_ERROR) {
-            changeMode(Mode.BROWSING);
-        }
+        mErrorsHelper.handlePlaybackState(TAG, state, ignoreSameState);
     }
 
-    private ErrorViewController getErrorController() {
+    private final PlaybackErrorsHelper mErrorsHelper = new PlaybackErrorsHelper(this) {
+
+        @Override
+        public void handlePlaybackState(@NonNull String tag,
+                PlaybackViewModel.PlaybackStateWrapper state, boolean ignoreSameState) {
+
+            // TODO rethink interactions between customized layouts and dynamic visibility.
+            mCanShowMiniPlaybackControls = (state != null) && state.shouldDisplay();
+            updateMiniPlaybackControls(true);
+            super.handlePlaybackState(tag, state, ignoreSameState);
+        }
+
+        @Override
+        public void handleNewPlaybackState(String displayedMessage, PendingIntent intent,
+                String label) {
+            maybeCancelToast();
+            maybeCancelDialog();
+
+            boolean isFatalError = false;
+            if (!TextUtils.isEmpty(displayedMessage)) {
+                if (mBrowseController.browseTreeHasChildren()) {
+                    if (intent != null && !isUxRestricted()) {
+                        showDialog(intent, displayedMessage, label,
+                                getString(android.R.string.cancel));
+                    } else {
+                        showToast(displayedMessage);
+                    }
+                } else {
+                    boolean isDistractionOptimized =
+                            intent != null && CarPackageManagerUtils.isDistractionOptimized(
+                                    mCarPackageManager, intent);
+                    getErrorController().setError(displayedMessage, label, intent,
+                            isDistractionOptimized);
+                    isFatalError = true;
+                }
+            }
+            if (isFatalError) {
+                changeMode(MediaActivity.Mode.FATAL_ERROR);
+            } else if (mMode == MediaActivity.Mode.FATAL_ERROR) {
+                changeMode(MediaActivity.Mode.BROWSING);
+            }
+        }
+    };
+
+    private ErrorScreenController getErrorController() {
         if (mErrorController == null) {
-            mErrorController = new ErrorViewController(this, mCarPackageManager, mErrorContainer);
+            mErrorController = new ErrorScreenController(this, mCarPackageManager, mErrorContainer);
             MediaSource mediaSource = getMediaSourceViewModel().getPrimaryMediaSource().getValue();
             mErrorController.onMediaSourceChanged(mediaSource);
         }
         return mErrorController;
     }
 
-    private String getDisplayedMessage(@Nullable PlaybackViewModel.PlaybackStateWrapper state) {
-        if (state == null) {
-            return null;
-        }
-        if (!TextUtils.isEmpty(state.getErrorMessage())) {
-            return state.getErrorMessage().toString();
-        }
-        // ERROR_CODE_UNKNOWN_ERROR means there is no error in PlaybackState.
-        if (state.getErrorCode() != PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR) {
-            Integer messageId = ERROR_CODE_MESSAGES_MAP.get(state.getErrorCode());
-            return messageId != null ? getString(messageId) : getString(
-                    R.string.default_error_message);
-        }
-        if (state.getState() == PlaybackStateCompat.STATE_ERROR) {
-            return getString(R.string.default_error_message);
-        }
-        return null;
-    }
-
     private void showDialog(PendingIntent intent, String message, String positiveBtnText,
             String negativeButtonText) {
         AlertDialogBuilder dialog = new AlertDialogBuilder(this);
diff --git a/src/com/android/car/media/ViewControllerBase.java b/src/com/android/car/media/ViewControllerBase.java
index 156c6b2..14bbda0 100644
--- a/src/com/android/car/media/ViewControllerBase.java
+++ b/src/com/android/car/media/ViewControllerBase.java
@@ -44,7 +44,7 @@
 
 /**
  * Functionality common to content view controllers. It mainly handles the AppBar view,
- * which is common to all them.
+ * which is common to all of them.
  */
 abstract class ViewControllerBase {
     private static final String TAG = "ViewControllerBase";