1 /*
2  * Copyright (C) 2015 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 package com.android.messaging.ui.mediapicker;
17 
18 import android.content.Context;
19 import android.graphics.Rect;
20 import android.net.Uri;
21 import android.os.Parcel;
22 import android.os.Parcelable;
23 import androidx.collection.ArrayMap;
24 import android.util.AttributeSet;
25 import android.view.Menu;
26 import android.view.MenuInflater;
27 import android.view.MenuItem;
28 import android.view.View;
29 
30 import com.android.messaging.R;
31 import com.android.messaging.datamodel.binding.BindingBase;
32 import com.android.messaging.datamodel.binding.ImmutableBindingRef;
33 import com.android.messaging.datamodel.data.DraftMessageData;
34 import com.android.messaging.datamodel.data.GalleryGridItemData;
35 import com.android.messaging.datamodel.data.MessagePartData;
36 import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
37 import com.android.messaging.ui.PersistentInstanceState;
38 import com.android.messaging.util.Assert;
39 import com.android.messaging.util.ContentType;
40 import com.android.messaging.util.LogUtil;
41 
42 import java.util.Iterator;
43 import java.util.Map;
44 
45 /**
46  * Shows a list of galley mediae from external storage in a GridView with multi-select capabilities,
47  * and with the option to intent out to a standalone media picker.
48  */
49 public class GalleryGridView extends MediaPickerGridView implements
50         GalleryGridItemView.HostInterface,
51         PersistentInstanceState,
52         DraftMessageDataListener {
53     /**
54      * Implemented by the owner of this GalleryGridView instance to communicate on media picking and
55      * multi-media selection events.
56      */
57     public interface GalleryGridViewListener {
onDocumentPickerItemClicked()58         void onDocumentPickerItemClicked();
onItemSelected(MessagePartData item)59         void onItemSelected(MessagePartData item);
onItemUnselected(MessagePartData item)60         void onItemUnselected(MessagePartData item);
onConfirmSelection()61         void onConfirmSelection();
onUpdate()62         void onUpdate();
63     }
64 
65     private GalleryGridViewListener mListener;
66 
67     // TODO: Consider putting this into the data model object if we add more states.
68     private final ArrayMap<Uri, MessagePartData> mSelectedImages;
69     private boolean mIsMultiSelectMode = false;
70     private ImmutableBindingRef<DraftMessageData> mDraftMessageDataModel;
71 
GalleryGridView(final Context context, final AttributeSet attrs)72     public GalleryGridView(final Context context, final AttributeSet attrs) {
73         super(context, attrs);
74         mSelectedImages = new ArrayMap<Uri, MessagePartData>();
75     }
76 
setHostInterface(final GalleryGridViewListener hostInterface)77     public void setHostInterface(final GalleryGridViewListener hostInterface) {
78         mListener = hostInterface;
79     }
80 
setDraftMessageDataModel(final BindingBase<DraftMessageData> dataModel)81     public void setDraftMessageDataModel(final BindingBase<DraftMessageData> dataModel) {
82         mDraftMessageDataModel = BindingBase.createBindingReference(dataModel);
83         mDraftMessageDataModel.getData().addListener(this);
84     }
85 
86     @Override
onItemClicked(final View view, final GalleryGridItemData data, final boolean longClick)87     public void onItemClicked(final View view, final GalleryGridItemData data,
88             final boolean longClick) {
89         if (data.isDocumentPickerItem()) {
90             mListener.onDocumentPickerItemClicked();
91         } else if (ContentType.isMediaType(data.getContentType())) {
92             if (longClick) {
93                 // Turn on multi-select mode when an item is long-pressed.
94                 setMultiSelectEnabled(true);
95             }
96 
97             final Rect startRect = new Rect();
98             view.getGlobalVisibleRect(startRect);
99             if (isMultiSelectEnabled()) {
100                 toggleItemSelection(startRect, data);
101             } else {
102                 mListener.onItemSelected(data.constructMessagePartData(startRect));
103             }
104         } else {
105             LogUtil.w(LogUtil.BUGLE_TAG,
106                     "Selected item has invalid contentType " + data.getContentType());
107         }
108     }
109 
110     @Override
isItemSelected(final GalleryGridItemData data)111     public boolean isItemSelected(final GalleryGridItemData data) {
112         return mSelectedImages.containsKey(data.getImageUri());
113     }
114 
getSelectionCount()115     int getSelectionCount() {
116         return mSelectedImages.size();
117     }
118 
119     @Override
isMultiSelectEnabled()120     public boolean isMultiSelectEnabled() {
121         return mIsMultiSelectMode;
122     }
123 
toggleItemSelection(final Rect startRect, final GalleryGridItemData data)124     private void toggleItemSelection(final Rect startRect, final GalleryGridItemData data) {
125         Assert.isTrue(isMultiSelectEnabled());
126         if (isItemSelected(data)) {
127             final MessagePartData item = mSelectedImages.remove(data.getImageUri());
128             mListener.onItemUnselected(item);
129             if (mSelectedImages.size() == 0) {
130                 // No media is selected any more, turn off multi-select mode.
131                 setMultiSelectEnabled(false);
132             }
133         } else {
134             final MessagePartData item = data.constructMessagePartData(startRect);
135             mSelectedImages.put(data.getImageUri(), item);
136             mListener.onItemSelected(item);
137         }
138         invalidateViews();
139     }
140 
toggleMultiSelect()141     private void toggleMultiSelect() {
142         mIsMultiSelectMode = !mIsMultiSelectMode;
143         invalidateViews();
144     }
145 
setMultiSelectEnabled(final boolean enabled)146     private void setMultiSelectEnabled(final boolean enabled) {
147         if (mIsMultiSelectMode != enabled) {
148             toggleMultiSelect();
149         }
150     }
151 
canToggleMultiSelect()152     private boolean canToggleMultiSelect() {
153         // We allow the user to toggle multi-select mode only when nothing has selected. If
154         // something has been selected, we show a confirm button instead.
155         return mSelectedImages.size() == 0;
156     }
157 
onCreateOptionsMenu(final MenuInflater inflater, final Menu menu)158     public void onCreateOptionsMenu(final MenuInflater inflater, final Menu menu) {
159         inflater.inflate(R.menu.gallery_picker_menu, menu);
160         final MenuItem toggleMultiSelect = menu.findItem(R.id.action_multiselect);
161         final MenuItem confirmMultiSelect = menu.findItem(R.id.action_confirm_multiselect);
162         final boolean canToggleMultiSelect = canToggleMultiSelect();
163         toggleMultiSelect.setVisible(canToggleMultiSelect);
164         confirmMultiSelect.setVisible(!canToggleMultiSelect);
165     }
166 
onOptionsItemSelected(final MenuItem item)167     public boolean onOptionsItemSelected(final MenuItem item) {
168         switch (item.getItemId()) {
169             case R.id.action_multiselect:
170                 Assert.isTrue(canToggleMultiSelect());
171                 toggleMultiSelect();
172                 return true;
173 
174             case R.id.action_confirm_multiselect:
175                 Assert.isTrue(!canToggleMultiSelect());
176                 mListener.onConfirmSelection();
177                 return true;
178         }
179         return false;
180     }
181 
182 
183     @Override
onDraftChanged(final DraftMessageData data, final int changeFlags)184     public void onDraftChanged(final DraftMessageData data, final int changeFlags) {
185         mDraftMessageDataModel.ensureBound(data);
186         // Whenever attachment changed, refresh selection state to remove those that are not
187         // selected.
188         if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) ==
189                 DraftMessageData.ATTACHMENTS_CHANGED) {
190             refreshImageSelectionStateOnAttachmentChange();
191         }
192     }
193 
194     @Override
onDraftAttachmentLimitReached(final DraftMessageData data)195     public void onDraftAttachmentLimitReached(final DraftMessageData data) {
196         mDraftMessageDataModel.ensureBound(data);
197         // Whenever draft attachment limit is reach, refresh selection state to remove those
198         // not actually added to draft.
199         refreshImageSelectionStateOnAttachmentChange();
200     }
201 
202     @Override
onDraftAttachmentLoadFailed()203     public void onDraftAttachmentLoadFailed() {
204         // Nothing to do since the failed attachment gets removed automatically.
205     }
206 
refreshImageSelectionStateOnAttachmentChange()207     private void refreshImageSelectionStateOnAttachmentChange() {
208         boolean changed = false;
209         final Iterator<Map.Entry<Uri, MessagePartData>> iterator =
210                 mSelectedImages.entrySet().iterator();
211         while (iterator.hasNext()) {
212             Map.Entry<Uri, MessagePartData> entry = iterator.next();
213             if (!mDraftMessageDataModel.getData().containsAttachment(entry.getKey())) {
214                 iterator.remove();
215                 changed = true;
216             }
217         }
218 
219         if (changed) {
220             mListener.onUpdate();
221             invalidateViews();
222         }
223     }
224 
225     @Override   // PersistentInstanceState
saveState()226     public Parcelable saveState() {
227         return onSaveInstanceState();
228     }
229 
230     @Override   // PersistentInstanceState
restoreState(final Parcelable restoredState)231     public void restoreState(final Parcelable restoredState) {
232         onRestoreInstanceState(restoredState);
233         invalidateViews();
234     }
235 
236     @Override
onSaveInstanceState()237     public Parcelable onSaveInstanceState() {
238         final Parcelable superState = super.onSaveInstanceState();
239         final SavedState savedState = new SavedState(superState);
240         savedState.isMultiSelectMode = mIsMultiSelectMode;
241         savedState.selectedImages = mSelectedImages.values()
242                 .toArray(new MessagePartData[mSelectedImages.size()]);
243         return savedState;
244     }
245 
246     @Override
onRestoreInstanceState(final Parcelable state)247     public void onRestoreInstanceState(final Parcelable state) {
248         if (!(state instanceof SavedState)) {
249             super.onRestoreInstanceState(state);
250             return;
251         }
252 
253         final SavedState savedState = (SavedState) state;
254         super.onRestoreInstanceState(savedState.getSuperState());
255         mIsMultiSelectMode = savedState.isMultiSelectMode;
256         mSelectedImages.clear();
257         for (int i = 0; i < savedState.selectedImages.length; i++) {
258             final MessagePartData selectedImage = savedState.selectedImages[i];
259             mSelectedImages.put(selectedImage.getContentUri(), selectedImage);
260         }
261     }
262 
263     @Override   // PersistentInstanceState
resetState()264     public void resetState() {
265         mSelectedImages.clear();
266         mIsMultiSelectMode = false;
267         invalidateViews();
268     }
269 
270     public static class SavedState extends BaseSavedState {
271         boolean isMultiSelectMode;
272         MessagePartData[] selectedImages;
273 
SavedState(final Parcelable superState)274         SavedState(final Parcelable superState) {
275             super(superState);
276         }
277 
SavedState(final Parcel in)278         private SavedState(final Parcel in) {
279             super(in);
280             isMultiSelectMode = in.readInt() == 1 ? true : false;
281 
282             // Read parts
283             final int partCount = in.readInt();
284             selectedImages = new MessagePartData[partCount];
285             for (int i = 0; i < partCount; i++) {
286                 selectedImages[i] = ((MessagePartData) in.readParcelable(
287                         MessagePartData.class.getClassLoader()));
288             }
289         }
290 
291         @Override
writeToParcel(final Parcel out, final int flags)292         public void writeToParcel(final Parcel out, final int flags) {
293             super.writeToParcel(out, flags);
294             out.writeInt(isMultiSelectMode ? 1 : 0);
295 
296             // Write parts
297             out.writeInt(selectedImages.length);
298             for (final MessagePartData image : selectedImages) {
299                 out.writeParcelable(image, flags);
300             }
301         }
302 
303         public static final Parcelable.Creator<SavedState> CREATOR =
304                 new Parcelable.Creator<SavedState>() {
305             @Override
306             public SavedState createFromParcel(final Parcel in) {
307                 return new SavedState(in);
308             }
309             @Override
310             public SavedState[] newArray(final int size) {
311                 return new SavedState[size];
312             }
313         };
314     }
315 }
316