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