1 /*
2  * Copyright (c) 2019, 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 package com.android.car.media.testmediaapp;
17 
18 import static com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNodeType.LEAF_CHILDREN;
19 import static com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNodeType.QUEUE_ONLY;
20 import static com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaLoginEventOrder.PLAYBACK_STATE_UPDATE_FIRST;
21 
22 import android.app.PendingIntent;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.media.AudioManager;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.support.v4.media.MediaBrowserCompat.MediaItem;
30 import android.support.v4.media.session.MediaSessionCompat;
31 import android.support.v4.media.session.PlaybackStateCompat;
32 import android.util.Log;
33 
34 import androidx.annotation.NonNull;
35 import androidx.annotation.Nullable;
36 import androidx.media.MediaBrowserServiceCompat;
37 import androidx.media.session.MediaButtonReceiver;
38 
39 import com.android.car.media.testmediaapp.loader.TmaLoader;
40 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaAccountType;
41 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaBrowseNodeType;
42 import com.android.car.media.testmediaapp.prefs.TmaEnumPrefs.TmaReplyDelay;
43 import com.android.car.media.testmediaapp.prefs.TmaPrefs;
44 
45 import java.util.ArrayList;
46 import java.util.List;
47 import java.util.Objects;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 
51 
52 /**
53  * Implementation of {@link MediaBrowserServiceCompat} that delivers {@link MediaItem}s based on
54  * json configuration files stored in the application's assets. Those assets combined with a few
55  * preferences (see: {@link TmaPrefs}), allow to create a variety of use cases (including error
56  * states) to stress test the Car Media application. <p/>
57  * The media items are cached in the {@link TmaLibrary}, and can be virtually played with
58  * {@link TmaPlayer}.
59  */
60 public class TmaBrowser extends MediaBrowserServiceCompat {
61     private static final String TAG = "TmaBrowser";
62 
63     private static final int MAX_SEARCH_DEPTH = 4;
64     private static final String MEDIA_SESSION_TAG = "TEST_MEDIA_SESSION";
65     private static final String ROOT_ID = "_ROOT_ID_";
66     private static final String SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED";
67     /**
68      * Extras key to allow Android Auto to identify the browse service from the media session.
69      */
70     private static final String BROWSE_SERVICE_FOR_SESSION_KEY =
71         "android.media.session.BROWSE_SERVICE";
72 
73     private TmaPrefs mPrefs;
74     private Handler mHandler;
75     private MediaSessionCompat mSession;
76     private TmaLibrary mLibrary;
77     private TmaPlayer mPlayer;
78 
79     private BrowserRoot mRoot;
80 
TmaBrowser()81     public TmaBrowser() {
82         super();
83     }
84 
85     @Override
onCreate()86     public void onCreate() {
87         super.onCreate();
88         mPrefs = TmaPrefs.getInstance(this);
89         mHandler = new Handler();
90 
91         ComponentName mbrComponent = MediaButtonReceiver.getMediaButtonReceiverComponent(this);
92         Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
93         mediaButtonIntent.setComponent(mbrComponent);
94         PendingIntent mbrIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent,
95                 PendingIntent.FLAG_IMMUTABLE);
96 
97         mSession = new MediaSessionCompat(this, MEDIA_SESSION_TAG, mbrComponent, mbrIntent);
98         setSessionToken(mSession.getSessionToken());
99 
100         mLibrary = new TmaLibrary(new TmaLoader(this));
101         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
102         mPlayer = new TmaPlayer(this, mLibrary, audioManager, mHandler, mSession);
103 
104         mSession.setCallback(mPlayer);
105         mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
106                 | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
107         Bundle mediaSessionExtras = new Bundle();
108         mediaSessionExtras.putString(BROWSE_SERVICE_FOR_SESSION_KEY, TmaBrowser.class.getName());
109         mSession.setExtras(mediaSessionExtras);
110 
111         mPrefs.mAccountType.registerChangeListener(mOnAccountChanged);
112         mPrefs.mRootNodeType.registerChangeListener(mOnRootNodeTypeChanged);
113         mPrefs.mRootReplyDelay.registerChangeListener(mOnReplyDelayChanged);
114 
115         Bundle browserRootExtras = new Bundle();
116         browserRootExtras.putBoolean(SEARCH_SUPPORTED, true);
117         mRoot = new BrowserRoot(ROOT_ID, browserRootExtras);
118 
119         updatePlaybackState(mPrefs.mAccountType.getValue());
120     }
121 
122     @Override
onDestroy()123     public void onDestroy() {
124         mPrefs.mAccountType.unregisterChangeListener(mOnAccountChanged);
125         mPrefs.mRootNodeType.unregisterChangeListener(mOnRootNodeTypeChanged);
126         mPrefs.mRootReplyDelay.unregisterChangeListener(mOnReplyDelayChanged);
127         mSession.release();
128         mHandler = null;
129         mPrefs = null;
130         super.onDestroy();
131     }
132 
133     private final TmaPrefs.PrefValueChangedListener<TmaAccountType> mOnAccountChanged =
134             (oldValue, newValue) -> {
135                 if (PLAYBACK_STATE_UPDATE_FIRST.equals(mPrefs.mLoginEventOrder.getValue())) {
136                     updatePlaybackState(newValue);
137                     invalidateRoot();
138                 } else {
139                     invalidateRoot();
140                     (new Handler()).postDelayed(() -> {
141                         updatePlaybackState(newValue);
142                     }, 3000);
143                 }
144             };
145 
146     private final TmaPrefs.PrefValueChangedListener<TmaBrowseNodeType> mOnRootNodeTypeChanged =
147             (oldValue, newValue) -> invalidateRoot();
148 
149     private final TmaPrefs.PrefValueChangedListener<TmaReplyDelay> mOnReplyDelayChanged =
150             (oldValue, newValue) -> invalidateRoot();
151 
updatePlaybackState(TmaAccountType accountType)152     private void updatePlaybackState(TmaAccountType accountType) {
153         if (accountType == TmaAccountType.NONE) {
154             mSession.setMetadata(null);
155             mPlayer.onStop();
156             mPlayer.setPlaybackState(
157                     new TmaMediaEvent(TmaMediaEvent.EventState.ERROR,
158                             TmaMediaEvent.StateErrorCode.AUTHENTICATION_EXPIRED,
159                             getResources().getString(R.string.no_account),
160                             getResources().getString(R.string.select_account),
161                             TmaMediaEvent.ResolutionIntent.PREFS,
162                             TmaMediaEvent.Action.NONE, 0, null, null));
163         } else {
164             // TODO don't reset error in all cases...
165             PlaybackStateCompat.Builder playbackState = new PlaybackStateCompat.Builder();
166             playbackState.setState(PlaybackStateCompat.STATE_PAUSED, 0, 0);
167             playbackState.setActions(PlaybackStateCompat.ACTION_PREPARE);
168             mSession.setPlaybackState(playbackState.build());
169         }
170     }
171 
invalidateRoot()172     private void invalidateRoot() {
173         notifyChildrenChanged(ROOT_ID);
174     }
175 
176     @Override
onGetRoot( @onNull String clientPackageName, int clientUid, Bundle rootHints)177     public BrowserRoot onGetRoot(
178             @NonNull String clientPackageName, int clientUid, Bundle rootHints) {
179         if (rootHints == null) {
180             Log.e(TAG, "Client " + clientPackageName + " didn't set rootHints.");
181             throw new NullPointerException("rootHints is null");
182         }
183         Log.i(TAG, "onGetroot client: " + clientPackageName + " EXTRA_MEDIA_ART_SIZE_HINT_PIXELS: "
184                 + rootHints.getInt(MediaKeys.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, 0));
185         return mRoot;
186     }
187 
188     @Override
onLoadChildren(@onNull String parentId, @NonNull Result<List<MediaItem>> result)189     public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaItem>> result) {
190         getMediaItemsWithDelay(parentId, result, null);
191 
192         if (QUEUE_ONLY.equals(mPrefs.mRootNodeType.getValue()) && ROOT_ID.equals(parentId)) {
193             TmaMediaItem queue = mLibrary.getRoot(LEAF_CHILDREN);
194             if (queue != null) {
195                 mSession.setQueue(queue.buildQueue());
196                 mPlayer.prepareMediaItem(queue.getPlayableByIndex(0));
197             }
198         }
199     }
200 
201     @Override
onSearch(@onNull String query, Bundle extras, @NonNull Result<List<MediaItem>> result)202     public void onSearch(@NonNull String query, Bundle extras,
203             @NonNull Result<List<MediaItem>> result) {
204         getMediaItemsWithDelay(ROOT_ID, result, query);
205     }
206 
getRoot()207     private TmaMediaItem getRoot() {
208         return mLibrary.getRoot(mPrefs.mRootNodeType.getValue());
209     }
210 
getMediaItemsWithDelay(@onNull String parentId, @NonNull Result<List<MediaItem>> result, @Nullable String filter)211     private void getMediaItemsWithDelay(@NonNull String parentId,
212             @NonNull Result<List<MediaItem>> result, @Nullable String filter) {
213         // TODO: allow per item override of the delay ?
214         TmaReplyDelay delay = mPrefs.mRootReplyDelay.getValue();
215         Runnable task = () -> {
216             TmaMediaItem node;
217             if (TmaAccountType.NONE.equals(mPrefs.mAccountType.getValue())) {
218                 node = null;
219             } else if (ROOT_ID.equals(parentId)) {
220                 node = getRoot();
221             } else {
222                 node = mLibrary.getMediaItemById(parentId);
223             }
224 
225             if (node == null) {
226                 result.sendResult(null);
227             } else if (filter != null) {
228                 List<MediaItem> hits = new ArrayList<>(50);
229                 Pattern pat = Pattern.compile(Pattern.quote(filter), Pattern.CASE_INSENSITIVE);
230                 addSearchResults(node, pat.matcher(""), hits, MAX_SEARCH_DEPTH);
231                 result.sendResult(hits);
232             } else {
233                 List<TmaMediaItem> children = node.getChildren();
234                 int childrenCount = children.size();
235                 List<MediaItem> items = new ArrayList<>(childrenCount);
236                 if (childrenCount <= 0) {
237                     result.sendResult(items);
238                 } else {
239                     int selfUpdateDelay = node.getSelfUpdateDelay();
240                     int toShow = (selfUpdateDelay > 0) ? 1 + node.mRevealCounter : childrenCount;
241                     for (int childIndex = 0 ; childIndex < toShow; childIndex++) {
242                         TmaMediaItem child = children.get(childIndex);
243                         if (child.mIsHidden) {
244                             continue;
245                         }
246                         items.add(child.toMediaItem());
247                     }
248                     result.sendResult(items);
249 
250                     if (selfUpdateDelay > 0) {
251                         mHandler.postDelayed(new UpdateNodeTask(parentId), selfUpdateDelay);
252                         node.mRevealCounter = (node.mRevealCounter + 1) % (childrenCount);
253                     }
254                 }
255             }
256         };
257         if (delay == TmaReplyDelay.NONE) {
258             task.run();
259         } else {
260             result.detach();
261             mHandler.postDelayed(task, delay.mReplyDelayMs);
262         }
263     }
264 
addSearchResults(@ullable TmaMediaItem node, Matcher matcher, List<MediaItem> hits, int currentDepth)265     private void addSearchResults(@Nullable TmaMediaItem node, Matcher matcher,
266             List<MediaItem> hits, int currentDepth) {
267         if (node == null || currentDepth <= 0) {
268             return;
269         }
270 
271         for (TmaMediaItem child : node.getChildren()) {
272             if (child.mIsHidden) {
273                 continue;
274             }
275             MediaItem item = child.toMediaItem();
276             CharSequence title = item.getDescription().getTitle();
277             if (title != null) {
278                 matcher.reset(title);
279                 if (matcher.find()) {
280                     hits.add(item);
281                 }
282             }
283 
284             // Ask the library to load the grand children
285             child = mLibrary.getMediaItemById(child.getMediaId());
286             addSearchResults(child, matcher, hits, currentDepth - 1);
287         }
288     }
289 
toggleItem(@ullable TmaMediaItem item)290     void toggleItem(@Nullable TmaMediaItem item) {
291         if (item == null) {
292             return;
293         }
294         item.mIsHidden = !item.mIsHidden;
295         if (item.getParent() != null) {
296             String parentId = item.getParent().getMediaId();
297             if (Objects.equals(parentId, getRoot().getMediaId())) {
298                 parentId = ROOT_ID;
299             }
300             notifyChildrenChanged(parentId);
301         }
302     }
303 
304     private class UpdateNodeTask implements Runnable {
305 
306         private final String mNodeId;
307 
UpdateNodeTask(@onNull String nodeId)308         UpdateNodeTask(@NonNull String nodeId) {
309             mNodeId = nodeId;
310         }
311 
312         @Override
run()313         public void run() {
314             notifyChildrenChanged(mNodeId);
315         }
316     }
317 }
318