1 /* 2 * Copyright (C) 2015 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.editor; 18 19 import com.android.contacts.ContactSaveService; 20 import com.android.contacts.R; 21 import com.android.contacts.activities.CompactContactEditorActivity; 22 import com.android.contacts.common.model.AccountTypeManager; 23 import com.android.contacts.common.model.RawContactDelta; 24 import com.android.contacts.common.model.RawContactDeltaList; 25 import com.android.contacts.common.model.ValuesDelta; 26 import com.android.contacts.common.model.account.AccountType; 27 import com.android.contacts.common.util.ImplicitIntentsUtil; 28 import com.android.contacts.detail.PhotoSelectionHandler; 29 import com.android.contacts.util.ContactPhotoUtils; 30 31 import android.app.Activity; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.graphics.Bitmap; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.util.Log; 38 import android.view.LayoutInflater; 39 import android.view.MenuItem; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.widget.LinearLayout; 43 44 import java.io.FileNotFoundException; 45 46 /** 47 * Contact editor with only the most important fields displayed initially. 48 */ 49 public class CompactContactEditorFragment extends ContactEditorBaseFragment implements 50 CompactRawContactsEditorView.Listener, PhotoSourceDialogFragment.Listener { 51 52 private static final String KEY_PHOTO_URI = "photo_uri"; 53 private static final String KEY_PHOTO_RAW_CONTACT_ID = "photo_raw_contact_id"; 54 55 /** 56 * Displays a PopupWindow with photo edit options. 57 */ 58 final class PhotoHandler extends PhotoSelectionHandler implements View.OnClickListener { 59 60 /** 61 * Receiver of photo edit option callbacks. 62 */ 63 private final class PhotoListener extends PhotoActionListener { 64 65 @Override onRemovePictureChosen()66 public void onRemovePictureChosen() { 67 getContent().setPhoto(/* bitmap =*/ null); 68 mUpdatedPhotos.remove(String.valueOf(mPhotoRawContactId)); 69 70 // Update the mode so the options change if user clicks the photo again 71 mPhotoMode = getPhotoMode(); 72 } 73 74 @Override onPhotoSelected(Uri uri)75 public void onPhotoSelected(Uri uri) throws FileNotFoundException { 76 final Bitmap bitmap = ContactPhotoUtils.getBitmapFromUri(getActivity(), uri); 77 if (bitmap == null || bitmap.getHeight() <= 0 || bitmap.getWidth() <= 0) { 78 Log.w(TAG, "Invalid photo selected"); 79 } 80 getContent().setPhoto(bitmap); 81 82 // Clear any previously saved full resolution photos under negative raw contact IDs 83 // so that we will use the newly selected photo, instead of an old one on rotations. 84 removeNewRawContactPhotos(); 85 86 // If a new photo was chosen but not yet saved, 87 // we need to update the UI immediately 88 mUpdatedPhotos.putParcelable(String.valueOf(mPhotoRawContactId), uri); 89 getContent().setFullSizePhoto(uri); 90 91 // Update the mode so the options change if user clicks the photo again 92 mPhotoMode = getPhotoMode(); 93 94 // Re-create the photo handler so that any additional photo selections create a 95 // new temp file (and don't hit the one that was just added to the cache). 96 mPhotoHandler = createPhotoHandler(); 97 } 98 99 @Override getCurrentPhotoUri()100 public Uri getCurrentPhotoUri() { 101 return mPhotoUri; 102 } 103 104 @Override onPhotoSelectionDismissed()105 public void onPhotoSelectionDismissed() { 106 } 107 } 108 109 private PhotoListener mPhotoListener; 110 private int mPhotoMode; 111 PhotoHandler(Context context, int photoMode, RawContactDeltaList state)112 public PhotoHandler(Context context, int photoMode, RawContactDeltaList state) { 113 // We pass a null changeAnchorView since we are overriding onClick so that we 114 // can show the photo options in a dialog instead of a ListPopupWindow (which would 115 // be anchored at changeAnchorView). 116 super(context, /* changeAnchorView =*/ null, photoMode, /* isDirectoryContact =*/ false, 117 state); 118 mPhotoListener = new PhotoListener(); 119 mPhotoMode = photoMode; 120 } 121 122 @Override onClick(View view)123 public void onClick(View view) { 124 PhotoSourceDialogFragment.show(CompactContactEditorFragment.this, mPhotoMode); 125 } 126 127 @Override getListener()128 public PhotoActionListener getListener() { 129 return mPhotoListener; 130 } 131 132 @Override startPhotoActivity(Intent intent, int requestCode, Uri photoUri)133 protected void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) { 134 mPhotoUri = photoUri; 135 mStatus = Status.SUB_ACTIVITY; 136 137 CompactContactEditorFragment.this.startActivityForResult(intent, requestCode); 138 } 139 } 140 141 private PhotoHandler mPhotoHandler; 142 private Uri mPhotoUri; 143 private long mPhotoRawContactId; 144 145 @Override onCreate(Bundle savedState)146 public void onCreate(Bundle savedState) { 147 super.onCreate(savedState); 148 149 if (savedState != null) { 150 mPhotoUri = savedState.getParcelable(KEY_PHOTO_URI); 151 mPhotoRawContactId = savedState.getLong(KEY_PHOTO_RAW_CONTACT_ID); 152 } 153 } 154 155 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)156 public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { 157 setHasOptionsMenu(true); 158 159 final View view = inflater.inflate( 160 R.layout.compact_contact_editor_fragment, container, false); 161 mContent = (LinearLayout) view.findViewById(R.id.editors); 162 return view; 163 } 164 165 @Override onSaveInstanceState(Bundle outState)166 public void onSaveInstanceState(Bundle outState) { 167 outState.putParcelable(KEY_PHOTO_URI, mPhotoUri); 168 outState.putLong(KEY_PHOTO_RAW_CONTACT_ID, mPhotoRawContactId); 169 super.onSaveInstanceState(outState); 170 } 171 172 @Override onActivityResult(int requestCode, int resultCode, Intent data)173 public void onActivityResult(int requestCode, int resultCode, Intent data) { 174 if (mStatus == Status.SUB_ACTIVITY) { 175 mStatus = Status.EDITING; 176 } 177 if (mPhotoHandler != null 178 && mPhotoHandler.handlePhotoActivityResult(requestCode, resultCode, data)) { 179 return; 180 } 181 super.onActivityResult(requestCode, resultCode, data); 182 } 183 184 @Override onStop()185 public void onStop() { 186 super.onStop(); 187 188 // If anything was left unsaved, save it now 189 if (!getActivity().isChangingConfigurations() && mStatus == Status.EDITING) { 190 save(SaveMode.RELOAD, /* backPressed =*/ false); 191 } 192 } 193 194 @Override onOptionsItemSelected(MenuItem item)195 public boolean onOptionsItemSelected(MenuItem item) { 196 if (item.getItemId() == android.R.id.home) { 197 return revert(); 198 } 199 return super.onOptionsItemSelected(item); 200 } 201 202 @Override bindEditors()203 protected void bindEditors() { 204 if (!isReadyToBindEditors()) { 205 return; 206 } 207 208 // Add input fields for the loaded Contact 209 final CompactRawContactsEditorView editorView = getContent(); 210 editorView.setListener(this); 211 editorView.setState(mState, getMaterialPalette(), mViewIdGenerator, mPhotoId, mNameId, 212 mReadOnlyDisplayName, mHasNewContact, mIsUserProfile); 213 if (mReadOnlyDisplayName != null) { 214 mReadOnlyNameEditorView = editorView.getDefaultNameEditorView(); 215 } 216 217 // Set up the photo widget 218 mPhotoHandler = createPhotoHandler(); 219 mPhotoRawContactId = editorView.getPhotoRawContactId(); 220 if (mPhotoRawContactId < 0) { 221 // Since the raw contact IDs for new contacts are random negative numbers 222 // we consider any negative key a match 223 for (String key : mUpdatedPhotos.keySet()) { 224 try { 225 if (Integer.parseInt(key) < 0) { 226 editorView.setFullSizePhoto((Uri) mUpdatedPhotos.getParcelable(key)); 227 break; 228 } 229 } catch (NumberFormatException ignored) { 230 } 231 } 232 } else if (mUpdatedPhotos.containsKey(String.valueOf(mPhotoRawContactId))) { 233 editorView.setFullSizePhoto((Uri) mUpdatedPhotos.getParcelable( 234 String.valueOf(mPhotoRawContactId))); 235 } 236 editorView.setPhotoHandler(mPhotoHandler); 237 238 // The editor is ready now so make it visible 239 editorView.setEnabled(isEnabled()); 240 editorView.setVisibility(View.VISIBLE); 241 242 // Refresh the ActionBar as the visibility of the join command 243 // Activity can be null if we have been detached from the Activity. 244 invalidateOptionsMenu(); 245 } 246 isReadyToBindEditors()247 private boolean isReadyToBindEditors() { 248 if (mState.isEmpty()) { 249 if (Log.isLoggable(TAG, Log.VERBOSE)) { 250 Log.v(TAG, "No data to bind editors"); 251 } 252 return false; 253 } 254 if (mIsEdit && !mExistingContactDataReady) { 255 if (Log.isLoggable(TAG, Log.VERBOSE)) { 256 Log.v(TAG, "Existing contact data is not ready to bind editors."); 257 } 258 return false; 259 } 260 if (mHasNewContact && !mNewContactDataReady) { 261 if (Log.isLoggable(TAG, Log.VERBOSE)) { 262 Log.v(TAG, "New contact data is not ready to bind editors."); 263 } 264 return false; 265 } 266 return true; 267 } 268 createPhotoHandler()269 private PhotoHandler createPhotoHandler() { 270 return new PhotoHandler(getActivity(), getPhotoMode(), mState); 271 } 272 getPhotoMode()273 private int getPhotoMode() { 274 // To determine the options that are available to the user to update their photo 275 // (i.e. the photo mode), check if any of the writable raw contacts has a photo set 276 Integer photoMode = null; 277 boolean hasWritableAccountType = false; 278 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext); 279 for (RawContactDelta rawContactDelta : mState) { 280 if (!rawContactDelta.isVisible()) { 281 continue; 282 } 283 final AccountType accountType = rawContactDelta.getAccountType(accountTypes); 284 if (accountType.areContactsWritable()) { 285 hasWritableAccountType = true; 286 if (getContent().isWritablePhotoSet()) { 287 photoMode = PhotoActionPopup.Modes.MULTIPLE_WRITE_ABLE_PHOTOS; 288 break; 289 } 290 } 291 } 292 // If the mode was not set, base it on whether we saw a writable contact or not 293 if (photoMode == null) { 294 photoMode = hasWritableAccountType 295 ? PhotoActionPopup.Modes.NO_PHOTO : PhotoActionPopup.Modes.READ_ONLY_PHOTO; 296 } 297 return photoMode; 298 } 299 300 @Override getAggregationAnchorView(long rawContactId)301 protected View getAggregationAnchorView(long rawContactId) { 302 return getContent().getAggregationAnchorView(); 303 } 304 305 @Override setGroupMetaData()306 protected void setGroupMetaData() { 307 // The compact editor does not support groups. 308 } 309 310 @Override doSaveAction(int saveMode, boolean backPressed)311 protected boolean doSaveAction(int saveMode, boolean backPressed) { 312 // Save contact. No need to pass the palette since we are finished editing after the save. 313 final Intent intent = ContactSaveService.createSaveContactIntent(mContext, mState, 314 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(), 315 ((Activity) mContext).getClass(), 316 CompactContactEditorActivity.ACTION_SAVE_COMPLETED, mUpdatedPhotos, backPressed); 317 mContext.startService(intent); 318 319 return true; 320 } 321 322 @Override joinAggregate(final long contactId)323 protected void joinAggregate(final long contactId) { 324 final Intent intent = ContactSaveService.createJoinContactsIntent( 325 mContext, mContactIdForJoin, contactId, CompactContactEditorActivity.class, 326 CompactContactEditorActivity.ACTION_JOIN_COMPLETED); 327 mContext.startService(intent); 328 } 329 330 @Override onRemovePictureChosen()331 public void onRemovePictureChosen() { 332 if (mPhotoHandler != null) { 333 mPhotoHandler.getListener().onRemovePictureChosen(); 334 } 335 } 336 337 @Override onTakePhotoChosen()338 public void onTakePhotoChosen() { 339 if (mPhotoHandler != null) { 340 mPhotoHandler.getListener().onTakePhotoChosen(); 341 } 342 } 343 344 @Override onPickFromGalleryChosen()345 public void onPickFromGalleryChosen() { 346 if (mPhotoHandler != null) { 347 mPhotoHandler.getListener().onPickFromGalleryChosen(); 348 } 349 } 350 351 @Override onExpandEditor()352 public void onExpandEditor() { 353 // Determine if this is an insert (new contact) or edit 354 final boolean isInsert = isInsert(getActivity().getIntent()); 355 356 if (isInsert) { 357 // For inserts, prevent any changes from being saved when the base fragment is destroyed 358 mStatus = Status.CLOSING; 359 } else if (hasPendingRawContactChanges()) { 360 // Save whatever is in the form 361 save(SaveMode.CLOSE, /* backPressed =*/ false); 362 } 363 364 // Prepare an Intent to start the expanded editor 365 final Intent intent = isInsert 366 ? EditorIntents.createInsertContactIntent( 367 mState, getDisplayName(), getPhoneticName(), mUpdatedPhotos) 368 : EditorIntents.createEditContactIntent(mLookupUri, getMaterialPalette(), 369 mPhotoId, mNameId); 370 ImplicitIntentsUtil.startActivityInApp(getActivity(), intent); 371 372 getActivity().finish(); 373 } 374 375 @Override onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta)376 public void onNameFieldChanged(long rawContactId, ValuesDelta valuesDelta) { 377 final Activity activity = getActivity(); 378 if (activity == null || activity.isFinishing()) { 379 return; 380 } 381 if (!mIsUserProfile) { 382 acquireAggregationSuggestions(activity, rawContactId, valuesDelta); 383 } 384 } 385 386 @Override getDisplayName()387 public String getDisplayName() { 388 final StructuredNameEditorView structuredNameEditorView = 389 getContent().getStructuredNameEditorView(); 390 return structuredNameEditorView == null 391 ? null : structuredNameEditorView.getDisplayName(); 392 } 393 394 @Override getPhoneticName()395 public String getPhoneticName() { 396 final PhoneticNameEditorView phoneticNameEditorView = 397 getContent().getFirstPhoneticNameEditorView(); 398 return phoneticNameEditorView == null 399 ? null : phoneticNameEditorView.getPhoneticName(); 400 } 401 getContent()402 private CompactRawContactsEditorView getContent() { 403 return (CompactRawContactsEditorView) mContent; 404 } 405 } 406