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