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 17 package com.android.contacts.editor; 18 19 import com.android.contacts.R; 20 import com.android.contacts.common.ContactPhotoManager; 21 import com.android.contacts.common.model.ValuesDelta; 22 import com.android.contacts.common.model.account.AccountType; 23 24 import android.app.Fragment; 25 import android.content.Context; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.provider.ContactsContract; 31 import android.util.DisplayMetrics; 32 import android.view.Display; 33 import android.view.LayoutInflater; 34 import android.view.MenuItem; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.accessibility.AccessibilityEvent; 38 import android.widget.AdapterView; 39 import android.widget.BaseAdapter; 40 import android.widget.GridView; 41 import android.widget.ImageView; 42 43 import java.util.ArrayList; 44 45 /** 46 * Displays {@link Photo}s in a grid and calls back the host when one is clicked. 47 */ 48 public class CompactPhotoSelectionFragment extends Fragment { 49 50 private static final String STATE_PHOTOS = "photos"; 51 private static final String STATE_PHOTO_MODE = "photoMode"; 52 private final int VIEW_TYPE_TAKE_PHOTO = 0; 53 private final int VIEW_TYPE_ALL_PHOTOS = 1; 54 private final int VIEW_TYPE_IMAGE = 2; 55 56 /** 57 * Callbacks hosts this Fragment. 58 */ 59 public interface Listener { 60 61 /** 62 * Invoked when the user wants to change their photo. 63 */ onPhotoSelected(Photo photo)64 void onPhotoSelected(Photo photo); 65 } 66 67 /** 68 * Holds a photo {@link ValuesDelta} and {@link AccountType} information to draw 69 * an account type icon over it. 70 */ 71 public static final class Photo implements Parcelable { 72 73 public static final Creator<Photo> CREATOR = new Creator<Photo>() { 74 75 public Photo createFromParcel(Parcel in) { 76 return new Photo(in); 77 } 78 79 public Photo[] newArray(int size) { 80 return new Photo[size]; 81 } 82 }; 83 Photo()84 public Photo() { 85 } 86 Photo(Parcel source)87 private Photo(Parcel source) { 88 readFromParcel(source); 89 } 90 91 // From AccountType, everything we need to display the account type icon 92 public int titleRes; 93 public int iconRes; 94 public String syncAdapterPackageName; 95 96 public String contentDescription; 97 public String contentDescriptionChecked; // Talkback announcement when the photo is checked 98 public String accountType; 99 public String accountName; 100 101 public ValuesDelta valuesDelta; 102 103 /** 104 * Whether the photo is being displayed for the aggregate contact. 105 * This may be because it is marked super primary or it is the one quick contacts picked 106 * randomly to display because none is marked super primary. 107 */ 108 public boolean primary; 109 110 /** 111 * Pointer back to the KindSectionDataList this photo came from. 112 * See {@link CompactRawContactsEditorView#getPhotos} 113 * See {@link CompactRawContactsEditorView#setPrimaryPhoto} 114 */ 115 public int kindSectionDataListIndex = -1; 116 public int valuesDeltaListIndex = -1; 117 118 /** Newly taken or selected photo that has not yet been saved to CP2. */ 119 public Uri updatedPhotoUri; 120 121 public long photoId; 122 123 @Override describeContents()124 public int describeContents() { 125 return 0; 126 } 127 128 @Override writeToParcel(Parcel dest, int flags)129 public void writeToParcel(Parcel dest, int flags) { 130 dest.writeInt(titleRes); 131 dest.writeInt(iconRes); 132 dest.writeString(syncAdapterPackageName); 133 dest.writeParcelable(valuesDelta, flags); 134 dest.writeInt(primary ? 1 : 0); 135 dest.writeInt(kindSectionDataListIndex); 136 dest.writeInt(valuesDeltaListIndex); 137 dest.writeParcelable(updatedPhotoUri, flags); 138 dest.writeLong(photoId); 139 } 140 readFromParcel(Parcel source)141 private void readFromParcel(Parcel source) { 142 final ClassLoader classLoader = getClass().getClassLoader(); 143 titleRes = source.readInt(); 144 iconRes = source.readInt(); 145 syncAdapterPackageName = source.readString(); 146 valuesDelta = source.readParcelable(classLoader); 147 primary = source.readInt() == 1; 148 kindSectionDataListIndex = source.readInt(); 149 valuesDeltaListIndex = source.readInt(); 150 updatedPhotoUri = source.readParcelable(classLoader); 151 photoId = source.readLong(); 152 } 153 } 154 155 private final class PhotoAdapter extends BaseAdapter { 156 157 private final Context mContext; 158 private final LayoutInflater mLayoutInflater; 159 PhotoAdapter()160 public PhotoAdapter() { 161 mContext = getContext(); 162 mLayoutInflater = LayoutInflater.from(mContext); 163 } 164 165 @Override getCount()166 public int getCount() { 167 return mPhotos == null ? 2 : mPhotos.size() + 2; 168 } 169 170 @Override getItem(int index)171 public Object getItem(int index) { 172 return mPhotos == null ? null : mPhotos.get(index); 173 } 174 175 @Override getItemId(int index)176 public long getItemId(int index) { 177 return index; 178 } 179 180 @Override getItemViewType(int index)181 public int getItemViewType(int index) { 182 if (index == 0) { 183 return VIEW_TYPE_TAKE_PHOTO; 184 } else if (index == 1) { 185 return VIEW_TYPE_ALL_PHOTOS; 186 } else { 187 return VIEW_TYPE_IMAGE; 188 } 189 } 190 191 @Override getView(int position, View convertView, ViewGroup parent)192 public View getView(int position, View convertView, ViewGroup parent) { 193 if (mPhotos == null) return null; 194 195 // when position is 0 or 1, we should make sure account_type *is not* in convertView 196 // before reusing it. 197 if (getItemViewType(position) == 0){ 198 if (convertView == null || convertView.findViewById(R.id.account_type) != null) { 199 return mLayoutInflater.inflate(R.layout.take_a_photo_button, /* root =*/ null); 200 } 201 return convertView; 202 } 203 204 if (getItemViewType(position) == 1) { 205 if (convertView == null || convertView.findViewById(R.id.account_type) != null) { 206 return mLayoutInflater.inflate(R.layout.all_photos_button, /* root =*/ null); 207 } 208 return convertView; 209 } 210 211 // when position greater than 1, we should make sure account_type *is* in convertView 212 // before reusing it. 213 position -= 2; 214 215 final View photoItemView; 216 if (convertView == null || convertView.findViewById(R.id.account_type) == null) { 217 photoItemView = mLayoutInflater.inflate( 218 R.layout.compact_photo_selection_item, /* root =*/ null); 219 } else { 220 photoItemView = convertView; 221 } 222 223 final Photo photo = mPhotos.get(position); 224 225 // Bind the photo 226 final ImageView imageView = (ImageView) photoItemView.findViewById(R.id.image); 227 if (photo.updatedPhotoUri != null) { 228 EditorUiUtils.loadPhoto(ContactPhotoManager.getInstance(mContext), 229 imageView, photo.updatedPhotoUri); 230 } else { 231 final Long photoFileId = EditorUiUtils.getPhotoFileId(photo.valuesDelta); 232 if (photoFileId != null) { 233 final Uri photoUri = ContactsContract.DisplayPhoto.CONTENT_URI.buildUpon() 234 .appendPath(photoFileId.toString()).build(); 235 EditorUiUtils.loadPhoto(ContactPhotoManager.getInstance(mContext), 236 imageView, photoUri); 237 } else { 238 imageView.setImageBitmap(EditorUiUtils.getPhotoBitmap(photo.valuesDelta)); 239 } 240 } 241 242 // Add the account type icon 243 final ImageView accountTypeImageView = (ImageView) 244 photoItemView.findViewById(R.id.account_type); 245 accountTypeImageView.setImageDrawable(AccountType.getDisplayIcon( 246 mContext, photo.titleRes, photo.iconRes, photo.syncAdapterPackageName)); 247 248 // Display a check icon over the primary photo 249 final ImageView checkImageView = (ImageView) photoItemView.findViewById(R.id.check); 250 checkImageView.setVisibility(photo.primary ? View.VISIBLE : View.GONE); 251 252 photoItemView.setContentDescription(photo.contentDescription); 253 254 return photoItemView; 255 } 256 } 257 258 private ArrayList<Photo> mPhotos; 259 private int mPhotoMode; 260 private Listener mListener; 261 private GridView mGridView; 262 setListener(Listener listener)263 public void setListener(Listener listener) { 264 mListener = listener; 265 } 266 setPhotos(ArrayList<Photo> photos, int photoMode)267 public void setPhotos(ArrayList<Photo> photos, int photoMode) { 268 mPhotos = photos; 269 mPhotoMode = photoMode; 270 mGridView.setAccessibilityDelegate(new View.AccessibilityDelegate() {}); 271 } 272 273 @Override onCreate(Bundle savedInstanceState)274 public void onCreate(Bundle savedInstanceState) { 275 super.onCreate(savedInstanceState); 276 if (savedInstanceState != null) { 277 mPhotos = savedInstanceState.getParcelableArrayList(STATE_PHOTOS); 278 mPhotoMode = savedInstanceState.getInt(STATE_PHOTO_MODE, 0); 279 } 280 } 281 282 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)283 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 284 setHasOptionsMenu(true); 285 286 final PhotoAdapter photoAdapter = new PhotoAdapter(); 287 288 final View view = inflater.inflate(R.layout.compact_photo_selection_fragment, 289 container, false); 290 mGridView = (GridView) view.findViewById(R.id.grid_view); 291 mGridView.setAdapter(photoAdapter); 292 mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 293 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 294 final PhotoSourceDialogFragment.Listener listener = 295 (PhotoSourceDialogFragment.Listener) getActivity(); 296 if (position == 0) { 297 listener.onTakePhotoChosen(); 298 } else if (position == 1) { 299 listener.onPickFromGalleryChosen(); 300 } else { 301 // Call the host back so it can set the new photo as primary 302 final Photo photo = (Photo) photoAdapter.getItem(position - 2); 303 if (mListener != null) { 304 mListener.onPhotoSelected(photo); 305 } 306 handleAccessibility(photo, position); 307 } 308 } 309 }); 310 311 final Display display = getActivity().getWindowManager().getDefaultDisplay(); 312 final DisplayMetrics outMetrics = new DisplayMetrics (); 313 display.getRealMetrics(outMetrics); // real metrics include the navigation Bar 314 315 final float numColumns = outMetrics.widthPixels / 316 getResources().getDimension(R.dimen.photo_picker_item_ideal_width); 317 mGridView.setNumColumns(Math.round(numColumns)); 318 319 return view; 320 } 321 handleAccessibility(Photo photo, int position)322 private void handleAccessibility(Photo photo, int position) { 323 // Use custom AccessibilityDelegate when closing this fragment to suppress event. 324 mGridView.setAccessibilityDelegate(new View.AccessibilityDelegate() { 325 @Override 326 public boolean onRequestSendAccessibilityEvent( 327 ViewGroup host, View child,AccessibilityEvent event) { 328 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { 329 return false; 330 } 331 return super.onRequestSendAccessibilityEvent(host, child, event); 332 } 333 }); 334 final ViewGroup clickedView = (ViewGroup) mGridView.getChildAt(position); 335 clickedView.announceForAccessibility(photo.contentDescriptionChecked); 336 } 337 338 @Override onSaveInstanceState(Bundle outState)339 public void onSaveInstanceState(Bundle outState) { 340 outState.putParcelableArrayList(STATE_PHOTOS, mPhotos); 341 outState.putInt(STATE_PHOTO_MODE, mPhotoMode); 342 super.onSaveInstanceState(outState); 343 } 344 345 @Override onOptionsItemSelected(MenuItem item)346 public boolean onOptionsItemSelected(MenuItem item) { 347 switch (item.getItemId()) { 348 case android.R.id.home: 349 getActivity().onBackPressed(); 350 return true; 351 default: 352 return super.onOptionsItemSelected(item); 353 } 354 } 355 356 @Override getContext()357 public Context getContext() { 358 return getActivity(); 359 } 360 }