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.os.Bundle;
22 import android.util.DisplayMetrics;
23 import android.view.LayoutInflater;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.widget.ImageView;
27 import android.widget.TextView;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.annotation.UiThread;
32 import androidx.core.view.ViewCompat;
33 import androidx.fragment.app.Fragment;
34 import androidx.recyclerview.widget.GridLayoutManager;
35 import androidx.recyclerview.widget.RecyclerView;
36 
37 import com.android.pump.R;
38 import com.android.pump.activity.AlbumDetailsActivity;
39 import com.android.pump.db.Album;
40 import com.android.pump.db.Artist;
41 import com.android.pump.db.MediaDb;
42 import com.android.pump.util.Globals;
43 
44 import java.util.List;
45 
46 @UiThread
47 public class AlbumFragment extends Fragment {
48     private RecyclerView mRecyclerView;
49 
newInstance()50     public static @NonNull Fragment newInstance() {
51         return new AlbumFragment();
52     }
53 
54     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)55     public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
56             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
57         View view = inflater.inflate(R.layout.fragment_album, container, false);
58         mRecyclerView = view.findViewById(R.id.fragment_album_recycler_view);
59         mRecyclerView.setHasFixedSize(true);
60         mRecyclerView.setAdapter(new AlbumAdapter(requireContext()));
61         mRecyclerView.addItemDecoration(new SpaceItemDecoration(4, 16));
62 
63         GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
64         gridLayoutManager.setSpanSizeLookup(
65                 new HeaderSpanSizeLookup(gridLayoutManager.getSpanCount()));
66 
67         // TODO(b/123707260) Enable view caching
68         //mRecyclerView.setItemViewCacheSize(0);
69         //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
70         return view;
71     }
72 
73     private static class AlbumAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
74             implements MediaDb.UpdateCallback {
75         private final MediaDb mMediaDb;
76         private final List<Album> mAlbums; // TODO(b/123710968) Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
77 
AlbumAdapter(@onNull Context context)78         private AlbumAdapter(@NonNull Context context) {
79             setHasStableIds(true);
80             mMediaDb = Globals.getMediaDb(context);
81             mAlbums = mMediaDb.getAlbums();
82         }
83 
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)84         public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
85             mMediaDb.addAlbumUpdateCallback(this);
86         }
87 
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)88         public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
89             mMediaDb.removeAlbumUpdateCallback(this);
90         }
91 
92         @Override
onCreateViewHolder( @onNull ViewGroup parent, int viewType)93         public @NonNull RecyclerView.ViewHolder onCreateViewHolder(
94                 @NonNull ViewGroup parent, int viewType) {
95             if (viewType == R.layout.header) {
96                 return new RecyclerView.ViewHolder(LayoutInflater.from(parent.getContext())
97                         .inflate(viewType, parent, false)) { };
98             } else {
99                 return new AlbumViewHolder(LayoutInflater.from(parent.getContext())
100                         .inflate(viewType, parent, false));
101             }
102         }
103 
104         @Override
onBindViewHolder(@onNull RecyclerView.ViewHolder holder, int position)105         public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
106             if (position == 0) {
107                 // TODO Handle header view
108             } else {
109                 Album album = mAlbums.get(position - 1);
110                 mMediaDb.loadData(album); // TODO Where should we call this? In bind()?
111                 ((AlbumViewHolder) holder).bind(album);
112             }
113         }
114 
115         @Override
getItemCount()116         public int getItemCount() {
117             return mAlbums.size() + 1;
118         }
119 
120         @Override
getItemId(int position)121         public long getItemId(int position) {
122             return position == 0 ? -1 : mAlbums.get(position - 1).getId();
123         }
124 
125         @Override
getItemViewType(int position)126         public int getItemViewType(int position) {
127             return position == 0 ? R.layout.header : R.layout.album;
128         }
129 
130         @Override
onItemsInserted(int index, int count)131         public void onItemsInserted(int index, int count) {
132             notifyItemRangeInserted(index + 1, count);
133         }
134 
135         @Override
onItemsUpdated(int index, int count)136         public void onItemsUpdated(int index, int count) {
137             notifyItemRangeChanged(index + 1, count);
138         }
139 
140         @Override
onItemsRemoved(int index, int count)141         public void onItemsRemoved(int index, int count) {
142             notifyItemRangeRemoved(index + 1, count);
143         }
144     }
145 
146     private static class AlbumViewHolder extends RecyclerView.ViewHolder {
AlbumViewHolder(@onNull View itemView)147         private AlbumViewHolder(@NonNull View itemView) {
148             super(itemView);
149         }
150 
bind(@onNull Album album)151         private void bind(@NonNull Album album) {
152             ImageView imageView = itemView.findViewById(R.id.album_image);
153             TextView titleView = itemView.findViewById(R.id.album_title);
154             TextView artistView = itemView.findViewById(R.id.album_artist);
155 
156             imageView.setImageURI(album.getAlbumArtUri());
157             titleView.setText(album.getTitle());
158             Artist artist = album.getArtist();
159             artistView.setText(artist == null ? null : artist.getName());
160 
161             itemView.setOnClickListener((view) ->
162                     AlbumDetailsActivity.start(view.getContext(), album));
163         }
164     }
165 
166     private static class SpaceItemDecoration extends RecyclerView.ItemDecoration {
167         private final int mXOffset;
168         private final int mYOffset;
169 
SpaceItemDecoration(int xOffset, int yOffset)170         private SpaceItemDecoration(int xOffset, int yOffset) {
171             mXOffset = xOffset;
172             mYOffset = yOffset;
173         }
174 
175         @Override
getItemOffsets(@onNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state)176         public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
177             DisplayMetrics displayMetrics = new DisplayMetrics();
178             ViewCompat.getDisplay(parent).getMetrics(displayMetrics);
179             outRect.left = outRect.right = (int) Math.ceil(mXOffset * displayMetrics.density);
180             if (parent.getChildAdapterPosition(view) > 0) {
181                 outRect.bottom = (int) Math.ceil(mYOffset * displayMetrics.density);
182             }
183         }
184     }
185 
186     private static class HeaderSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
187         private final int mSpanCount;
188 
HeaderSpanSizeLookup(int spanCount)189         private HeaderSpanSizeLookup(int spanCount) {
190             mSpanCount = spanCount;
191         }
192 
193         @Override
getSpanSize(int position)194         public int getSpanSize(int position) {
195             return position == 0 ? mSpanCount : 1;
196         }
197 
198         @Override
getSpanIndex(int position, int spanCount)199         public int getSpanIndex(int position, int spanCount) {
200             return position == 0 ? 0 : (position - 1) % spanCount;
201         }
202 
203         @Override
getSpanGroupIndex(int adapterPosition, int spanCount)204         public int getSpanGroupIndex(int adapterPosition, int spanCount) {
205             return adapterPosition == 0 ? 0 : ((adapterPosition - 1) / spanCount) + 1;
206         }
207     }
208 }
209