Java tutorial
/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.murati.oszk.audiobook.model; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.net.Uri; import android.os.AsyncTask; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.util.Log; import com.murati.oszk.audiobook.OfflineBookService; import com.murati.oszk.audiobook.R; import com.murati.oszk.audiobook.utils.BitmapHelper; import com.murati.oszk.audiobook.utils.FavoritesHelper; import com.murati.oszk.audiobook.utils.LogHelper; import com.murati.oszk.audiobook.utils.MediaIDHelper; import com.murati.oszk.audiobook.utils.PlaybackHelper; import com.murati.oszk.audiobook.utils.TextHelper; import java.text.Collator; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import static android.media.MediaMetadata.METADATA_KEY_TRACK_NUMBER; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_BY_DOWNLOADS; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_BY_EBOOK; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_BY_FAVORITES; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_BY_GENRE; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_CATEGORY_HEADER; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_EBOOK_HEADER; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_BY_QUEUE; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_BY_SEARCH; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_BY_WRITER; import static com.murati.oszk.audiobook.utils.MediaIDHelper.MEDIA_ID_ROOT; import static com.murati.oszk.audiobook.utils.MediaIDHelper.createMediaID; import static com.murati.oszk.audiobook.utils.MediaIDHelper.getCategoryValueFromMediaID; /** * Simple data provider for music tracks. The actual metadata source is delegated to a * MusicProviderSource defined by a constructor argument of this class. */ public class MusicProvider { private static final String TAG = LogHelper.makeLogTag(MusicProvider.class); private MusicProviderSource mSource; // Ebook cache private static ConcurrentMap<String, List<MediaMetadataCompat>> mEbookList; private static ConcurrentMap<String, MutableMediaMetadata> mTrackListById; // Category caches private static ConcurrentMap<String, List<String>> mEbookListByGenre; private static ConcurrentMap<String, List<String>> mEbookListByWriter; enum State { NON_INITIALIZED, INITIALIZING, INITIALIZED } private static volatile State mCurrentState = State.NON_INITIALIZED; Collator collator = Collator.getInstance(Locale.GERMAN); public interface Callback { void onMusicCatalogReady(boolean success); } public MusicProvider(Context c) { //this(new RemoteJSONSource()); this(new OfflineJSONSource(c), c); //TODO: new thread for favorites try { FavoritesHelper.setContext(c); FavoritesHelper.loadFavorites(); } catch (Exception e) { Log.e(TAG, e.getMessage()); } PlaybackHelper.setContext(c); } public MusicProvider(MusicProviderSource source, Context c) { mSource = source; if (mTrackListById == null) { mTrackListById = new ConcurrentHashMap<>(); mEbookList = new ConcurrentHashMap<>(); //TODO: rethink dynamic/async construction mEbookListByGenre = new ConcurrentHashMap<>(); mEbookListByWriter = new ConcurrentHashMap<>(); } } //region EBOOK_GETTERS public Iterable<String> getEbooksByGenre(String genre) { if (mCurrentState != State.INITIALIZED || !mEbookListByGenre.containsKey(genre)) { return Collections.emptyList(); } List<String> ebookList = mEbookListByGenre.get(genre); Collections.sort(ebookList, collator); return ebookList; } public Iterable<String> getEbooksByWriter(String writer) { if (mCurrentState != State.INITIALIZED || !mEbookListByWriter.containsKey(writer)) { return Collections.emptyList(); } List<String> ebookList = mEbookListByWriter.get(writer); Collections.sort(ebookList, collator); return ebookList; } public static Iterable<MediaMetadataCompat> getTracksByEbook(String ebook) { if (mCurrentState != State.INITIALIZED || !mEbookList.containsKey(ebook)) { return Collections.emptyList(); } return mEbookList.get(ebook); } public Iterable<String> getEbooksByQueryString(String query) { if (mCurrentState != State.INITIALIZED) { return Collections.emptyList(); } TreeSet<String> sortedEbookTitles = new TreeSet<String>(); //TODO: Handle accents query = query.toLowerCase(Locale.US); for (MutableMediaMetadata track : mTrackListById.values()) { String title = track.metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM); if (!sortedEbookTitles.contains(title)) { String search_fields = track.metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM) + "|" + track.metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE) + "|" + track.metadata.getString(MediaMetadataCompat.METADATA_KEY_WRITER) + "|" + track.metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE); if (search_fields.toLowerCase(Locale.US).contains(query)) { sortedEbookTitles.add(title); } } } return sortedEbookTitles; } //endregion /** * Return the MediaMetadataCompat for the given musicID. * * @param musicId The unique, non-hierarchical music ID. */ public static MediaMetadataCompat getTrack(String musicId) { return mTrackListById.containsKey(musicId) ? mTrackListById.get(musicId).metadata : null; } public synchronized void updateTrackArt(String musicId, Bitmap albumArt, Bitmap icon) { MediaMetadataCompat metadata = getTrack(musicId); metadata = new MediaMetadataCompat.Builder(metadata) // set high resolution bitmap in METADATA_KEY_ALBUM_ART. This is used, for // example, on the lockscreen background when the media session is active. .putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, albumArt) // set small version of the album art in the DISPLAY_ICON. This is used on // the MediaDescription and thus it should be small to be serialized if // necessary .putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, icon) .build(); MutableMediaMetadata mutableMetadata = mTrackListById.get(musicId); if (mutableMetadata == null) { throw new IllegalStateException("Unexpected error: Inconsistent data structures in " + "MusicProvider"); } mutableMetadata.metadata = metadata; } public boolean isInitialized() { return mCurrentState == State.INITIALIZED; } /** * Get the list of music tracks from a server and caches the track information * for future reference, keying tracks by musicId and grouping by genre. */ public void retrieveMediaAsync(final Callback callback) { LogHelper.d(TAG, "retrieveMediaAsync called"); if (mCurrentState == State.INITIALIZED) { if (callback != null) { // Nothing to do, execute callback immediately callback.onMusicCatalogReady(true); } return; } // Asynchronously load the music catalog in a separate thread new AsyncTask<Void, Void, State>() { @Override protected State doInBackground(Void... params) { retrieveMedia(); return mCurrentState; } @Override protected void onPostExecute(State current) { if (callback != null) { callback.onMusicCatalogReady(current == State.INITIALIZED); } } }.execute(); } //region BUILD_TYPELISTS private synchronized void buildAlbumList() { //TODO: rename album to ebook ConcurrentMap<String, List<MediaMetadataCompat>> newAlbumList = new ConcurrentHashMap<>(); // Add tracks to ebook for (MutableMediaMetadata m : mTrackListById.values()) { String album = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM); List<MediaMetadataCompat> list = newAlbumList.get(album); if (list == null) { list = new ArrayList<>(); newAlbumList.put(album, list); } list.add(m.metadata); } //Sort Individual ebooks by track numbers for (List<MediaMetadataCompat> album : newAlbumList.values()) { //MediaMetadataCompat[] sortedOrder = album.toArray(new MediaMetadataCompat[album.size()]); java.util.Collections.sort(album, new Comparator<MediaMetadataCompat>() { @Override public int compare(final MediaMetadataCompat lhs, MediaMetadataCompat rhs) { if (lhs.getLong(METADATA_KEY_TRACK_NUMBER) < rhs.getLong(METADATA_KEY_TRACK_NUMBER)) return -1; return 1; } }); } mEbookList = newAlbumList; } private synchronized void addMediaToCategory(MutableMediaMetadata m, String metadata, ConcurrentMap<String, List<String>> newListByMetadata) { // Get Key String metaValueString = m.metadata.getString(metadata); for (String mv : metaValueString.split(",")) { //TODO: Client resource translations String key = mv.replaceAll("\\(.*\\)", ""); if (key.matches("^(\\d+|\\.).*")) { // Numbers or dots Log.i(TAG, "Skipping " + key); continue; } key = TextHelper.Capitalize(key); // Get List by Key List<String> list = newListByMetadata.get(key); if (list == null) { list = new ArrayList<>(); newListByMetadata.put(key, list); } // Add ebook by key String ebook = m.metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM); if (!list.contains(ebook)) { list.add(ebook); } } } // Load MediaData from mSource private synchronized void retrieveMedia() { try { if (mCurrentState == State.NON_INITIALIZED) { mCurrentState = State.INITIALIZING; Iterator<MediaMetadataCompat> tracks = mSource.iterator(); mEbookListByGenre = new ConcurrentHashMap<>(); mEbookListByWriter = new ConcurrentHashMap<>(); while (tracks.hasNext()) { MediaMetadataCompat item = tracks.next(); String musicId = item.getString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID); MutableMediaMetadata m = new MutableMediaMetadata(musicId, item); mTrackListById.put(musicId, m); addMediaToCategory(m, MediaMetadataCompat.METADATA_KEY_GENRE, mEbookListByGenre); addMediaToCategory(m, MediaMetadataCompat.METADATA_KEY_WRITER, mEbookListByWriter); } Long startTime = System.currentTimeMillis(); Log.d(TAG, "Build catalog started at " + startTime.toString()); buildAlbumList(); Long endTime = System.currentTimeMillis(); Log.d(TAG, "Build catalog finished at " + endTime.toString()); Log.d(TAG, "Build time was: " + Long.toString(endTime - startTime)); mCurrentState = State.INITIALIZED; } } catch (Exception e) { LogHelper.e(e.getMessage()); } finally { if (mCurrentState != State.INITIALIZED) mCurrentState = State.NON_INITIALIZED; } } //endregion //region Hierarchy browser public List<MediaBrowserCompat.MediaItem> getChildren(String mediaId, Resources resources) { List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>(); if (mediaId == null) return Collections.emptyList(); if (MEDIA_ID_ROOT.equals(mediaId)) { try { // Show Last ebook if (PlaybackHelper.getLastEBook() != null) { mediaItems.add(createHeader(resources.getString(R.string.browse_queue_subtitle))); mediaItems.add(new MediaBrowserCompat.MediaItem(PlaybackHelper.getLastEBookDescriptor(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)); } } catch (Exception ex) { Log.d(TAG, "Error restoring last-ebook tile" + ex.getMessage()); } // Catalog header mediaItems.add(createHeader(resources.getString(R.string.label_catalog))); // Add writers mediaItems.add(createGroupItem(MEDIA_ID_BY_WRITER, resources.getString(R.string.browse_writer), resources.getString(R.string.browse_writer_subtitle), BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_writer))); // Add Genres mediaItems.add(createGroupItem(MEDIA_ID_BY_GENRE, resources.getString(R.string.browse_genres), resources.getString(R.string.browse_genre_subtitle), BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_list))); // Add EBooks mediaItems.add(createGroupItem(MEDIA_ID_BY_EBOOK, resources.getString(R.string.browse_ebook), resources.getString(R.string.browse_ebook_subtitle), BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_books))); // Add Favorites mediaItems.add(createGroupItem(MEDIA_ID_BY_FAVORITES, resources.getString(R.string.browse_favorites), resources.getString(R.string.browse_favorites_subtitle), BitmapHelper.convertDrawabletoUri(R.drawable.ic_star_on))); // Add Offline mediaItems.add(createGroupItem(MEDIA_ID_BY_DOWNLOADS, resources.getString(R.string.browse_downloads), resources.getString(R.string.browse_downloads_subtitle), BitmapHelper.convertDrawabletoUri(R.drawable.ic_action_download))); return mediaItems; } // Not root section //Refresh button for empty states if (mCurrentState != State.INITIALIZED) { mediaItems.add(createRefreshItem(mediaId, resources)); return mediaItems; } try { // Swap mediaId from queue to current eBook if (mediaId.equals(MEDIA_ID_BY_QUEUE)) return getChildren(PlaybackHelper.getLastEBook(), resources); //Rethink edgecase of misbrowse if (!MediaIDHelper.isBrowseable(mediaId)) { return mediaItems; } // Search ebooks by Query String if (mediaId.startsWith(MEDIA_ID_BY_SEARCH)) { String search_query = MediaIDHelper.extractMusicIDFromMediaID(mediaId); for (String ebook : getEbooksByQueryString(search_query)) { mediaItems.add(createEbookItem(ebook, resources)); } } // List all Genre Items else if (MEDIA_ID_BY_GENRE.equals(mediaId)) { mediaItems.addAll(createGroupList(mEbookListByGenre, MEDIA_ID_BY_GENRE, BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_list), resources)); } // List ebooks in a specific Genre else if (mediaId.startsWith(MEDIA_ID_BY_GENRE)) { String genre = MediaIDHelper.getHierarchy(mediaId)[1]; for (String ebook : getEbooksByGenre(genre)) mediaItems.add(createEbookItem(ebook, resources)); } // List Writers else if (MEDIA_ID_BY_WRITER.equals(mediaId)) { mediaItems.addAll(createGroupList(mEbookListByWriter, MEDIA_ID_BY_WRITER, BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_writer), resources)); } // Open a specific Genre else if (mediaId.startsWith(MEDIA_ID_BY_WRITER)) { String writer = MediaIDHelper.getHierarchy(mediaId)[1]; for (String ebook : getEbooksByWriter(writer)) mediaItems.add(createEbookItem(ebook, resources)); } // List all EBooks Items else if (MEDIA_ID_BY_EBOOK.equals(mediaId)) { TreeSet<String> sortedEbookTitles = new TreeSet<String>(collator); sortedEbookTitles.addAll(mEbookList.keySet()); for (String ebook : sortedEbookTitles) { mediaItems.add(createEbookItem(ebook, resources)); } } // List all Favorites else if (MEDIA_ID_BY_FAVORITES.equals(mediaId)) { for (String ebook : FavoritesHelper.getFavorites()) { try { if (ebook.startsWith(MEDIA_ID_BY_EBOOK)) { String title = getCategoryValueFromMediaID(ebook); mediaItems.add(createEbookItem(title, resources)); } } catch (Exception ex) { Log.i(TAG, "Exception listing favorite:" + ebook); } } } // List all Downloads else if (MEDIA_ID_BY_DOWNLOADS.equals(mediaId)) { //TODO: canonical book list List<String> offlineBookList = OfflineBookService.getOfflineBooks(); //TODO: generalize errorhandling - with customized exception/message if (offlineBookList != null) { for (String ebook : offlineBookList) { try { mediaItems.add(createEbookItem(ebook, resources)); } catch (Exception ex) { Log.i(TAG, "Exception listing favorite:" + ebook); } } } } // Open a specific Ebook for direct play else if (mediaId.startsWith(MEDIA_ID_BY_EBOOK)) { // Add header mediaItems.add(createEbookHeader(mediaId, resources)); //Add tracks String ebook = MediaIDHelper.getHierarchy(mediaId)[1]; for (MediaMetadataCompat metadata : getTracksByEbook(ebook)) { mediaItems.add(createTrackItem(metadata)); } } // Can't open media else { LogHelper.w(TAG, "Skipping unmatched mediaId: ", mediaId); } } catch (Exception ex) { //TODO: signal errors properly to UI LogHelper.w(TAG, "Something went wrong processing", mediaId); Log.d(TAG, ex.getMessage()); } return mediaItems; } //endregion //region BROWSABLE_ITEMS private Collection<MediaBrowserCompat.MediaItem> createGroupList( ConcurrentMap<String, List<String>> categoryMap, String mediaIdCategory, Uri imageUri, Resources resources) { if (mCurrentState != State.INITIALIZED) return Collections.emptyList(); TreeSet<String> sortedCategoryList = new TreeSet<String>(collator); sortedCategoryList.addAll(categoryMap.keySet()); List<MediaBrowserCompat.MediaItem> categoryList = new ArrayList<MediaBrowserCompat.MediaItem>(); for (String categoryName : sortedCategoryList) { try { MediaBrowserCompat.MediaItem browsableCategory = createGroupItem( createMediaID(null, mediaIdCategory, categoryName), categoryName, String.format(resources.getString(R.string.browse_title_count), String.valueOf(categoryMap.get(categoryName).size())), imageUri); categoryList.add(browsableCategory); } catch (Exception e) { //TODO: log } } return categoryList; } private MediaBrowserCompat.MediaItem createGroupItem(String mediaId, String title, String subtitle, Uri iconUri) { MediaDescriptionCompat description = new MediaDescriptionCompat.Builder().setMediaId(mediaId) .setTitle(title).setSubtitle(subtitle).setIconUri(iconUri).build(); return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); } private MediaBrowserCompat.MediaItem createEbookItem(String ebook, Resources resources) { //TODO: canonize ebook mediaid and title conversion if (ebook.startsWith(MEDIA_ID_BY_EBOOK)) { ebook = getCategoryValueFromMediaID(ebook); } MediaMetadataCompat metadata = getTracksByEbook(ebook).iterator().next(); MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() .setMediaId(createMediaID(null, MEDIA_ID_BY_EBOOK, ebook)).setTitle(ebook) .setSubtitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_WRITER)) //TODO: Fix Album art .setIconUri(Uri.parse(metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI))) //TODO: fix default image // .setIconBitmap(BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_books)) .build(); return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); } private MediaBrowserCompat.MediaItem createHeader(String title) { MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() .setMediaId(createMediaID(null, MEDIA_ID_CATEGORY_HEADER, title)).setTitle(title).build(); return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); } private MediaBrowserCompat.MediaItem createEbookHeader(String ebook, Resources resources) { //TODO: canonize ebook mediaid and title conversion if (ebook.startsWith(MEDIA_ID_BY_EBOOK)) { ebook = getCategoryValueFromMediaID(ebook); } MediaMetadataCompat metadata = getTracksByEbook(ebook).iterator().next(); //TODO: fix header notation MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() .setMediaId(createMediaID(MEDIA_ID_EBOOK_HEADER, MEDIA_ID_BY_EBOOK, ebook)).setTitle(ebook) .setSubtitle(metadata.getString(MediaMetadataCompat.METADATA_KEY_WRITER)) //TODO: Fix Album art .setIconUri(Uri.parse(metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI))) //TODO: fix default image // .setIconBitmap(BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_books)) .build(); return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); } //endregion; private MediaBrowserCompat.MediaItem createRefreshItem(String mediaId, Resources resources) { MediaDescriptionCompat description = new MediaDescriptionCompat.Builder().setMediaId(mediaId) .setTitle("jra-prbl").setSubtitle("") //TODO: Fix resource loading //Uri.parse(metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI)) .setIconUri(BitmapHelper.convertDrawabletoUri(R.drawable.ic_navigate_books)).build(); return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); } private MediaBrowserCompat.MediaItem createTrackItem(MediaMetadataCompat metadata) { // Since mediaMetadata fields are immutable, we need to create a copy, so we // can set a hierarchy-aware mediaID. We will need to know the media hierarchy // when we get a onPlayFromMusicID call, so we can create the proper queue based // on where the music was selected from (by artist, by genre, random, etc) String ebook = metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM); String hierarchyAwareMediaID = MediaIDHelper.createMediaID(metadata.getDescription().getMediaId(), MEDIA_ID_BY_EBOOK, ebook); MediaMetadataCompat copy = new MediaMetadataCompat.Builder(metadata) .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, hierarchyAwareMediaID).build(); return new MediaBrowserCompat.MediaItem(copy.getDescription(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); } }