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