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