1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.media.common;
18 
19 import android.app.PendingIntent;
20 import android.content.Context;
21 import android.os.Bundle;
22 import android.support.v4.media.session.PlaybackStateCompat;
23 import android.text.TextUtils;
24 import android.util.Log;
25 
26 import androidx.annotation.NonNull;
27 import androidx.annotation.Nullable;
28 
29 import com.android.car.media.common.playback.PlaybackViewModel.PlaybackStateWrapper;
30 
31 import java.util.Collections;
32 import java.util.HashMap;
33 import java.util.Map;
34 
35 /**
36  * Abstract class to factorize most of the error handling logic.
37  */
38 public abstract class PlaybackErrorsHelper {
39 
40     private static final Map<Integer, Integer> ERROR_CODE_MESSAGES_MAP;
41 
42     static {
43         Map<Integer, Integer> map = new HashMap<>();
map.put(PlaybackStateCompat.ERROR_CODE_APP_ERROR, R.string.error_code_app_error)44         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)45         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)46         map.put(PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED,
47                 R.string.error_code_authentication_expired);
map.put(PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, R.string.error_code_premium_account_required)48         map.put(PlaybackStateCompat.ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED,
49                 R.string.error_code_premium_account_required);
map.put(PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT, R.string.error_code_concurrent_stream_limit)50         map.put(PlaybackStateCompat.ERROR_CODE_CONCURRENT_STREAM_LIMIT,
51                 R.string.error_code_concurrent_stream_limit);
map.put(PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, R.string.error_code_parental_control_restricted)52         map.put(PlaybackStateCompat.ERROR_CODE_PARENTAL_CONTROL_RESTRICTED,
53                 R.string.error_code_parental_control_restricted);
map.put(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION, R.string.error_code_not_available_in_region)54         map.put(PlaybackStateCompat.ERROR_CODE_NOT_AVAILABLE_IN_REGION,
55                 R.string.error_code_not_available_in_region);
map.put(PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING, R.string.error_code_content_already_playing)56         map.put(PlaybackStateCompat.ERROR_CODE_CONTENT_ALREADY_PLAYING,
57                 R.string.error_code_content_already_playing);
map.put(PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED, R.string.error_code_skip_limit_reached)58         map.put(PlaybackStateCompat.ERROR_CODE_SKIP_LIMIT_REACHED,
59                 R.string.error_code_skip_limit_reached);
map.put(PlaybackStateCompat.ERROR_CODE_ACTION_ABORTED, R.string.error_code_action_aborted)60         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)61         map.put(PlaybackStateCompat.ERROR_CODE_END_OF_QUEUE, R.string.error_code_end_of_queue);
62         ERROR_CODE_MESSAGES_MAP = Collections.unmodifiableMap(map);
63     }
64 
65     private final Context mContext;
66     private PlaybackStateWrapper mCurrentPlaybackStateWrapper;
67 
PlaybackErrorsHelper(Context context)68     public PlaybackErrorsHelper(Context context) {
69         mContext = context;
70     }
71 
handleNewPlaybackState(String displayedMessage, PendingIntent intent, String label)72     protected abstract void handleNewPlaybackState(String displayedMessage, PendingIntent intent,
73             String label);
74 
75     /**
76      * Triggers updates of the error state.
77      * Must be called when the children list of the root of the browse tree changes AND when
78      * the playback state changes.
79      */
handlePlaybackState(@onNull String tag, PlaybackStateWrapper state, boolean ignoreSameState)80     public void handlePlaybackState(@NonNull String tag, PlaybackStateWrapper state,
81             boolean ignoreSameState) {
82         if (Log.isLoggable(tag, Log.DEBUG)) {
83             Log.d(tag,
84                     "handlePlaybackState(); state change: " + (mCurrentPlaybackStateWrapper != null
85                             ? mCurrentPlaybackStateWrapper.getState() : null) + " -> " + (
86                             state != null ? state.getState() : null));
87         }
88 
89         if (state == null) {
90             mCurrentPlaybackStateWrapper = null;
91             return;
92         }
93 
94         String displayedMessage = getDisplayedMessage(mContext, state);
95         if (Log.isLoggable(tag, Log.DEBUG)) {
96             Log.d(tag, "Displayed error message: [" + displayedMessage + "]");
97         }
98         if (ignoreSameState && mCurrentPlaybackStateWrapper != null
99                 && mCurrentPlaybackStateWrapper.getState() == state.getState()
100                 && TextUtils.equals(displayedMessage,
101                 getDisplayedMessage(mContext, mCurrentPlaybackStateWrapper))) {
102             if (Log.isLoggable(tag, Log.DEBUG)) {
103                 Log.d(tag, "Ignore same playback state.");
104             }
105             return;
106         }
107 
108         mCurrentPlaybackStateWrapper = state;
109 
110         PendingIntent intent = getErrorResolutionIntent(state);
111         String label = getErrorResolutionLabel(state);
112         handleNewPlaybackState(displayedMessage, intent, label);
113     }
114 
115 
116     @Nullable
getDisplayedMessage(Context ctx, @Nullable PlaybackStateWrapper state)117     private String getDisplayedMessage(Context ctx, @Nullable PlaybackStateWrapper state) {
118         if (state == null) {
119             return null;
120         }
121         if (!TextUtils.isEmpty(state.getErrorMessage())) {
122             return state.getErrorMessage().toString();
123         }
124         // ERROR_CODE_UNKNOWN_ERROR means there is no error in PlaybackState.
125         if (state.getErrorCode() != PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR) {
126             Integer messageId = ERROR_CODE_MESSAGES_MAP.get(state.getErrorCode());
127             return messageId != null ? ctx.getString(messageId) : ctx.getString(
128                     R.string.default_error_message);
129         }
130         if (state.getState() == PlaybackStateCompat.STATE_ERROR) {
131             return ctx.getString(R.string.default_error_message);
132         }
133         return null;
134     }
135 
136     @Nullable
getErrorResolutionIntent(@onNull PlaybackStateWrapper state)137     private PendingIntent getErrorResolutionIntent(@NonNull PlaybackStateWrapper state) {
138         Bundle extras = state.getExtras();
139         return extras == null ? null : extras.getParcelable(
140                 MediaConstants.ERROR_RESOLUTION_ACTION_INTENT);
141     }
142 
143     @Nullable
getErrorResolutionLabel(@onNull PlaybackStateWrapper state)144     private String getErrorResolutionLabel(@NonNull PlaybackStateWrapper state) {
145         Bundle extras = state.getExtras();
146         return extras == null ? null : extras.getString(
147                 MediaConstants.ERROR_RESOLUTION_ACTION_LABEL);
148     }
149 
150 }
151