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 */
16package com.android.messaging.ui.mediapicker;
17
18import android.content.Context;
19import android.graphics.Rect;
20import android.net.Uri;
21import android.os.Parcel;
22import android.os.Parcelable;
23import android.support.v4.util.ArrayMap;
24import android.util.AttributeSet;
25import android.view.Menu;
26import android.view.MenuInflater;
27import android.view.MenuItem;
28import android.view.View;
29
30import com.android.messaging.R;
31import com.android.messaging.datamodel.binding.BindingBase;
32import com.android.messaging.datamodel.binding.ImmutableBindingRef;
33import com.android.messaging.datamodel.data.DraftMessageData;
34import com.android.messaging.datamodel.data.GalleryGridItemData;
35import com.android.messaging.datamodel.data.MessagePartData;
36import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener;
37import com.android.messaging.ui.PersistentInstanceState;
38import com.android.messaging.util.Assert;
39import com.android.messaging.util.ContentType;
40import com.android.messaging.util.LogUtil;
41
42import java.util.Iterator;
43import java.util.Map;
44
45/**
46 * Shows a list of galley images from external storage in a GridView with multi-select
47 * capabilities, and with the option to intent out to a standalone image picker.
48 */
49public 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 image
55     * picking and multi-image selection events.
56     */
57    public interface GalleryGridViewListener {
58        void onDocumentPickerItemClicked();
59        void onItemSelected(MessagePartData item);
60        void onItemUnselected(MessagePartData item);
61        void onConfirmSelection();
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
72    public GalleryGridView(final Context context, final AttributeSet attrs) {
73        super(context, attrs);
74        mSelectedImages = new ArrayMap<Uri, MessagePartData>();
75    }
76
77    public void setHostInterface(final GalleryGridViewListener hostInterface) {
78        mListener = hostInterface;
79    }
80
81    public void setDraftMessageDataModel(final BindingBase<DraftMessageData> dataModel) {
82        mDraftMessageDataModel = BindingBase.createBindingReference(dataModel);
83        mDraftMessageDataModel.getData().addListener(this);
84    }
85
86    @Override
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
111    public boolean isItemSelected(final GalleryGridItemData data) {
112        return mSelectedImages.containsKey(data.getImageUri());
113    }
114
115    int getSelectionCount() {
116        return mSelectedImages.size();
117    }
118
119    @Override
120    public boolean isMultiSelectEnabled() {
121        return mIsMultiSelectMode;
122    }
123
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 image 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
141    private void toggleMultiSelect() {
142        mIsMultiSelectMode = !mIsMultiSelectMode;
143        invalidateViews();
144    }
145
146    private void setMultiSelectEnabled(final boolean enabled) {
147        if (mIsMultiSelectMode != enabled) {
148            toggleMultiSelect();
149        }
150    }
151
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
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
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
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
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
203    public void onDraftAttachmentLoadFailed() {
204        // Nothing to do since the failed attachment gets removed automatically.
205    }
206
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
226    public Parcelable saveState() {
227        return onSaveInstanceState();
228    }
229
230    @Override   // PersistentInstanceState
231    public void restoreState(final Parcelable restoredState) {
232        onRestoreInstanceState(restoredState);
233        invalidateViews();
234    }
235
236    @Override
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
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
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
274        SavedState(final Parcelable superState) {
275            super(superState);
276        }
277
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
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