public class CarHelper { private static final String TAG = LogHelper.makeLogTag(CarHelper.class); private static final String AUTO_APP_PACKAGE_NAME = "com.google.android.projection.gearhead"; // Use these extras to reserve space for the corresponding actions, even when they are disabled // in the playbackstate, so the custom actions don't reflow. private static final String SLOT_RESERVATION_SKIP_TO_NEXT = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"; private static final String SLOT_RESERVATION_SKIP_TO_PREV = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"; private static final String SLOT_RESERVATION_QUEUE = "com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE"; /** * Action for an intent broadcast by Android Auto when a media app is connected or disconnected. A * "connected" media app is the one currently attached to the "media" facet on Android Auto. So, * this intent is sent by AA on: * * <p>- connection: when the phone is projecting and at the moment the app is selected from the * list of media apps - disconnection: when another media app is selected from the list of media * apps or when the phone stops projecting (when the user unplugs it, for example) * * <p>The actual event (connected or disconnected) will come as an Intent extra, with the key * MEDIA_CONNECTION_STATUS (see below). */ public static final String ACTION_MEDIA_STATUS = "com.google.android.gms.car.media.STATUS"; /** * Key in Intent extras that contains the media connection event type (connected or disconnected) */ public static final String MEDIA_CONNECTION_STATUS = "media_connection_status"; /** * Value of the key MEDIA_CONNECTION_STATUS in Intent extras used when the current media app is * connected. */ public static final String MEDIA_CONNECTED = "media_connected"; /** * Value of the key MEDIA_CONNECTION_STATUS in Intent extras used when the current media app is * disconnected. */ public static final String MEDIA_DISCONNECTED = "media_disconnected"; public static boolean isValidCarPackage(String packageName) { return AUTO_APP_PACKAGE_NAME.equals(packageName); } public static void setSlotReservationFlags( Bundle extras, boolean reservePlayingQueueSlot, boolean reserveSkipToNextSlot, boolean reserveSkipToPrevSlot) { if (reservePlayingQueueSlot) { extras.putBoolean(SLOT_RESERVATION_QUEUE, true); } else { extras.remove(SLOT_RESERVATION_QUEUE); } if (reserveSkipToPrevSlot) { extras.putBoolean(SLOT_RESERVATION_SKIP_TO_PREV, true); } else { extras.remove(SLOT_RESERVATION_SKIP_TO_PREV); } if (reserveSkipToNextSlot) { extras.putBoolean(SLOT_RESERVATION_SKIP_TO_NEXT, true); } else { extras.remove(SLOT_RESERVATION_SKIP_TO_NEXT); } } /** * Returns true when running Android Auto or a car dock. * * <p>A preferable way of detecting if your app is running in the context of an Android Auto * compatible car is by registering a BroadcastReceiver for the action {@link * CarHelper#ACTION_MEDIA_STATUS}. See a sample implementation in {@link MusicService#onCreate()}. * * @param c Context to detect UI Mode. * @return true when device is running in car mode, false otherwise. */ public static boolean isCarUiMode(Context c) { UiModeManager uiModeManager = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE); if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { LogHelper.d(TAG, "Running in Car mode"); return true; } else { LogHelper.d(TAG, "Running on a non-Car mode"); return false; } } }
/** Utility class to help on queue related tasks. */ public class QueueHelper { private static final String TAG = LogHelper.makeLogTag(QueueHelper.class); public static List<MediaSession.QueueItem> getPlayingQueue( String mediaId, MusicProvider musicProvider) { // extract the browsing hierarchy from the media ID: String[] hierarchy = MediaIDHelper.getHierarchy(mediaId); if (hierarchy.length != 2) { LogHelper.e(TAG, "Could not build a playing queue for this mediaId: ", mediaId); return null; } String categoryType = hierarchy[0]; String categoryValue = hierarchy[1]; LogHelper.d(TAG, "Creating playing queue for ", categoryType, ", ", categoryValue); Iterable<MediaMetadata> tracks = null; // This sample only supports genre and by_search category types. if (categoryType.equals(MEDIA_ID_MUSICS_BY_GENRE)) { tracks = musicProvider.getMusicsByGenre(categoryValue); } else if (categoryType.equals(MEDIA_ID_MUSICS_BY_SEARCH)) { tracks = musicProvider.searchMusicBySongTitle(categoryValue); } if (tracks == null) { LogHelper.e(TAG, "Unrecognized category type: ", categoryType, " for media ", mediaId); return null; } return convertToQueue(tracks, hierarchy[0], hierarchy[1]); } public static List<MediaSession.QueueItem> getPlayingQueueFromSearch( String query, Bundle queryParams, MusicProvider musicProvider) { LogHelper.d( TAG, "Creating playing queue for musics from search: ", query, " params=", queryParams); VoiceSearchParams params = new VoiceSearchParams(query, queryParams); LogHelper.d(TAG, "VoiceSearchParams: ", params); if (params.isAny) { // If isAny is true, we will play anything. This is app-dependent, and can be, // for example, favorite playlists, "I'm feeling lucky", most recent, etc. return getRandomQueue(musicProvider); } Iterable<MediaMetadata> result = null; if (params.isAlbumFocus) { result = musicProvider.searchMusicByAlbum(params.album); } else if (params.isGenreFocus) { result = musicProvider.getMusicsByGenre(params.genre); } else if (params.isArtistFocus) { result = musicProvider.searchMusicByArtist(params.artist); } else if (params.isSongFocus) { result = musicProvider.searchMusicBySongTitle(params.song); } // If there was no results using media focus parameter, we do an unstructured query. // This is useful when the user is searching for something that looks like an artist // to Google, for example, but is not. For example, a user searching for Madonna on // a PodCast application wouldn't get results if we only looked at the // Artist (podcast author). Then, we can instead do an unstructured search. if (params.isUnstructured || result == null || !result.iterator().hasNext()) { // To keep it simple for this example, we do unstructured searches on the // song title only. A real world application could search on other fields as well. result = musicProvider.searchMusicBySongTitle(query); } return convertToQueue(result, MEDIA_ID_MUSICS_BY_SEARCH, query); } public static int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue, String mediaId) { int index = 0; for (MediaSession.QueueItem item : queue) { if (mediaId.equals(item.getDescription().getMediaId())) { return index; } index++; } return -1; } public static int getMusicIndexOnQueue(Iterable<MediaSession.QueueItem> queue, long queueId) { int index = 0; for (MediaSession.QueueItem item : queue) { if (queueId == item.getQueueId()) { return index; } index++; } return -1; } private static List<MediaSession.QueueItem> convertToQueue( Iterable<MediaMetadata> tracks, String... categories) { List<MediaSession.QueueItem> queue = new ArrayList<>(); int count = 0; for (MediaMetadata track : tracks) { // We create a hierarchy-aware mediaID, so we know what the queue is about by looking // at the QueueItem media IDs. String hierarchyAwareMediaID = MediaIDHelper.createMediaID(track.getDescription().getMediaId(), categories); MediaMetadata trackCopy = new MediaMetadata.Builder(track) .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID) .build(); // We don't expect queues to change after created, so we use the item index as the // queueId. Any other number unique in the queue would work. MediaSession.QueueItem item = new MediaSession.QueueItem(trackCopy.getDescription(), count++); queue.add(item); } return queue; } /** * Create a random queue. * * @param musicProvider the provider used for fetching music. * @return list containing {@link MediaSession.QueueItem}'s */ public static List<MediaSession.QueueItem> getRandomQueue(MusicProvider musicProvider) { List<MediaMetadata> result = new ArrayList<>(); for (String genre : musicProvider.getGenres()) { Iterable<MediaMetadata> tracks = musicProvider.getMusicsByGenre(genre); for (MediaMetadata track : tracks) { if (ThreadLocalRandom.current().nextBoolean()) { result.add(track); } } } LogHelper.d(TAG, "getRandomQueue: result.size=", result.size()); Collections.shuffle(result); return convertToQueue(result, MEDIA_ID_MUSICS_BY_SEARCH, "random"); } public static boolean isIndexPlayable(int index, List<MediaSession.QueueItem> queue) { return (queue != null && index >= 0 && index < queue.size()); } }