1/*
2 * Copyright (C) 2011 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.detail;
18
19import android.app.Activity;
20import android.content.ActivityNotFoundException;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.database.Cursor;
25import android.graphics.Bitmap;
26import android.graphics.BitmapFactory;
27import android.media.MediaScannerConnection;
28import android.net.Uri;
29import android.provider.ContactsContract.CommonDataKinds.Photo;
30import android.provider.ContactsContract.DisplayPhoto;
31import android.provider.ContactsContract.RawContacts;
32import android.provider.MediaStore;
33import android.util.Log;
34import android.view.View;
35import android.view.View.OnClickListener;
36import android.widget.ListPopupWindow;
37import android.widget.PopupWindow.OnDismissListener;
38import android.widget.Toast;
39
40import com.android.contacts.R;
41import com.android.contacts.editor.PhotoActionPopup;
42import com.android.contacts.model.AccountTypeManager;
43import com.android.contacts.model.RawContactModifier;
44import com.android.contacts.model.RawContactDelta;
45import com.android.contacts.model.RawContactDelta.ValuesDelta;
46import com.android.contacts.model.account.AccountType;
47import com.android.contacts.model.RawContactDeltaList;
48import com.android.contacts.util.ContactPhotoUtils;
49
50import java.io.File;
51
52/**
53 * Handles displaying a photo selection popup for a given photo view and dealing with the results
54 * that come back.
55 */
56public abstract class PhotoSelectionHandler implements OnClickListener {
57
58    private static final String TAG = PhotoSelectionHandler.class.getSimpleName();
59
60    private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1001;
61    private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 1002;
62
63    protected final Context mContext;
64    private final View mPhotoView;
65    private final int mPhotoMode;
66    private final int mPhotoPickSize;
67    private final RawContactDeltaList mState;
68    private final boolean mIsDirectoryContact;
69    private ListPopupWindow mPopup;
70
71    public PhotoSelectionHandler(Context context, View photoView, int photoMode,
72            boolean isDirectoryContact, RawContactDeltaList state) {
73        mContext = context;
74        mPhotoView = photoView;
75        mPhotoMode = photoMode;
76        mIsDirectoryContact = isDirectoryContact;
77        mState = state;
78        mPhotoPickSize = getPhotoPickSize();
79    }
80
81    public void destroy() {
82        if (mPopup != null) {
83            mPopup.dismiss();
84        }
85    }
86
87    public abstract PhotoActionListener getListener();
88
89    @Override
90    public void onClick(View v) {
91        final PhotoActionListener listener = getListener();
92        if (listener != null) {
93            if (getWritableEntityIndex() != -1) {
94                mPopup = PhotoActionPopup.createPopupMenu(
95                        mContext, mPhotoView, listener, mPhotoMode);
96                mPopup.setOnDismissListener(new OnDismissListener() {
97                    @Override
98                    public void onDismiss() {
99                        listener.onPhotoSelectionDismissed();
100                    }
101                });
102                mPopup.show();
103            }
104        }
105    }
106
107    /**
108     * Attempts to handle the given activity result.  Returns whether this handler was able to
109     * process the result successfully.
110     * @param requestCode The request code.
111     * @param resultCode The result code.
112     * @param data The intent that was returned.
113     * @return Whether the handler was able to process the result.
114     */
115    public boolean handlePhotoActivityResult(int requestCode, int resultCode, Intent data) {
116        final PhotoActionListener listener = getListener();
117        if (resultCode == Activity.RESULT_OK) {
118            switch (requestCode) {
119                // Photo was chosen (either new or existing from gallery), and cropped.
120                case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
121                    final String path = ContactPhotoUtils.pathForCroppedPhoto(
122                            mContext, listener.getCurrentPhotoFile());
123                    Bitmap bitmap = BitmapFactory.decodeFile(path);
124                    listener.onPhotoSelected(bitmap);
125                    return true;
126                }
127                // Photo was successfully taken, now crop it.
128                case REQUEST_CODE_CAMERA_WITH_DATA: {
129                    doCropPhoto(listener.getCurrentPhotoFile());
130                    return true;
131                }
132            }
133        }
134        return false;
135    }
136
137    /**
138     * Return the index of the first entity in the contact data that belongs to a contact-writable
139     * account, or -1 if no such entity exists.
140     */
141    private int getWritableEntityIndex() {
142        // Directory entries are non-writable.
143        if (mIsDirectoryContact) return -1;
144        return mState.indexOfFirstWritableRawContact(mContext);
145    }
146
147    /**
148     * Return the raw-contact id of the first entity in the contact data that belongs to a
149     * contact-writable account, or -1 if no such entity exists.
150     */
151    protected long getWritableEntityId() {
152        int index = getWritableEntityIndex();
153        if (index == -1) return -1;
154        return mState.get(index).getValues().getId();
155    }
156
157    /**
158     * Utility method to retrieve the entity delta for attaching the given bitmap to the contact.
159     * This will attach the photo to the first contact-writable account that provided data to the
160     * contact.  It is the caller's responsibility to apply the delta.
161     * @return An entity delta list that can be applied to associate the bitmap with the contact,
162     *     or null if the photo could not be parsed or none of the accounts associated with the
163     *     contact are writable.
164     */
165    public RawContactDeltaList getDeltaForAttachingPhotoToContact() {
166        // Find the first writable entity.
167        int writableEntityIndex = getWritableEntityIndex();
168        if (writableEntityIndex != -1) {
169            // We are guaranteed to have contact data if we have a writable entity index.
170            final RawContactDelta delta = mState.get(writableEntityIndex);
171
172            // Need to find the right account so that EntityModifier knows which fields to add
173            final ContentValues entityValues = delta.getValues().getCompleteValues();
174            final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
175            final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
176            final AccountType accountType = AccountTypeManager.getInstance(mContext).getAccountType(
177                        type, dataSet);
178
179            final ValuesDelta child = RawContactModifier.ensureKindExists(
180                    delta, accountType, Photo.CONTENT_ITEM_TYPE);
181            child.setFromTemplate(false);
182            child.setSuperPrimary(true);
183
184            return mState;
185        }
186        return null;
187    }
188
189    /** Used by subclasses to delegate to their enclosing Activity or Fragment. */
190    protected abstract void startPhotoActivity(Intent intent, int requestCode, String photoFile);
191
192    /**
193     * Sends a newly acquired photo to Gallery for cropping
194     */
195    private void doCropPhoto(String fileName) {
196        try {
197            // Obtain the absolute paths for the newly-taken photo, and the destination
198            // for the soon-to-be-cropped photo.
199            final String newPath = ContactPhotoUtils.pathForNewCameraPhoto(fileName);
200            final String croppedPath = ContactPhotoUtils.pathForCroppedPhoto(mContext, fileName);
201
202            // Add the image to the media store
203            MediaScannerConnection.scanFile(
204                    mContext,
205                    new String[] { newPath },
206                    new String[] { null },
207                    null);
208
209            // Launch gallery to crop the photo
210            final Intent intent = getCropImageIntent(newPath, croppedPath);
211            startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, fileName);
212        } catch (Exception e) {
213            Log.e(TAG, "Cannot crop image", e);
214            Toast.makeText(mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
215        }
216    }
217
218    /**
219     * Should initiate an activity to take a photo using the camera.
220     * @param photoFile The file path that will be used to store the photo.  This is generally
221     *     what should be returned by
222     *     {@link PhotoSelectionHandler.PhotoActionListener#getCurrentPhotoFile()}.
223     */
224    private void startTakePhotoActivity(String photoFile) {
225        final Intent intent = getTakePhotoIntent(photoFile);
226        startPhotoActivity(intent, REQUEST_CODE_CAMERA_WITH_DATA, photoFile);
227    }
228
229    /**
230     * Should initiate an activity pick a photo from the gallery.
231     * @param photoFile The temporary file that the cropped image is written to before being
232     *     stored by the content-provider.
233     *     {@link PhotoSelectionHandler#handlePhotoActivityResult(int, int, Intent)}.
234     */
235    private void startPickFromGalleryActivity(String photoFile) {
236        final Intent intent = getPhotoPickIntent(photoFile);
237        startPhotoActivity(intent, REQUEST_CODE_PHOTO_PICKED_WITH_DATA, photoFile);
238    }
239
240    private int getPhotoPickSize() {
241        // Note that this URI is safe to call on the UI thread.
242        Cursor c = mContext.getContentResolver().query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
243                new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null);
244        try {
245            c.moveToFirst();
246            return c.getInt(0);
247        } finally {
248            c.close();
249        }
250    }
251
252    /**
253     * Constructs an intent for picking a photo from Gallery, cropping it and returning the bitmap.
254     */
255    private Intent getPhotoPickIntent(String photoFile) {
256        final String croppedPhotoPath = ContactPhotoUtils.pathForCroppedPhoto(mContext, photoFile);
257        final Uri croppedPhotoUri = Uri.fromFile(new File(croppedPhotoPath));
258        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
259        intent.setType("image/*");
260        ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize);
261        return intent;
262    }
263
264    /**
265     * Constructs an intent for image cropping.
266     */
267    private Intent getCropImageIntent(String inputPhotoPath, String croppedPhotoPath) {
268        final Uri inputPhotoUri = Uri.fromFile(new File(inputPhotoPath));
269        final Uri croppedPhotoUri = Uri.fromFile(new File(croppedPhotoPath));
270        Intent intent = new Intent("com.android.camera.action.CROP");
271        intent.setDataAndType(inputPhotoUri, "image/*");
272        ContactPhotoUtils.addGalleryIntentExtras(intent, croppedPhotoUri, mPhotoPickSize);
273        return intent;
274    }
275
276    /**
277     * Constructs an intent for capturing a photo and storing it in a temporary file.
278     */
279    private static Intent getTakePhotoIntent(String fileName) {
280        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE, null);
281        final String newPhotoPath = ContactPhotoUtils.pathForNewCameraPhoto(fileName);
282        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(newPhotoPath)));
283        return intent;
284    }
285
286    public abstract class PhotoActionListener implements PhotoActionPopup.Listener {
287        @Override
288        public void onUseAsPrimaryChosen() {
289            // No default implementation.
290        }
291
292        @Override
293        public void onRemovePictureChosen() {
294            // No default implementation.
295        }
296
297        @Override
298        public void onTakePhotoChosen() {
299            try {
300                // Launch camera to take photo for selected contact
301                startTakePhotoActivity(ContactPhotoUtils.generateTempPhotoFileName());
302            } catch (ActivityNotFoundException e) {
303                Toast.makeText(
304                        mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
305            }
306        }
307
308        @Override
309        public void onPickFromGalleryChosen() {
310            try {
311                // Launch picker to choose photo for selected contact
312                startPickFromGalleryActivity(ContactPhotoUtils.generateTempPhotoFileName());
313            } catch (ActivityNotFoundException e) {
314                Toast.makeText(
315                        mContext, R.string.photoPickerNotFoundText, Toast.LENGTH_LONG).show();
316            }
317        }
318
319        /**
320         * Called when the user has completed selection of a photo.
321         * @param bitmap The selected and cropped photo.
322         */
323        public abstract void onPhotoSelected(Bitmap bitmap);
324
325        /**
326         * Gets the current photo file that is being interacted with.  It is the activity or
327         * fragment's responsibility to maintain this in saved state, since this handler instance
328         * will not survive rotation.
329         */
330        public abstract String getCurrentPhotoFile();
331
332        /**
333         * Called when the photo selection dialog is dismissed.
334         */
335        public abstract void onPhotoSelectionDismissed();
336    }
337}
338