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
17package com.android.contacts.editor;
18
19import com.android.contacts.R;
20import com.android.contacts.common.ContactPhotoManager;
21import com.android.contacts.common.model.ValuesDelta;
22import com.android.contacts.common.model.account.AccountType;
23
24import android.app.Fragment;
25import android.content.Context;
26import android.net.Uri;
27import android.os.Bundle;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.provider.ContactsContract;
31import android.util.DisplayMetrics;
32import android.view.Display;
33import android.view.LayoutInflater;
34import android.view.MenuItem;
35import android.view.View;
36import android.view.ViewGroup;
37import android.view.accessibility.AccessibilityEvent;
38import android.widget.AdapterView;
39import android.widget.BaseAdapter;
40import android.widget.GridView;
41import android.widget.ImageView;
42
43import java.util.ArrayList;
44
45/**
46 * Displays {@link Photo}s in a grid and calls back the host when one is clicked.
47 */
48public 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         */
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
84        public Photo() {
85        }
86
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
124        public int describeContents() {
125            return 0;
126        }
127
128        @Override
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
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
160        public PhotoAdapter() {
161            mContext = getContext();
162            mLayoutInflater = LayoutInflater.from(mContext);
163        }
164
165        @Override
166        public int getCount() {
167            return mPhotos == null ? 2 : mPhotos.size() + 2;
168        }
169
170        @Override
171        public Object getItem(int index) {
172            return mPhotos == null ? null : mPhotos.get(index);
173        }
174
175        @Override
176        public long getItemId(int index) {
177            return index;
178        }
179
180        @Override
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
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
263    public void setListener(Listener listener) {
264        mListener = listener;
265    }
266
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
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
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
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
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
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
357    public Context getContext() {
358        return getActivity();
359    }
360}