1 /* 2 * Copyright (C) 2006 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.activities; 18 19 import android.content.ActivityNotFoundException; 20 import android.content.ContentResolver; 21 import android.content.Intent; 22 import android.content.Loader; 23 import android.content.Loader.OnLoadCompleteListener; 24 import android.content.pm.PackageManager; 25 import android.database.Cursor; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapFactory; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.provider.ContactsContract.CommonDataKinds.Photo; 31 import android.provider.ContactsContract.Contacts; 32 import android.provider.ContactsContract.DisplayPhoto; 33 import android.util.Log; 34 import android.widget.Toast; 35 36 import com.android.contacts.ContactSaveService; 37 import com.android.contacts.ContactsActivity; 38 import com.android.contacts.R; 39 import com.android.contacts.common.model.Contact; 40 import com.android.contacts.common.model.ContactLoader; 41 import com.android.contacts.common.model.RawContactDelta; 42 import com.android.contacts.common.model.RawContactDeltaList; 43 import com.android.contacts.common.model.RawContactModifier; 44 import com.android.contacts.common.ContactsUtils; 45 import com.android.contacts.common.model.account.AccountType; 46 import com.android.contacts.common.model.ValuesDelta; 47 import com.android.contacts.util.ContactPhotoUtils; 48 49 import java.io.File; 50 import java.io.FileNotFoundException; 51 52 /** 53 * Provides an external interface for other applications to attach images 54 * to contacts. It will first present a contact picker and then run the 55 * image that is handed to it through the cropper to make the image the proper 56 * size and give the user a chance to use the face detector. 57 */ 58 public class AttachPhotoActivity extends ContactsActivity { 59 private static final String TAG = AttachPhotoActivity.class.getSimpleName(); 60 61 private static final int REQUEST_PICK_CONTACT = 1; 62 private static final int REQUEST_CROP_PHOTO = 2; 63 64 private static final String KEY_CONTACT_URI = "contact_uri"; 65 private static final String KEY_TEMP_PHOTO_URI = "temp_photo_uri"; 66 private static final String KEY_CROPPED_PHOTO_URI = "cropped_photo_uri"; 67 68 private Uri mTempPhotoUri; 69 private Uri mCroppedPhotoUri; 70 71 private ContentResolver mContentResolver; 72 73 // Height and width (in pixels) to request for the photo - queried from the provider. 74 private static int mPhotoDim; 75 // Default photo dimension to use if unable to query the provider. 76 private static final int mDefaultPhotoDim = 720; 77 78 private Uri mContactUri; 79 80 @Override 81 public void onCreate(Bundle icicle) { 82 super.onCreate(icicle); 83 84 if (icicle != null) { 85 final String uri = icicle.getString(KEY_CONTACT_URI); 86 mContactUri = (uri == null) ? null : Uri.parse(uri); 87 mTempPhotoUri = Uri.parse(icicle.getString(KEY_TEMP_PHOTO_URI)); 88 mCroppedPhotoUri = Uri.parse(icicle.getString(KEY_CROPPED_PHOTO_URI)); 89 } else { 90 mTempPhotoUri = ContactPhotoUtils.generateTempImageUri(this); 91 mCroppedPhotoUri = ContactPhotoUtils.generateTempCroppedImageUri(this); 92 Intent intent = new Intent(Intent.ACTION_PICK); 93 intent.setType(Contacts.CONTENT_TYPE); 94 startActivityForResult(intent, REQUEST_PICK_CONTACT); 95 } 96 97 mContentResolver = getContentResolver(); 98 99 // Load the photo dimension to request. mPhotoDim is a static class 100 // member varible so only need to load this if this is the first time 101 // through. 102 if (mPhotoDim == 0) { 103 Cursor c = mContentResolver.query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 104 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 105 if (c != null) { 106 try { 107 if (c.moveToFirst()) { 108 mPhotoDim = c.getInt(0); 109 } 110 } finally { 111 c.close(); 112 } 113 } 114 } 115 } 116 117 @Override 118 protected void onSaveInstanceState(Bundle outState) { 119 super.onSaveInstanceState(outState); 120 if (mContactUri != null) { 121 outState.putString(KEY_CONTACT_URI, mContactUri.toString()); 122 } 123 if (mTempPhotoUri != null) { 124 outState.putString(KEY_TEMP_PHOTO_URI, mTempPhotoUri.toString()); 125 } 126 if (mCroppedPhotoUri != null) { 127 outState.putString(KEY_CROPPED_PHOTO_URI, mCroppedPhotoUri.toString()); 128 } 129 } 130 131 @Override 132 protected void onActivityResult(int requestCode, int resultCode, Intent result) { 133 if (requestCode == REQUEST_PICK_CONTACT) { 134 if (resultCode != RESULT_OK) { 135 finish(); 136 return; 137 } 138 // A contact was picked. Launch the cropper to get face detection, the right size, etc. 139 // TODO: get these values from constants somewhere 140 final Intent myIntent = getIntent(); 141 final Uri inputUri = myIntent.getData(); 142 143 final Uri toCrop; 144 // Save the URI into a temporary file provider URI so that 145 // we can add the FLAG_GRANT_WRITE_URI_PERMISSION flag to the eventual 146 // crop intent for read-only URI's. 147 // TODO: With b/10837468 fixed should be able to avoid this copy. 148 ContactPhotoUtils.savePhotoFromUriToUri(this, inputUri, mTempPhotoUri, false); 149 toCrop = mTempPhotoUri; 150 151 final Intent intent = new Intent("com.android.camera.action.CROP", toCrop); 152 if (myIntent.getStringExtra("mimeType") != null) { 153 intent.setDataAndType(toCrop, myIntent.getStringExtra("mimeType")); 154 } 155 ContactPhotoUtils.addPhotoPickerExtras(intent, mCroppedPhotoUri); 156 ContactPhotoUtils.addCropExtras(intent, mPhotoDim != 0 ? mPhotoDim : mDefaultPhotoDim); 157 158 try { 159 startActivityForResult(intent, REQUEST_CROP_PHOTO); 160 } catch (ActivityNotFoundException ex) { 161 Toast.makeText(this, R.string.missing_app, Toast.LENGTH_SHORT).show(); 162 return; 163 } 164 165 mContactUri = result.getData(); 166 167 } else if (requestCode == REQUEST_CROP_PHOTO) { 168 // Delete the temporary photo from cache now that we have a cropped version. 169 // We should do this even if the crop failed and we eventually bail 170 getContentResolver().delete(mTempPhotoUri, null, null); 171 if (resultCode != RESULT_OK) { 172 finish(); 173 return; 174 } 175 loadContact(mContactUri, new Listener() { 176 @Override 177 public void onContactLoaded(Contact contact) { 178 saveContact(contact); 179 } 180 }); 181 } 182 } 183 184 // TODO: consider moving this to ContactLoader, especially if we keep adding similar 185 // code elsewhere (ViewNotificationService is another case). The only concern is that, 186 // although this is convenient, it isn't quite as robust as using LoaderManager... for 187 // instance, the loader doesn't persist across Activity restarts. 188 private void loadContact(Uri contactUri, final Listener listener) { 189 final ContactLoader loader = new ContactLoader(this, contactUri, true); 190 loader.registerListener(0, new OnLoadCompleteListener<Contact>() { 191 @Override 192 public void onLoadComplete( 193 Loader<Contact> loader, Contact contact) { 194 try { 195 loader.reset(); 196 } 197 catch (RuntimeException e) { 198 Log.e(TAG, "Error resetting loader", e); 199 } 200 listener.onContactLoaded(contact); 201 } 202 }); 203 loader.startLoading(); 204 } 205 206 private interface Listener { 207 public void onContactLoaded(Contact contact); 208 } 209 210 /** 211 * If prerequisites have been met, attach the photo to a raw-contact and save. 212 * The prerequisites are: 213 * - photo has been cropped 214 * - contact has been loaded 215 */ 216 private void saveContact(Contact contact) { 217 218 if (contact.getRawContacts() == null) { 219 Log.w(TAG, "No raw contacts found for contact"); 220 finish(); 221 return; 222 } 223 224 // Obtain the raw-contact that we will save to. 225 RawContactDeltaList deltaList = contact.createRawContactDeltaList(); 226 RawContactDelta raw = deltaList.getFirstWritableRawContact(this); 227 if (raw == null) { 228 Log.w(TAG, "no writable raw-contact found"); 229 return; 230 } 231 232 // Create a scaled, compressed bitmap to add to the entity-delta list. 233 final int size = ContactsUtils.getThumbnailSize(this); 234 Bitmap bitmap; 235 try { 236 bitmap = ContactPhotoUtils.getBitmapFromUri(this, mCroppedPhotoUri); 237 } catch (FileNotFoundException e) { 238 Log.w(TAG, "Could not find bitmap"); 239 return; 240 } 241 if (bitmap == null) { 242 Log.w(TAG, "Could not decode bitmap"); 243 return; 244 } 245 246 final Bitmap scaled = Bitmap.createScaledBitmap(bitmap, size, size, false); 247 final byte[] compressed = ContactPhotoUtils.compressBitmap(scaled); 248 if (compressed == null) { 249 Log.w(TAG, "could not create scaled and compressed Bitmap"); 250 return; 251 } 252 // Add compressed bitmap to entity-delta... this allows us to save to 253 // a new contact; otherwise the entity-delta-list would be empty, and 254 // the ContactSaveService would not create the new contact, and the 255 // full-res photo would fail to be saved to the non-existent contact. 256 AccountType account = raw.getRawContactAccountType(this); 257 ValuesDelta values = 258 RawContactModifier.ensureKindExists(raw, account, Photo.CONTENT_ITEM_TYPE); 259 if (values == null) { 260 Log.w(TAG, "cannot attach photo to this account type"); 261 return; 262 } 263 values.setPhoto(compressed); 264 265 // Finally, invoke the ContactSaveService. 266 Log.v(TAG, "all prerequisites met, about to save photo to contact"); 267 Intent intent = ContactSaveService.createSaveContactIntent( 268 this, 269 deltaList, 270 "", 0, 271 contact.isUserProfile(), 272 null, null, 273 raw.getRawContactId(), 274 mCroppedPhotoUri 275 ); 276 startService(intent); 277 finish(); 278 } 279 } 280