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