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