154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez/* 254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Copyright 2018 The Android Open Source Project 354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Licensed under the Apache License, Version 2.0 (the "License"); 554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * you may not use this file except in compliance with the License. 654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * You may obtain a copy of the License at 754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * http://www.apache.org/licenses/LICENSE-2.0 954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 1054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Unless required by applicable law or agreed to in writing, software 1154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * distributed under the License is distributed on an "AS IS" BASIS, 1254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * See the License for the specific language governing permissions and 1454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * limitations under the License. 1554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 1654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 1754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezpackage com.android.car.media.browse; 1854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 1954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.content.Context; 2054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.media.browse.MediaBrowser; 2154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.os.Bundle; 2254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.support.annotation.NonNull; 2354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.support.annotation.Nullable; 2454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.support.v7.util.DiffUtil; 2554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.support.v7.widget.GridLayoutManager; 2654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.support.v7.widget.RecyclerView; 2754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.util.Log; 2854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.view.LayoutInflater; 2954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.view.View; 3054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport android.view.ViewGroup; 3154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 3254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport com.android.car.media.common.MediaItemMetadata; 33fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perezimport com.android.car.media.common.MediaSource; 3454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 3554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport java.util.ArrayList; 3654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport java.util.Collection; 3754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport java.util.LinkedHashMap; 3854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport java.util.List; 3954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport java.util.Objects; 4054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport java.util.function.Consumer; 4154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perezimport java.util.stream.Collectors; 4254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 43a51d23d9109f44187200495fdb4523d47255b14aRoberto Perezimport androidx.car.widget.PagedListView; 44a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez 4554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez/** 4654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * A {@link RecyclerView.Adapter} that can be used to display a single level of a 4754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * {@link android.service.media.MediaBrowserService} media tree into a 4854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * {@link androidx.car.widget.PagedListView} or any other {@link RecyclerView}. 4954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 5054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * <p>This adapter assumes that the attached {@link RecyclerView} uses a {@link GridLayoutManager}, 5154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * as it can use both grid and list elements to produce the desired representation. 5254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 5354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * <p> The actual strategy to group and expand media items has to be supplied by providing an 5454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * instance of {@link ContentForwardStrategy}. 5554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 5654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * <p> The adapter will only start updating once {@link #start()} is invoked. At this point, the 5754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * provided {@link MediaBrowser} must be already in connected state. 5854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 5954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * <p>Resources and asynchronous data loading must be released by callign {@link #stop()}. 6054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 6154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * <p>No views will be actually updated until {@link #update()} is invoked (normally as a result of 6254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * the {@link Observer#onDirty()} event. This way, the consumer of this adapter has the opportunity 6354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * to decide whether updates should be displayd immediately, or if they should be delayed to 6454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * prevent flickering. 6554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 6654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * <p>Consumers of this adapter should use {@link #registerObserver(Observer)} to receive updates. 6754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 68a51d23d9109f44187200495fdb4523d47255b14aRoberto Perezpublic class BrowseAdapter extends RecyclerView.Adapter<BrowseViewHolder> implements 69a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez PagedListView.DividerVisibilityManager { 70450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez private static final String TAG = "BrowseAdapter"; 7154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @NonNull 7254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private final Context mContext; 73fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez private final MediaSource mMediaSource; 7454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private final ContentForwardStrategy mCFBStrategy; 75d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez private MediaItemMetadata mParentMediaItem; 7654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private LinkedHashMap<String, MediaItemState> mItemStates = new LinkedHashMap<>(); 7754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private List<BrowseViewData> mViewData = new ArrayList<>(); 7854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private String mParentMediaItemId; 7954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private List<Observer> mObservers = new ArrayList<>(); 8054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private List<MediaItemMetadata> mQueue; 8154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private CharSequence mQueueTitle; 8254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private int mMaxSpanSize = 1; 83450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez private State mState = State.IDLE; 84450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez 85450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez /** 86450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez * Possible states of the adapter 87450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez */ 88450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez public enum State { 89450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez /** Loading of this item hasn't started yet */ 90450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez IDLE, 91450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez /** There is pending information before this item can be displayed */ 92450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez LOADING, 93450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez /** It was not possible to load metadata for this item */ 94450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez ERROR, 95450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez /** Metadata for this items has been correctly loaded */ 96450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez LOADED 97450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez } 9854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 9954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 10054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * An {@link BrowseAdapter} observer. 10154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 102d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez public static abstract class Observer { 10354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 10454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Callback invoked anytime there is more information to be displayed, or if there is a 10554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * change in the overall state of the adapter. 10654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 107d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez protected void onDirty() {}; 10854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 10954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 11054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Callback invoked when a user clicks on a playable item. 11154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 112d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez protected void onPlayableItemClicked(MediaItemMetadata item) {}; 11354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 11454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 11554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Callback invoked when a user clicks on a browsable item. 11654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 117d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez protected void onBrowseableItemClicked(MediaItemMetadata item) {}; 11854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 11954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 12054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Callback invoked when a user clicks on a the "more items" button on a section. 12154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 122d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez protected void onMoreButtonClicked(MediaItemMetadata item) {}; 12354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 12454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 12554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Callback invoked when the user clicks on the title of the queue. 12654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 127d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez protected void onQueueTitleClicked() {}; 12854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 12954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 13054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Callback invoked when the user clicks on a queue item. 13154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 132d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez protected void onQueueItemClicked(MediaItemMetadata item) {}; 13354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 13454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 135fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez private MediaSource.ItemsSubscription mSubscriptionCallback = 136fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez (mediaSource, parentId, items) -> { 137fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez if (items != null) { 138fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez onItemsLoaded(parentId, items); 139fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez } else { 140fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez onLoadingError(parentId); 141fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez } 142fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez }; 14354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 14454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 14554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 14654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Represents the loading state of children of a single {@link MediaItemMetadata} in the 14754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * {@link BrowseAdapter} 14854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 14954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private class MediaItemState { 15054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 15154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * {@link com.android.car.media.common.MediaItemMetadata} whose children are being loaded 15254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 15354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez final MediaItemMetadata mItem; 15454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** Current loading state for this item */ 155450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez State mState = State.LOADING; 15654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** Playable children of the given item */ 15754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez List<MediaItemMetadata> mPlayableChildren = new ArrayList<>(); 15854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** Browsable children of the given item */ 15954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez List<MediaItemMetadata> mBrowsableChildren = new ArrayList<>(); 16054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** Whether we are subscribed to updates for this item or not */ 16154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez boolean mIsSubscribed; 16254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 163fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez MediaItemState(MediaItemMetadata item) { 164fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mItem = item; 16554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 16654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 167fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez void setChildren(List<MediaItemMetadata> children) { 16854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mPlayableChildren.clear(); 16954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mBrowsableChildren.clear(); 170fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez for (MediaItemMetadata child : children) { 17154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (child.isBrowsable()) { 17254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez // Browsable items could also be playable 173fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mBrowsableChildren.add(child); 17454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } else if (child.isPlayable()) { 175fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mPlayableChildren.add(child); 17654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 17754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 17854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 17954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 18054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 18154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 18254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Creates a {@link BrowseAdapter} that displays the children of the given media tree node. 18354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 184fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez * @param mediaSource the {@link MediaSource} to get data from. 18554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * @param parentItem the node to display children of, or NULL if the 18654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * @param strategy a {@link ContentForwardStrategy} that would determine which items would be 18754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * expanded and how. 18854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 189fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez public BrowseAdapter(Context context, @NonNull MediaSource mediaSource, 19054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Nullable MediaItemMetadata parentItem, @NonNull ContentForwardStrategy strategy) { 19154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mContext = context; 192fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource = mediaSource; 19354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mParentMediaItem = parentItem; 19454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mCFBStrategy = strategy; 19554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 19654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 19754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 19854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Initiates or resumes the data loading process and subscribes to updates. The client can use 19954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * {@link #registerObserver(Observer)} to receive updates on the progress. 20054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 20154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void start() { 202fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mParentMediaItemId = mParentMediaItem != null ? mParentMediaItem.getId() : 203fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource.getRoot(); 204fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource.subscribeChildren(mParentMediaItemId, mSubscriptionCallback); 20554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez for (MediaItemState itemState : mItemStates.values()) { 20654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez subscribe(itemState); 20754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 20854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 20954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 21054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 21154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Stops the data loading and releases any subscriptions. 21254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 21354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void stop() { 21454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (mParentMediaItemId == null) { 21554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez // Not started 21654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return; 21754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 218fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource.unsubscribeChildren(mParentMediaItemId, mSubscriptionCallback); 21954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez for (MediaItemState itemState : mItemStates.values()) { 22054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez unsubscribe(itemState); 22154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 22254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mParentMediaItemId = null; 22354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 22454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 22554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 226d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez * Replaces the media item whose children are being displayed in this adapter. The content of 227d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez * the adapter will be replaced once the children of the new item are loaded. 228d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez * 229d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez * @param parentItem new media item to expand. 230d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez */ 231d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez public void setParentMediaItemId(@Nullable MediaItemMetadata parentItem) { 232fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez String newParentMediaItemId = parentItem != null ? parentItem.getId() : 233fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource.getRoot(); 234d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez if (Objects.equals(newParentMediaItemId, mParentMediaItemId)) { 235d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez return; 236d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez } 237a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez stop(); 238d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez mParentMediaItem = parentItem; 239d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez mParentMediaItemId = newParentMediaItemId; 240fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource.subscribeChildren(mParentMediaItemId, mSubscriptionCallback); 241d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez } 242d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez 243d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez /** 24454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Sets media queue items into this adapter. 24554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 24654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void setQueue(List<MediaItemMetadata> items, CharSequence queueTitle) { 24754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mQueue = items; 24854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mQueueTitle = queueTitle; 24954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez notify(Observer::onDirty); 25054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 25154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 25254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 25354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Registers an {@link Observer} 25454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 25554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void registerObserver(Observer observer) { 25654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mObservers.add(observer); 25754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 25854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 25954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 26054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Unregisters an {@link Observer} 26154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 26254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void unregisterObserver(Observer observer) { 26354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mObservers.remove(observer); 26454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 26554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 26654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 26754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * @return the global loading state. Consumers can use this state to determine if more 26854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * information is still pending to arrive or not. This method will report 269450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez * {@link State#ERROR} only if the list of immediate children fails to load. 27054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 271450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez public State getState() { 27254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return mState; 27354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 27454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 27554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 27654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Sets the number of columns that items can take. This method only needs to be used if the 27754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * attached {@link RecyclerView} is NOT using a {@link GridLayoutManager}. This class will 27854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * automatically determine this value on {@link #onAttachedToRecyclerView(RecyclerView)} 27954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * otherwise. 28054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 28154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void setMaxSpanSize(int maxSpanSize) { 28254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mMaxSpanSize = maxSpanSize; 28354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 28454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 28554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 28654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * @return a {@link GridLayoutManager.SpanSizeLookup} that can be used to obtain the span size 28754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * of each item in this adapter. This method is only needed if the {@link RecyclerView} is NOT 28854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * using a {@link GridLayoutManager}. This class will automatically use it on\ 28954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * {@link #onAttachedToRecyclerView(RecyclerView)} otherwise. 29054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 29154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public GridLayoutManager.SpanSizeLookup getSpanSizeLookup() { 29254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return new GridLayoutManager.SpanSizeLookup() { 29354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 29454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public int getSpanSize(int position) { 29554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseItemViewType viewType = mViewData.get(position).mViewType; 29654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return viewType.getSpanSize(mMaxSpanSize); 29754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 29854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez }; 29954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 30054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 30154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 30254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Updates the {@link RecyclerView} with newly loaded information. This normally should be 30354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * invoked as a result of a {@link Observer#onDirty()} callback. 30454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * 30554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * This method is idempotent and can be used at any time (even delayed if needed). Additions, 30654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * removals and insertions would be notified to the {@link RecyclerView} so it can be 30754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * animated appropriately. 30854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 30954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void update() { 31054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez List<BrowseViewData> newItems = generateViewData(mItemStates.values()); 31154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez List<BrowseViewData> oldItems = mViewData; 31254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mViewData = newItems; 31354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez DiffUtil.DiffResult result = DiffUtil.calculateDiff(createDiffUtil(oldItems, newItems)); 31454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez result.dispatchUpdatesTo(this); 31554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 31654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 31754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private void subscribe(MediaItemState state) { 31854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (!state.mIsSubscribed && state.mItem.isBrowsable()) { 319fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource.subscribeChildren(state.mItem.getId(), mSubscriptionCallback); 32054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez state.mIsSubscribed = true; 321450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez } else { 322450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez state.mState = State.LOADED; 32354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 32454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 32554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 32654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private void unsubscribe(MediaItemState state) { 32754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (state.mIsSubscribed) { 328fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mMediaSource.unsubscribeChildren(state.mItem.getId(), mSubscriptionCallback); 32954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez state.mIsSubscribed = false; 33054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 33154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 33254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 33354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @NonNull 33454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 33554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public BrowseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 33654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez int layoutId = BrowseItemViewType.values()[viewType].getLayoutId(); 33754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez View view = LayoutInflater.from(mContext).inflate(layoutId, parent, false); 33854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return new BrowseViewHolder(view); 33954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 34054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 34154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 34254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void onBindViewHolder(@NonNull BrowseViewHolder holder, int position) { 34354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseViewData viewData = mViewData.get(position); 34454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez holder.bind(mContext, viewData); 34554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 34654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 34754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 34854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public int getItemCount() { 34954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return mViewData.size(); 35054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 35154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 35254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 35354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public int getItemViewType(int position) { 35454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return mViewData.get(position).mViewType.ordinal(); 35554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 35654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 357fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez private void onItemsLoaded(String parentId, List<MediaItemMetadata> children) { 35854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (parentId.equals(mParentMediaItemId)) { 35954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez // Direct children from the requested media item id. Update subscription list. 36054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez LinkedHashMap<String, MediaItemState> newItemStates = new LinkedHashMap<>(); 3611d3e7e02676ed495b9f99701fa4647aa524d3f1bRoberto Perez List<MediaItemState> itemsToSubscribe = new ArrayList<>(); 362fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez for (MediaItemMetadata item : children) { 363fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez MediaItemState itemState = mItemStates.get(item.getId()); 36454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (itemState != null) { 36554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez // Reuse existing section. 366fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez newItemStates.put(item.getId(), itemState); 367fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez mItemStates.remove(item.getId()); 36854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } else { 36954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez // New section, subscribe to it. 37054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemState = new MediaItemState(item); 371fc30bbfdb4221ecd18adf59501d5ca82ca631310Roberto Perez newItemStates.put(item.getId(), itemState); 3721d3e7e02676ed495b9f99701fa4647aa524d3f1bRoberto Perez itemsToSubscribe.add(itemState); 37354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 37454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 37554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez // Remove unused sections 37654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez for (MediaItemState itemState : mItemStates.values()) { 37754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez unsubscribe(itemState); 37854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 37954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mItemStates = newItemStates; 3801d3e7e02676ed495b9f99701fa4647aa524d3f1bRoberto Perez // Subscribe items once we have updated the map (updates might happen synchronously 3811d3e7e02676ed495b9f99701fa4647aa524d3f1bRoberto Perez // if data is already available). 3821d3e7e02676ed495b9f99701fa4647aa524d3f1bRoberto Perez for (MediaItemState itemState : itemsToSubscribe) { 3831d3e7e02676ed495b9f99701fa4647aa524d3f1bRoberto Perez subscribe(itemState); 3841d3e7e02676ed495b9f99701fa4647aa524d3f1bRoberto Perez } 38554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } else { 38654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez MediaItemState itemState = mItemStates.get(parentId); 38754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (itemState == null) { 38854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez Log.w(TAG, "Loaded children for a section we don't have: " + parentId); 38954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return; 39054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 39154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemState.setChildren(children); 392450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez itemState.mState = State.LOADED; 39354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 39454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez updateGlobalState(); 39554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez notify(Observer::onDirty); 39654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 39754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 39854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private void notify(Consumer<Observer> notification) { 39954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez for (Observer observer : mObservers) { 40054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez notification.accept(observer); 40154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 40254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 40354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 40454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private void onLoadingError(String parentId) { 40554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (parentId.equals(mParentMediaItemId)) { 406450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez mState = State.ERROR; 40754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } else { 40854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez MediaItemState state = mItemStates.get(parentId); 40954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (state == null) { 41054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez Log.w(TAG, "Error loading children for a section we don't have: " + parentId); 41154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return; 41254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 41354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez state.setChildren(new ArrayList<>()); 414450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez state.mState = State.ERROR; 415450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez updateGlobalState(); 41654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 41754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez notify(Observer::onDirty); 41854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 41954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 42054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private void updateGlobalState() { 42154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez for (MediaItemState state: mItemStates.values()) { 422450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez if (state.mState == State.LOADING) { 423450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez mState = State.LOADING; 42454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return; 42554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 42654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 427450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez mState = State.LOADED; 42854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 42954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 43054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private DiffUtil.Callback createDiffUtil(List<BrowseViewData> oldList, 43154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez List<BrowseViewData> newList) { 43254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return new DiffUtil.Callback() { 43354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 43454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public int getOldListSize() { 43554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return oldList.size(); 43654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 43754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 43854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 43954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public int getNewListSize() { 44054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return newList.size(); 44154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 44254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 44354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 44454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public boolean areItemsTheSame(int oldPos, int newPos) { 44554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseViewData oldItem = oldList.get(oldPos); 44654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseViewData newItem = newList.get(newPos); 44754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 44854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return Objects.equals(oldItem.mMediaItem, newItem.mMediaItem) 44954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez && Objects.equals(oldItem.mText, newItem.mText); 45054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 45154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 45254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 45354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public boolean areContentsTheSame(int oldPos, int newPos) { 45454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseViewData oldItem = oldList.get(oldPos); 45554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseViewData newItem = newList.get(newPos); 45654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 45754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return oldItem.equals(newItem); 45854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 45954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez }; 46054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 46154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 46254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez @Override 46354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez public void onAttachedToRecyclerView(RecyclerView recyclerView) { 46454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (recyclerView.getLayoutManager() instanceof GridLayoutManager) { 46554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez GridLayoutManager manager = (GridLayoutManager) recyclerView.getLayoutManager(); 46654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mMaxSpanSize = manager.getSpanCount(); 46754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez manager.setSpanSizeLookup(getSpanSizeLookup()); 46854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 46954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 47054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 47154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private class ItemsBuilder { 47254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private List<BrowseViewData> result = new ArrayList<>(); 47354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 474450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez void addItem(MediaItemMetadata item, State state, 47554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseItemViewType viewType, Consumer<Observer> notification) { 476d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez View.OnClickListener listener = notification != null ? 477d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez view -> BrowseAdapter.this.notify(notification) : 478d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez null; 479d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez result.add(new BrowseViewData(item, viewType, state, listener)); 48054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 48154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 48254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez void addItems(List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxRows) { 48354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez int spanSize = viewType.getSpanSize(mMaxSpanSize); 48454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez int maxChildren = maxRows * (mMaxSpanSize / spanSize); 48554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez result.addAll(items.stream() 48654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez .limit(maxChildren) 48754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez .map(item -> { 48854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez Consumer<Observer> notification = item.getQueueId() != null 48954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez ? observer -> observer.onQueueItemClicked(item) 49054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez : item.isBrowsable() 49154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez ? observer -> observer.onBrowseableItemClicked(item) 49254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez : observer -> observer.onPlayableItemClicked(item); 49354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return new BrowseViewData(item, viewType, null, view -> 49454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez BrowseAdapter.this.notify(notification)); 49554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez }) 49654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez .collect(Collectors.toList())); 49754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 49854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 49954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez void addTitle(CharSequence title, Consumer<Observer> notification) { 50054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez result.add(new BrowseViewData(title, BrowseItemViewType.HEADER, 50154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez view -> BrowseAdapter.this.notify(notification))); 50254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 50354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 50454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 505450c73a79e566f1c4a8758648525b457005e9f9aRoberto Perez void addBrowseBlock(MediaItemMetadata header, State state, 50654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez List<MediaItemMetadata> items, BrowseItemViewType viewType, int maxChildren, 50754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez boolean showHeader, boolean showMoreFooter) { 50854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (showHeader) { 50954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez addItem(header, state, BrowseItemViewType.HEADER, null); 51054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 51154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez addItems(items, viewType, maxChildren); 51254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (showMoreFooter) { 51354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez addItem(header, null, BrowseItemViewType.MORE_FOOTER, 51454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez observer -> observer.onMoreButtonClicked(header)); 51554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 51654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 51754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 51854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez List<BrowseViewData> build() { 51954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return result; 52054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 52154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 52254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 52354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez /** 52454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * Flatten the given collection of item states into a list of {@link BrowseViewData}s. To avoid 52554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * flickering, the flatting will stop at the first "loading" section, avoiding unnecessary 52654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez * insertion animations during the initial data load. 52754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez */ 52854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez private List<BrowseViewData> generateViewData(Collection<MediaItemState> itemStates) { 52954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez ItemsBuilder itemsBuilder = new ItemsBuilder(); 53054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 531a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez if (Log.isLoggable(TAG, Log.VERBOSE)) { 532a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez Log.v(TAG, "Generating browse view from:"); 533a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez for (MediaItemState item : itemStates) { 534a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez Log.v(TAG, String.format("[%s%s] '%s' (%s)", 535a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez item.mItem.isBrowsable() ? "B" : " ", 536a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez item.mItem.isPlayable() ? "P" : " ", 537a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez item.mItem.getTitle(), 538a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez item.mItem.getId())); 539a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez List<MediaItemMetadata> items = new ArrayList<>(); 540a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez items.addAll(item.mBrowsableChildren); 541a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez items.addAll(item.mPlayableChildren); 542a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez for (MediaItemMetadata child : items) { 543a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez Log.v(TAG, String.format(" [%s%s] '%s' (%s)", 544a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez child.isBrowsable() ? "B" : " ", 545a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez child.isPlayable() ? "P" : " ", 546a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez child.getTitle(), 547a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez child.getId())); 548a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez } 549a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez } 550a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez } 551a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez 55254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (mQueue != null && !mQueue.isEmpty() && mCFBStrategy.getMaxQueueRows() > 0 55354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez && mCFBStrategy.getQueueViewType() != null) { 55454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (mQueueTitle != null) { 55554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemsBuilder.addTitle(mQueueTitle, Observer::onQueueTitleClicked); 55654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 55754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemsBuilder.addItems(mQueue, mCFBStrategy.getQueueViewType(), 55854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mCFBStrategy.getMaxQueueRows()); 55954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 560a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez 561a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez boolean containsBrowsableItems = false; 562a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez boolean containsPlayableItems = false; 563a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez for (MediaItemState itemState : itemStates) { 564a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez containsBrowsableItems |= itemState.mItem.isBrowsable(); 565a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez containsPlayableItems |= itemState.mItem.isPlayable(); 566a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez } 567a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez 56854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez for (MediaItemState itemState : itemStates) { 56954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez MediaItemMetadata item = itemState.mItem; 570a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez if (containsPlayableItems && containsBrowsableItems) { 571a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez // If we have a mix of browsable and playable items: show them all in a list 572a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez itemsBuilder.addItem(item, itemState.mState, 573a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez BrowseItemViewType.PANEL_ITEM, 574a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez item.isBrowsable() 575a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez ? observer -> observer.onBrowseableItemClicked(item) 576a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez : observer -> observer.onPlayableItemClicked(item)); 577a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez } else if (itemState.mItem.isBrowsable()) { 578a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez // If we only have browsable items, check whether we should expand them or not. 57954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez if (!itemState.mBrowsableChildren.isEmpty() 58054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez && !itemState.mPlayableChildren.isEmpty() 58154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez || !mCFBStrategy.shouldBeExpanded(item)) { 58254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemsBuilder.addItem(item, itemState.mState, 58354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mCFBStrategy.getBrowsableViewType(mParentMediaItem), null); 58454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } else if (!itemState.mPlayableChildren.isEmpty()) { 58554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemsBuilder.addBrowseBlock(item, 58654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemState.mState, 58754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemState.mPlayableChildren, 58854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mCFBStrategy.getPlayableViewType(item), 58954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mCFBStrategy.getMaxRows(item, mCFBStrategy.getPlayableViewType(item)), 590a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez mCFBStrategy.includeHeader(item), 591a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez mCFBStrategy.showMoreButton(item)); 59254a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } else if (!itemState.mBrowsableChildren.isEmpty()) { 59354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemsBuilder.addBrowseBlock(item, 59454a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemState.mState, 59554a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemState.mBrowsableChildren, 59654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mCFBStrategy.getBrowsableViewType(item), 59754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez mCFBStrategy.getMaxRows(item, mCFBStrategy.getBrowsableViewType(item)), 598a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez mCFBStrategy.includeHeader(item), 599a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez mCFBStrategy.showMoreButton(item)); 60054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 60154a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } else if (item.isPlayable()) { 602a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez // If we only have playable items: show them as so. 60354a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez itemsBuilder.addItem(item, itemState.mState, 604d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez mCFBStrategy.getPlayableViewType(mParentMediaItem), 605d1f0ec11284f6e6ed05d6a6fb7e2e086cc4a9837Roberto Perez observer -> observer.onPlayableItemClicked(item)); 60654a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 60754a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 60854a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez 60954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez return itemsBuilder.build(); 61054a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez } 611a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez 612a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez @Override 613a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez public boolean shouldHideDivider(int position) { 614a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez return position >= mViewData.size() - 1 615a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez || position < 0 616a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez || mViewData.get(position).mViewType != BrowseItemViewType.PANEL_ITEM 617a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez || mViewData.get(position + 1).mViewType != BrowseItemViewType.PANEL_ITEM; 618a51d23d9109f44187200495fdb4523d47255b14aRoberto Perez } 61954a9df95716c26d7ae3a226ad506061fb9ed6e6cRoberto Perez} 620