1 /*
2  * Copyright 2018 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 
17 package com.android.pump.fragment;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.net.Uri;
22 import android.os.Bundle;
23 import android.text.TextUtils;
24 import android.util.DisplayMetrics;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.ImageView;
29 import android.widget.TextView;
30 
31 import androidx.annotation.NonNull;
32 import androidx.annotation.Nullable;
33 import androidx.annotation.UiThread;
34 import androidx.core.view.ViewCompat;
35 import androidx.fragment.app.Fragment;
36 import androidx.recyclerview.widget.GridLayoutManager;
37 import androidx.recyclerview.widget.RecyclerView;
38 
39 import com.android.pump.R;
40 import com.android.pump.activity.PlaylistDetailsActivity;
41 import com.android.pump.db.Album;
42 import com.android.pump.db.Artist;
43 import com.android.pump.db.Audio;
44 import com.android.pump.db.MediaDb;
45 import com.android.pump.db.Playlist;
46 import com.android.pump.util.Globals;
47 
48 import java.util.HashSet;
49 import java.util.Iterator;
50 import java.util.List;
51 import java.util.Set;
52 
53 @UiThread
54 public class PlaylistFragment extends Fragment {
55     private RecyclerView mRecyclerView;
56 
newInstance()57     public static @NonNull Fragment newInstance() {
58         return new PlaylistFragment();
59     }
60 
61     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)62     public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
63             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
64         View view = inflater.inflate(R.layout.fragment_playlist, container, false);
65         mRecyclerView = view.findViewById(R.id.fragment_playlist_recycler_view);
66         mRecyclerView.setHasFixedSize(true);
67         mRecyclerView.setAdapter(new PlaylistAdapter(requireContext()));
68         mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
69 
70         GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
71         gridLayoutManager.setSpanSizeLookup(
72                 new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
73 
74         // TODO(b/123707260) Enable view caching
75         //mRecyclerView.setItemViewCacheSize(0);
76         //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
77         return view;
78     }
79 
80     private static class PlaylistAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
81             implements MediaDb.UpdateCallback {
82         private final MediaDb mMediaDb;
83         private final List<Playlist> mPlaylists; // TODO(b/123710968) Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
84 
PlaylistAdapter(@onNull Context context)85         private PlaylistAdapter(@NonNull Context context) {
86             setHasStableIds(true);
87             mMediaDb = Globals.getMediaDb(context);
88             mPlaylists = mMediaDb.getPlaylists();
89         }
90 
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)91         public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
92             mMediaDb.addPlaylistUpdateCallback(this);
93         }
94 
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)95         public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
96             mMediaDb.removePlaylistUpdateCallback(this);
97         }
98 
99         @Override
onCreateViewHolder( @onNull ViewGroup parent, int viewType)100         public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
101                 @NonNull ViewGroup parent, int viewType) {
102             if (viewType == R.layout.header) {
103                 return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
104                         .inflate(viewType, parent, false)) { };
105             } else {
106                 return new PlaylistViewHolder(LayoutInflater.from(parent.getContext())
107                         .inflate(viewType, parent, false));
108             }
109         }
110 
111         @Override
onBindViewHolder(@onNull RecyclerView.ViewHolder holder, int position)112         public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
113             if (position == 0) {
114                 // TODO Handle header view
115             } else {
116                 Playlist playlist = mPlaylists.get(position - 1);
117                 mMediaDb.loadData(playlist); // TODO Where should we call this? In bind()?
118                 ((PlaylistViewHolder) holder).bind(playlist);
119             }
120         }
121 
122         @Override
getItemCount()123         public int getItemCount() {
124             return mPlaylists.size() + 1;
125         }
126 
127         @Override
getItemId(int position)128         public long getItemId(int position) {
129             return position == 0 ? -1 : mPlaylists.get(position - 1).getId();
130         }
131 
132         @Override
getItemViewType(int position)133         public int getItemViewType(int position) {
134             return position == 0 ? R.layout.header : R.layout.playlist;
135         }
136 
137         @Override
onItemsInserted(int index, int count)138         public void onItemsInserted(int index, int count) {
139             notifyItemRangeInserted(index + 1, count);
140         }
141 
142         @Override
onItemsUpdated(int index, int count)143         public void onItemsUpdated(int index, int count) {
144             notifyItemRangeChanged(index + 1, count);
145         }
146 
147         @Override
onItemsRemoved(int index, int count)148         public void onItemsRemoved(int index, int count) {
149             notifyItemRangeRemoved(index + 1, count);
150         }
151     }
152 
153     private static class PlaylistViewHolder extends RecyclerView.ViewHolder {
PlaylistViewHolder(@onNull View itemView)154         private PlaylistViewHolder(@NonNull View itemView) {
155             super(itemView);
156         }
157 
bind(@onNull Playlist playlist)158         private void bind(@NonNull Playlist playlist) {
159             ImageView image0View = itemView.findViewById(R.id.playlist_image_0);
160             ImageView image1View = itemView.findViewById(R.id.playlist_image_1);
161             ImageView image2View = itemView.findViewById(R.id.playlist_image_2);
162             ImageView image3View = itemView.findViewById(R.id.playlist_image_3);
163             TextView titleView = itemView.findViewById(R.id.playlist_title);
164             TextView artistsView = itemView.findViewById(R.id.playlist_artists);
165 
166             // TODO Find a better way to handle 2x2 art
167             Set<Uri> albumArtUris = new HashSet<>();
168             Set<String> artistNames = new HashSet<>();
169             List<Audio> audios = playlist.getAudios();
170             for (Audio audio : audios) {
171                 Album album = audio.getAlbum();
172                 if (album != null && album.getAlbumArtUri() != null) {
173                     albumArtUris.add(album.getAlbumArtUri());
174                 }
175 
176                 Artist artist = audio.getArtist();
177                 if (artist != null && artist.getName() != null) {
178                     artistNames.add(artist.getName());
179                 }
180             }
181 
182             int numAlbumArt = albumArtUris.size();
183             if (numAlbumArt == 0) {
184                 image0View.setImageURI(null);
185                 image1View.setImageURI(null);
186                 image2View.setImageURI(null);
187                 image3View.setImageURI(null);
188                 image0View.setVisibility(View.VISIBLE);
189                 image1View.setVisibility(View.GONE);
190                 image2View.setVisibility(View.GONE);
191                 image3View.setVisibility(View.GONE);
192             } else if (numAlbumArt < 4) {
193                 Iterator<Uri> iterator = albumArtUris.iterator();
194                 image0View.setImageURI(iterator.next());
195                 image1View.setImageURI(null);
196                 image2View.setImageURI(null);
197                 image3View.setImageURI(null);
198                 image0View.setVisibility(View.VISIBLE);
199                 image1View.setVisibility(View.GONE);
200                 image2View.setVisibility(View.GONE);
201                 image3View.setVisibility(View.GONE);
202             } else {
203                 Iterator<Uri> iterator = albumArtUris.iterator();
204                 image0View.setImageURI(iterator.next());
205                 image1View.setImageURI(iterator.next());
206                 image2View.setImageURI(iterator.next());
207                 image3View.setImageURI(iterator.next());
208                 image0View.setVisibility(View.VISIBLE);
209                 image1View.setVisibility(View.VISIBLE);
210                 image2View.setVisibility(View.VISIBLE);
211                 image3View.setVisibility(View.VISIBLE);
212             }
213             titleView.setText(playlist.getName());
214             // TODO Fix comma separation for i18n/l11n
215             artistsView.setText(artistNames.isEmpty() ? null : TextUtils.join(", ", artistNames));
216 
217             itemView.setOnClickListener((view) ->
218                     PlaylistDetailsActivity.start(view.getContext(), playlist));
219         }
220     }
221 
222     private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
223         private final int mXOffset;
224         private final int mYOffset;
225 
SpaceItemDecoration(int xOffset, int yOffset)226         private SpaceItemDecoration(int xOffset, int yOffset) {
227             mXOffset = xOffset;
228             mYOffset = yOffset;
229         }
230 
231         @Override
getItemOffsets(@onNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)232         public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
233             DisplayMetrics displayMetrics = new DisplayMetrics();
234             ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
235             outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
236             if (parent.getChildAdapterPosition(view) > 0) {
237                 outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
238             }
239         }
240     }
241 
242     private static class HeaderSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
243         private final int mSpanCount;
244 
HeaderSpanSizeLookup(int spanCount)245         private HeaderSpanSizeLookup(int spanCount) {
246             mSpanCount = spanCount;
247         }
248 
249         @Override
getSpanSize(int position)250         public int getSpanSize(int position) {
251             return position == 0 ? mSpanCount : 1;
252         }
253 
254         @Override
getSpanIndex(int position, int spanCount)255         public int getSpanIndex(int position, int spanCount) {
256             return position == 0 ? 0 : (position - 1) % spanCount;
257         }
258 
259         @Override
getSpanGroupIndex(int adapterPosition, int spanCount)260         public int getSpanGroupIndex(int adapterPosition, int spanCount) {
261             return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
262         }
263     }
264 }
265