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 }