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.Bitmap;
21 import android.net.Uri;
22 import android.os.Bundle;
23 import android.util.SparseIntArray;
24 import android.view.LayoutInflater;
25 import android.view.View;
26 import android.view.ViewGroup;
27 
28 import androidx.annotation.NonNull;
29 import androidx.annotation.Nullable;
30 import androidx.annotation.UiThread;
31 import androidx.fragment.app.Fragment;
32 import androidx.recyclerview.widget.GridLayoutManager;
33 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup;
34 import androidx.recyclerview.widget.RecyclerView;
35 import androidx.recyclerview.widget.RecyclerView.Adapter;
36 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
37 
38 import com.android.pump.R;
39 import com.android.pump.activity.OtherDetailsActivity;
40 import com.android.pump.db.MediaDb;
41 import com.android.pump.db.Other;
42 import com.android.pump.util.Globals;
43 import com.android.pump.util.ImageLoader;
44 import com.android.pump.util.Orientation;
45 import com.android.pump.widget.UriImageView;
46 
47 import java.util.List;
48 
49 @UiThread
50 public class OtherFragment extends Fragment {
51     private static final int SPAN_COUNT = 6;
52 
53     private RecyclerView mRecyclerView;
54 
newInstance()55     public static @NonNull Fragment newInstance() {
56         return new OtherFragment();
57     }
58 
59     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)60     public @NonNull View onCreateView(@NonNull LayoutInflater inflater,
61             @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
62         View view = inflater.inflate(R.layout.fragment_other, container, false);
63         mRecyclerView = view.findViewById(R.id.fragment_other_recycler_view);
64         mRecyclerView.setHasFixedSize(true);
65 
66         OtherAdapter otherAdapter = new OtherAdapter(requireContext());
67         mRecyclerView.setAdapter(otherAdapter);
68 
69         GridLayoutManager gridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
70         gridLayoutManager.setSpanSizeLookup(otherAdapter.getSpanSizeLookup());
71         if (gridLayoutManager.getSpanCount() != SPAN_COUNT) {
72             throw new IllegalArgumentException("Expected a span count of " + SPAN_COUNT +
73                     ", found a span count of " + gridLayoutManager.getSpanCount() + ".");
74         }
75 
76         mRecyclerView.setItemAnimator(null); // TODO Re-enable add/remove animations
77 
78         // TODO(b/123707260) Enable view caching
79         //mRecyclerView.setItemViewCacheSize(0);
80         //mRecyclerView.setRecycledViewPool(Globals.getRecycledViewPool(requireContext()));
81         return view;
82     }
83 
84     private static class OtherAdapter extends Adapter<ViewHolder>
85             implements MediaDb.UpdateCallback, ImageLoader.Callback {
86         private final ImageLoader mImageLoader;
87         private final MediaDb mMediaDb;
88         private final List<Other> mOthers; // TODO(b/123710968) Use android.support.v7.util.SortedList/android.support.v7.widget.util.SortedListAdapterCallback instead
89         private final SparseIntArray mSpanSize = new SparseIntArray();
90 
OtherAdapter(@onNull Context context)91         private OtherAdapter(@NonNull Context context) {
92             setHasStableIds(true);
93 
94             mImageLoader = Globals.getImageLoader(context);
95             mMediaDb = Globals.getMediaDb(context);
96             mOthers = mMediaDb.getOthers();
97             recalculateSpans();
98         }
99 
onAttachedToRecyclerView(@onNull RecyclerView recyclerView)100         public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
101             mMediaDb.addOtherUpdateCallback(this);
102             mImageLoader.addCallback(this);
103         }
104 
onDetachedFromRecyclerView(@onNull RecyclerView recyclerView)105         public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
106             mMediaDb.removeOtherUpdateCallback(this);
107             mImageLoader.removeCallback(this);
108         }
109 
110         @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)111         public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
112             if (viewType == R.layout.header) {
113                 return new ViewHolder(LayoutInflater.from(parent.getContext())
114                         .inflate(viewType, parent, false)) { };
115             } else {
116                 return new OtherViewHolder(LayoutInflater.from(parent.getContext())
117                         .inflate(viewType, parent, false));
118             }
119         }
120 
121         @Override
onBindViewHolder(@onNull ViewHolder holder, int position)122         public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
123             if (position == 0) {
124                 // TODO Handle header view
125             } else {
126                 Other other = mOthers.get(position - 1);
127                 mMediaDb.loadData(other); // TODO Where should we call this? In bind()?
128                 ((OtherViewHolder) holder).bind(other);
129             }
130         }
131 
132         @Override
getItemCount()133         public int getItemCount() {
134             return mOthers.size() + 1;
135         }
136 
137         @Override
getItemId(int position)138         public long getItemId(int position) {
139             return position == 0 ? -1 : mOthers.get(position - 1).getId();
140         }
141 
142         @Override
getItemViewType(int position)143         public int getItemViewType(int position) {
144             return position == 0 ? R.layout.header : R.layout.other;
145         }
146 
147         @Override
onImageLoaded(@onNull Uri uri, @Nullable Bitmap bitmap)148         public void onImageLoaded(@NonNull Uri uri, @Nullable Bitmap bitmap) {
149             // TODO Optimize this (only update necessary parts -- not the whole world)
150             recalculateSpans();
151             notifyItemRangeChanged(1, mOthers.size());
152         }
153 
154         @Override
onItemsInserted(int index, int count)155         public void onItemsInserted(int index, int count) {
156             notifyItemRangeInserted(index + 1, count);
157         }
158 
159         @Override
onItemsUpdated(int index, int count)160         public void onItemsUpdated(int index, int count) {
161             notifyItemRangeChanged(index + 1, count);
162         }
163 
164         @Override
onItemsRemoved(int index, int count)165         public void onItemsRemoved(int index, int count) {
166             notifyItemRangeRemoved(index + 1, count);
167         }
168 
recalculateSpans()169         private void recalculateSpans() {
170             // TODO Recalculate when an image is loaded
171             // TODO Recalculate when notifyXxx is called
172             // TODO Optimize
173             mSpanSize.clear();
174             int current = 0;
175             while (current < mOthers.size()) {
176                 int orientation = getOrientation(current);
177                 if (orientation == Orientation.LANDSCAPE) {
178                     orientation = getOrientation(current + 1);
179                     if (orientation == Orientation.LANDSCAPE) {
180                         // L L
181                         mSpanSize.append(current++, SPAN_COUNT / 2);
182                         mSpanSize.append(current++, SPAN_COUNT / 2);
183                     } else if (orientation == Orientation.PORTRAIT) {
184                         // L P
185                         mSpanSize.append(current++, SPAN_COUNT * 2 / 3);
186                         mSpanSize.append(current++, SPAN_COUNT * 1 / 3);
187                     } else {
188                         // L
189                         mSpanSize.append(current++, SPAN_COUNT);
190                     }
191                 } else if (orientation == Orientation.PORTRAIT) {
192                     orientation = getOrientation(current + 1);
193                     if (orientation == Orientation.LANDSCAPE) {
194                         // P L
195                         mSpanSize.append(current++, SPAN_COUNT * 1 / 3);
196                         mSpanSize.append(current++, SPAN_COUNT * 2 / 3);
197                     } else if (orientation == Orientation.PORTRAIT &&
198                             getOrientation(current + 2) == Orientation.PORTRAIT) {
199                         // P P P
200                         mSpanSize.append(current++, SPAN_COUNT / 3);
201                         mSpanSize.append(current++, SPAN_COUNT / 3);
202                         mSpanSize.append(current++, SPAN_COUNT / 3);
203                     } else {
204                         // P
205                         mSpanSize.append(current++, SPAN_COUNT);
206                     }
207                 } else {
208                     // unknown
209                     mSpanSize.append(current++, SPAN_COUNT);
210                 }
211             }
212         }
213 
getOrientation(int index)214         private @Orientation int getOrientation(int index) {
215             Uri thumbUri = index >= mOthers.size() ? null : mOthers.get(index).getThumbnailUri();
216             if (thumbUri == null) {
217                 return Orientation.UNKNOWN;
218             }
219             return mImageLoader.getOrientation(thumbUri);
220         }
221 
getSpanSizeLookup()222         private @NonNull SpanSizeLookup getSpanSizeLookup() {
223             return new SpanSizeLookup() {
224                 @Override
225                 public int getSpanSize(int position) {
226                     return position == 0 ? SPAN_COUNT : mSpanSize.get(position - 1);
227                 }
228 
229                 @Override
230                 public int getSpanIndex(int position, int spanCount) {
231                     // TODO Optimize
232                     return super.getSpanIndex(position, spanCount);
233                 }
234 
235                 @Override
236                 public int getSpanGroupIndex(int adapterPosition, int spanCount) {
237                     // TODO Optimize
238                     return super.getSpanGroupIndex(adapterPosition, spanCount);
239                 }
240             };
241         }
242     }
243 
244     private static class OtherViewHolder extends ViewHolder {
245         private OtherViewHolder(@NonNull View itemView) {
246             super(itemView);
247         }
248 
249         private void bind(@NonNull Other other) {
250             UriImageView imageView = itemView.findViewById(R.id.other_image);
251 
252             imageView.setImageURI(other.getThumbnailUri());
253 
254             itemView.setOnClickListener((view) ->
255                     OtherDetailsActivity.start(view.getContext(), other));
256         }
257     }
258 }
259