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 17package com.android.contacts.activities; 18 19import com.android.contacts.ContactsActivity; 20import com.android.contacts.R; 21import com.android.contacts.model.ExchangeAccountType; 22import com.android.contacts.model.GoogleAccountType; 23 24import android.content.ContentProviderOperation; 25import android.content.ContentResolver; 26import android.content.ContentUris; 27import android.content.ContentValues; 28import android.content.Intent; 29import android.content.OperationApplicationException; 30import android.database.Cursor; 31import android.graphics.Bitmap; 32import android.net.Uri; 33import android.os.Bundle; 34import android.os.RemoteException; 35import android.provider.ContactsContract; 36import android.provider.ContactsContract.CommonDataKinds.Photo; 37import android.provider.ContactsContract.Contacts; 38import android.provider.ContactsContract.DisplayPhoto; 39import android.provider.ContactsContract.RawContacts; 40import android.widget.Toast; 41 42import java.io.ByteArrayOutputStream; 43import java.util.ArrayList; 44 45/** 46 * Provides an external interface for other applications to attach images 47 * to contacts. It will first present a contact picker and then run the 48 * image that is handed to it through the cropper to make the image the proper 49 * size and give the user a chance to use the face detector. 50 */ 51public class AttachPhotoActivity extends ContactsActivity { 52 private static final int REQUEST_PICK_CONTACT = 1; 53 private static final int REQUEST_CROP_PHOTO = 2; 54 55 private static final String RAW_CONTACT_URIS_KEY = "raw_contact_uris"; 56 57 private Long[] mRawContactIds; 58 59 private ContentResolver mContentResolver; 60 61 // Height/width (in pixels) to request for the photo - queried from the provider. 62 private static int mPhotoDim; 63 64 @Override 65 public void onCreate(Bundle icicle) { 66 super.onCreate(icicle); 67 68 if (icicle != null) { 69 mRawContactIds = toClassArray(icicle.getLongArray(RAW_CONTACT_URIS_KEY)); 70 } else { 71 Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 72 intent.setType(Contacts.CONTENT_ITEM_TYPE); 73 startActivityForResult(intent, REQUEST_PICK_CONTACT); 74 } 75 76 mContentResolver = getContentResolver(); 77 78 // Load the photo dimension to request. 79 Cursor c = mContentResolver.query(DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI, 80 new String[]{DisplayPhoto.DISPLAY_MAX_DIM}, null, null, null); 81 try { 82 c.moveToFirst(); 83 mPhotoDim = c.getInt(0); 84 } finally { 85 c.close(); 86 } 87 } 88 89 @Override 90 protected void onSaveInstanceState(Bundle outState) { 91 super.onSaveInstanceState(outState); 92 93 if (mRawContactIds != null && mRawContactIds.length != 0) { 94 outState.putLongArray(RAW_CONTACT_URIS_KEY, toPrimativeArray(mRawContactIds)); 95 } 96 } 97 98 private static long[] toPrimativeArray(Long[] in) { 99 if (in == null) { 100 return null; 101 } 102 long[] out = new long[in.length]; 103 for (int i = 0; i < in.length; i++) { 104 out[i] = in[i]; 105 } 106 return out; 107 } 108 109 private static Long[] toClassArray(long[] in) { 110 if (in == null) { 111 return null; 112 } 113 Long[] out = new Long[in.length]; 114 for (int i = 0; i < in.length; i++) { 115 out[i] = in[i]; 116 } 117 return out; 118 } 119 120 @Override 121 protected void onActivityResult(int requestCode, int resultCode, Intent result) { 122 if (resultCode != RESULT_OK) { 123 finish(); 124 return; 125 } 126 127 if (requestCode == REQUEST_PICK_CONTACT) { 128 // A contact was picked. Launch the cropper to get face detection, the right size, etc. 129 // TODO: get these values from constants somewhere 130 Intent myIntent = getIntent(); 131 Intent intent = new Intent("com.android.camera.action.CROP", myIntent.getData()); 132 if (myIntent.getStringExtra("mimeType") != null) { 133 intent.setDataAndType(myIntent.getData(), myIntent.getStringExtra("mimeType")); 134 } 135 intent.putExtra("crop", "true"); 136 intent.putExtra("aspectX", 1); 137 intent.putExtra("aspectY", 1); 138 intent.putExtra("outputX", mPhotoDim); 139 intent.putExtra("outputY", mPhotoDim); 140 intent.putExtra("return-data", true); 141 startActivityForResult(intent, REQUEST_CROP_PHOTO); 142 143 // while they're cropping, convert the contact into a raw_contact 144 final long contactId = ContentUris.parseId(result.getData()); 145 final ArrayList<Long> rawContactIdsList = queryForAllRawContactIds( 146 mContentResolver, contactId); 147 mRawContactIds = new Long[rawContactIdsList.size()]; 148 mRawContactIds = rawContactIdsList.toArray(mRawContactIds); 149 150 if (mRawContactIds == null || rawContactIdsList.isEmpty()) { 151 Toast.makeText(this, R.string.contactSavedErrorToast, Toast.LENGTH_LONG).show(); 152 } 153 } else if (requestCode == REQUEST_CROP_PHOTO) { 154 final Bundle extras = result.getExtras(); 155 if (extras != null && mRawContactIds != null) { 156 Bitmap photo = extras.getParcelable("data"); 157 if (photo != null) { 158 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 159 photo.compress(Bitmap.CompressFormat.JPEG, 75, stream); 160 161 final ContentValues imageValues = new ContentValues(); 162 imageValues.put(Photo.PHOTO, stream.toByteArray()); 163 imageValues.put(RawContacts.Data.IS_SUPER_PRIMARY, 1); 164 165 // attach the photo to every raw contact 166 for (Long rawContactId : mRawContactIds) { 167 168 // exchange and google only allow one image, so do an update rather than insert 169 boolean shouldUpdate = false; 170 171 final Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, 172 rawContactId); 173 final Uri rawContactDataUri = Uri.withAppendedPath(rawContactUri, 174 RawContacts.Data.CONTENT_DIRECTORY); 175 insertPhoto(imageValues, rawContactDataUri, true); 176 } 177 } 178 } 179 finish(); 180 } 181 } 182 183 // TODO: move to background 184 public static ArrayList<Long> queryForAllRawContactIds(ContentResolver cr, long contactId) { 185 Cursor rawContactIdCursor = null; 186 ArrayList<Long> rawContactIds = new ArrayList<Long>(); 187 try { 188 rawContactIdCursor = cr.query(RawContacts.CONTENT_URI, 189 new String[] {RawContacts._ID}, 190 RawContacts.CONTACT_ID + "=" + contactId, null, null); 191 if (rawContactIdCursor != null) { 192 while (rawContactIdCursor.moveToNext()) { 193 rawContactIds.add(rawContactIdCursor.getLong(0)); 194 } 195 } 196 } finally { 197 if (rawContactIdCursor != null) { 198 rawContactIdCursor.close(); 199 } 200 } 201 return rawContactIds; 202 } 203 204 /** 205 * Inserts a photo on the raw contact. 206 * @param values the photo values 207 * @param assertAccount if true, will check to verify that no photos exist for Google, 208 * Exchange and unsynced phone account types. These account types only take one picture, 209 * so if one exists, the account will be updated with the new photo. 210 */ 211 private void insertPhoto(ContentValues values, Uri rawContactDataUri, 212 boolean assertAccount) { 213 214 ArrayList<ContentProviderOperation> operations = 215 new ArrayList<ContentProviderOperation>(); 216 217 if (assertAccount) { 218 // Make sure no pictures exist for Google, Exchange and unsynced phone accounts. 219 operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri) 220 .withSelection(Photo.MIMETYPE + "=? AND " 221 + RawContacts.DATA_SET + " IS NULL AND (" 222 + RawContacts.ACCOUNT_TYPE + " IN (?,?) OR " 223 + RawContacts.ACCOUNT_TYPE + " IS NULL)", 224 new String[] {Photo.CONTENT_ITEM_TYPE, GoogleAccountType.ACCOUNT_TYPE, 225 ExchangeAccountType.ACCOUNT_TYPE}) 226 .withExpectedCount(0).build()); 227 } 228 229 // insert the photo 230 values.put(Photo.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 231 operations.add(ContentProviderOperation.newInsert(rawContactDataUri) 232 .withValues(values).build()); 233 234 try { 235 mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations); 236 } catch (RemoteException e) { 237 throw new IllegalStateException("Problem querying raw_contacts/data", e); 238 } catch (OperationApplicationException e) { 239 // the account doesn't allow multiple photos, so update 240 if (assertAccount) { 241 updatePhoto(values, rawContactDataUri, false); 242 } else { 243 throw new IllegalStateException("Problem inserting photo into raw_contacts/data", e); 244 } 245 } 246 } 247 248 /** 249 * Tries to update the photo on the raw_contact. If no photo exists, and allowInsert == true, 250 * then will try to {@link #updatePhoto(ContentValues, boolean)} 251 */ 252 private void updatePhoto(ContentValues values, Uri rawContactDataUri, 253 boolean allowInsert) { 254 ArrayList<ContentProviderOperation> operations = 255 new ArrayList<ContentProviderOperation>(); 256 257 values.remove(Photo.MIMETYPE); 258 259 // check that a photo exists 260 operations.add(ContentProviderOperation.newAssertQuery(rawContactDataUri) 261 .withSelection(Photo.MIMETYPE + "=?", new String[] { 262 Photo.CONTENT_ITEM_TYPE 263 }).withExpectedCount(1).build()); 264 265 // update that photo 266 operations.add(ContentProviderOperation.newUpdate(rawContactDataUri) 267 .withSelection(Photo.MIMETYPE + "=?", new String[] {Photo.CONTENT_ITEM_TYPE}) 268 .withValues(values).build()); 269 270 try { 271 mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations); 272 } catch (RemoteException e) { 273 throw new IllegalStateException("Problem querying raw_contacts/data", e); 274 } catch (OperationApplicationException e) { 275 if (allowInsert) { 276 // they deleted the photo between insert and update, so insert one 277 insertPhoto(values, rawContactDataUri, false); 278 } else { 279 throw new IllegalStateException("Problem inserting photo raw_contacts/data", e); 280 } 281 } 282 } 283} 284