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.activities; 18 19 import android.app.Dialog; 20 import android.app.FragmentTransaction; 21 import android.content.ComponentName; 22 import android.content.ContentValues; 23 import android.content.Intent; 24 import android.net.Uri; 25 import android.os.Bundle; 26 import android.provider.ContactsContract.QuickContact; 27 import android.support.v7.widget.Toolbar; 28 import android.util.Log; 29 import android.view.View; 30 import android.view.inputmethod.InputMethodManager; 31 32 import com.android.contacts.AppCompatContactsActivity; 33 import com.android.contacts.ContactSaveService; 34 import com.android.contacts.DynamicShortcuts; 35 import com.android.contacts.R; 36 import com.android.contacts.detail.PhotoSelectionHandler; 37 import com.android.contacts.editor.ContactEditorFragment; 38 import com.android.contacts.editor.EditorIntents; 39 import com.android.contacts.editor.PhotoSourceDialogFragment; 40 import com.android.contacts.interactions.ContactDeletionInteraction; 41 import com.android.contacts.model.RawContactDeltaList; 42 import com.android.contacts.util.DialogManager; 43 import com.android.contacts.util.ImplicitIntentsUtil; 44 45 import java.io.FileNotFoundException; 46 import java.util.ArrayList; 47 48 /** 49 * Contact editor with only the most important fields displayed initially. 50 */ 51 public class ContactEditorActivity extends AppCompatContactsActivity implements 52 PhotoSourceDialogFragment.Listener, 53 DialogManager.DialogShowingViewActivity { 54 private static final String TAG = "ContactEditorActivity"; 55 56 public static final String ACTION_JOIN_COMPLETED = "joinCompleted"; 57 public static final String ACTION_SAVE_COMPLETED = "saveCompleted"; 58 59 public static final int RESULT_CODE_SPLIT = 2; 60 // 3 used for ContactDeletionInteraction.RESULT_CODE_DELETED 61 public static final int RESULT_CODE_EDITED = 4; 62 63 /** 64 * The contact will be saved to this account when this is set for an insert. This 65 * is necessary because {@link android.accounts.Account} cannot be created with null values 66 * for the name and type and an Account is needed for 67 * {@link android.provider.ContactsContract.Intents.Insert#EXTRA_ACCOUNT} 68 */ 69 public static final String EXTRA_ACCOUNT_WITH_DATA_SET = 70 "com.android.contacts.ACCOUNT_WITH_DATA_SET"; 71 72 private static final String TAG_EDITOR_FRAGMENT = "editor_fragment"; 73 74 private static final String STATE_PHOTO_MODE = "photo_mode"; 75 private static final String STATE_ACTION_BAR_TITLE = "action_bar_title"; 76 private static final String STATE_PHOTO_URI = "photo_uri"; 77 78 /** 79 * Boolean intent key that specifies that this activity should finish itself 80 * (instead of launching a new view intent) after the editor changes have been 81 * saved. 82 */ 83 public static final String INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED = 84 "finishActivityOnSaveCompleted"; 85 86 /** 87 * Contract for contact editors Fragments that are managed by this Activity. 88 */ 89 public interface ContactEditor { 90 91 /** 92 * Modes that specify what the AsyncTask has to perform after saving 93 */ 94 interface SaveMode { 95 /** 96 * Close the editor after saving 97 */ 98 int CLOSE = 0; 99 100 /** 101 * Reload the data so that the user can continue editing 102 */ 103 int RELOAD = 1; 104 105 /** 106 * Split the contact after saving 107 */ 108 int SPLIT = 2; 109 110 /** 111 * Join another contact after saving 112 */ 113 int JOIN = 3; 114 115 /** 116 * Navigate to the editor view after saving. 117 */ 118 int EDITOR = 4; 119 } 120 121 /** 122 * The status of the contact editor. 123 */ 124 interface Status { 125 /** 126 * The loader is fetching data 127 */ 128 int LOADING = 0; 129 130 /** 131 * Not currently busy. We are waiting for the user to enter data 132 */ 133 int EDITING = 1; 134 135 /** 136 * The data is currently being saved. This is used to prevent more 137 * auto-saves (they shouldn't overlap) 138 */ 139 int SAVING = 2; 140 141 /** 142 * Prevents any more saves. This is used if in the following cases: 143 * - After Save/Close 144 * - After Revert 145 * - After the user has accepted an edit suggestion 146 * - After the user chooses to expand the editor 147 */ 148 int CLOSING = 3; 149 150 /** 151 * Prevents saving while running a child activity. 152 */ 153 int SUB_ACTIVITY = 4; 154 } 155 156 /** 157 * Sets the hosting Activity that will receive callbacks from the contact editor. 158 */ 159 void setListener(ContactEditorFragment.Listener listener); 160 161 /** 162 * Initialize the contact editor. 163 */ 164 void load(String action, Uri lookupUri, Bundle intentExtras); 165 166 /** 167 * Applies extras from the hosting Activity to the writable raw contact. 168 */ 169 void setIntentExtras(Bundle extras); 170 171 /** 172 * Saves or creates the contact based on the mode, and if successful 173 * finishes the activity. 174 */ 175 boolean save(int saveMode); 176 177 /** 178 * If there are no unsaved changes, just close the editor, otherwise the user is prompted 179 * before discarding unsaved changes. 180 */ 181 boolean revert(); 182 183 /** 184 * Invoked after the contact is saved. 185 */ 186 void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 187 Uri contactLookupUri, Long joinContactId); 188 189 /** 190 * Invoked after the contact is joined. 191 */ 192 void onJoinCompleted(Uri uri); 193 } 194 195 /** 196 * Displays a PopupWindow with photo edit options. 197 */ 198 private final class EditorPhotoSelectionHandler extends PhotoSelectionHandler { 199 200 /** 201 * Receiver of photo edit option callbacks. 202 */ 203 private final class EditorPhotoActionListener extends PhotoActionListener { 204 205 @Override 206 public void onRemovePictureChosen() { 207 getEditorFragment().removePhoto(); 208 } 209 210 @Override 211 public void onPhotoSelected(Uri uri) throws FileNotFoundException { 212 mPhotoUri = uri; 213 getEditorFragment().updatePhoto(uri); 214 215 // Re-create the photo handler the next time we need it so that additional photo 216 // selections create a new temp file (and don't hit the one that was just added 217 // to the cache). 218 mPhotoSelectionHandler = null; 219 } 220 221 @Override 222 public Uri getCurrentPhotoUri() { 223 return mPhotoUri; 224 } 225 226 @Override 227 public void onPhotoSelectionDismissed() { 228 } 229 } 230 231 private final EditorPhotoActionListener mPhotoActionListener; 232 233 public EditorPhotoSelectionHandler(int photoMode) { 234 // We pass a null changeAnchorView since we are overriding onClick so that we 235 // can show the photo options in a dialog instead of a ListPopupWindow (which would 236 // be anchored at changeAnchorView). 237 238 // TODO: empty raw contact delta list 239 super(ContactEditorActivity.this, /* changeAnchorView =*/ null, photoMode, 240 /* isDirectoryContact =*/ false, new RawContactDeltaList()); 241 mPhotoActionListener = new EditorPhotoActionListener(); 242 } 243 244 @Override 245 public PhotoActionListener getListener() { 246 return mPhotoActionListener; 247 } 248 249 @Override 250 protected void startPhotoActivity(Intent intent, int requestCode, Uri photoUri) { 251 mPhotoUri = photoUri; 252 startActivityForResult(intent, requestCode); 253 } 254 } 255 256 private int mActionBarTitleResId; 257 private ContactEditor mFragment; 258 private Toolbar mToolbar; 259 private boolean mFinishActivityOnSaveCompleted; 260 private DialogManager mDialogManager = new DialogManager(this); 261 262 private EditorPhotoSelectionHandler mPhotoSelectionHandler; 263 private Uri mPhotoUri; 264 private int mPhotoMode; 265 266 private final ContactEditorFragment.Listener mFragmentListener = 267 new ContactEditorFragment.Listener() { 268 269 @Override 270 public void onDeleteRequested(Uri contactUri) { 271 ContactDeletionInteraction.start( 272 ContactEditorActivity.this, contactUri, true); 273 } 274 275 @Override 276 public void onReverted() { 277 finish(); 278 } 279 280 @Override 281 public void onSaveFinished(Intent resultIntent) { 282 if (mFinishActivityOnSaveCompleted) { 283 setResult(resultIntent == null ? RESULT_CANCELED : RESULT_OK, resultIntent); 284 } else if (resultIntent != null) { 285 ImplicitIntentsUtil.startActivityInApp( 286 ContactEditorActivity.this, resultIntent); 287 } 288 finish(); 289 } 290 291 @Override 292 public void onContactSplit(Uri newLookupUri) { 293 setResult(RESULT_CODE_SPLIT, /* data */ null); 294 finish(); 295 } 296 297 @Override 298 public void onContactNotFound() { 299 finish(); 300 } 301 302 @Override 303 public void onEditOtherRawContactRequested( 304 Uri contactLookupUri, long rawContactId, ArrayList<ContentValues> values) { 305 final Intent intent = EditorIntents.createEditOtherRawContactIntent( 306 ContactEditorActivity.this, contactLookupUri, rawContactId, values); 307 ImplicitIntentsUtil.startActivityInApp( 308 ContactEditorActivity.this, intent); 309 finish(); 310 } 311 }; 312 313 @Override 314 public void onCreate(Bundle savedState) { 315 super.onCreate(savedState); 316 317 RequestPermissionsActivity.startPermissionActivityIfNeeded(this); 318 319 final Intent intent = getIntent(); 320 final String action = intent.getAction(); 321 322 // Update the component name of our intent to be this class to clear out any activity 323 // aliases. Otherwise ContactSaveService won't notify this activity once a save is finished. 324 // See b/34154706 for more info. 325 intent.setComponent(new ComponentName(this, ContactEditorActivity.class)); 326 327 // Determine whether or not this activity should be finished after the user is done 328 // editing the contact or if this activity should launch another activity to view the 329 // contact's details. 330 mFinishActivityOnSaveCompleted = intent.getBooleanExtra( 331 INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, false); 332 333 // The only situation where action could be ACTION_JOIN_COMPLETED is if the 334 // user joined the contact with another and closed the activity before 335 // the save operation was completed. The activity should remain closed then. 336 if (ACTION_JOIN_COMPLETED.equals(action)) { 337 finish(); 338 return; 339 } 340 341 if (ACTION_SAVE_COMPLETED.equals(action)) { 342 finish(); 343 return; 344 } 345 346 setContentView(R.layout.contact_editor_activity); 347 mToolbar = (Toolbar) findViewById(R.id.toolbar); 348 setSupportActionBar(mToolbar); 349 if (Intent.ACTION_EDIT.equals(action)) { 350 mActionBarTitleResId = R.string.contact_editor_title_existing_contact; 351 } else { 352 mActionBarTitleResId = R.string.contact_editor_title_new_contact; 353 } 354 mToolbar.setTitle(mActionBarTitleResId); 355 // Set activity title for Talkback 356 setTitle(mActionBarTitleResId); 357 358 if (savedState == null) { 359 // Create the editor and photo selection fragments 360 mFragment = new ContactEditorFragment(); 361 getFragmentManager().beginTransaction() 362 .add(R.id.fragment_container, getEditorFragment(), TAG_EDITOR_FRAGMENT) 363 .commit(); 364 } else { 365 // Restore state 366 mPhotoMode = savedState.getInt(STATE_PHOTO_MODE); 367 mActionBarTitleResId = savedState.getInt(STATE_ACTION_BAR_TITLE); 368 mPhotoUri = Uri.parse(savedState.getString(STATE_PHOTO_URI)); 369 370 // Show/hide the editor and photo selection fragments (w/o animations) 371 mFragment = (ContactEditorFragment) getFragmentManager() 372 .findFragmentByTag(TAG_EDITOR_FRAGMENT); 373 final FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); 374 fragmentTransaction.show(getEditorFragment()).commit(); 375 mToolbar.setTitle(mActionBarTitleResId); 376 } 377 378 // Set listeners 379 mFragment.setListener(mFragmentListener); 380 381 // Load editor data (even if it's hidden) 382 final Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null; 383 mFragment.load(action, uri, getIntent().getExtras()); 384 385 if (Intent.ACTION_INSERT.equals(action)) { 386 DynamicShortcuts.reportShortcutUsed(this, DynamicShortcuts.SHORTCUT_ADD_CONTACT); 387 } 388 } 389 390 @Override 391 protected void onPause() { 392 super.onPause(); 393 final InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 394 final View currentFocus = getCurrentFocus(); 395 if (imm != null && currentFocus != null) { 396 imm.hideSoftInputFromWindow(currentFocus.getWindowToken(), 0); 397 } 398 } 399 400 @Override 401 protected void onNewIntent(Intent intent) { 402 super.onNewIntent(intent); 403 404 if (mFragment == null) { 405 return; 406 } 407 408 final String action = intent.getAction(); 409 if (Intent.ACTION_EDIT.equals(action)) { 410 mFragment.setIntentExtras(intent.getExtras()); 411 } else if (ACTION_SAVE_COMPLETED.equals(action)) { 412 mFragment.onSaveCompleted(true, 413 intent.getIntExtra(ContactEditorFragment.SAVE_MODE_EXTRA_KEY, 414 ContactEditor.SaveMode.CLOSE), 415 intent.getBooleanExtra(ContactSaveService.EXTRA_SAVE_SUCCEEDED, false), 416 intent.getData(), 417 intent.getLongExtra(ContactEditorFragment.JOIN_CONTACT_ID_EXTRA_KEY, -1)); 418 } else if (ACTION_JOIN_COMPLETED.equals(action)) { 419 mFragment.onJoinCompleted(intent.getData()); 420 } 421 } 422 423 @Override 424 protected Dialog onCreateDialog(int id, Bundle args) { 425 if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args); 426 427 // Nobody knows about the Dialog 428 Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args); 429 return null; 430 } 431 432 @Override 433 public DialogManager getDialogManager() { 434 return mDialogManager; 435 } 436 437 @Override 438 protected void onSaveInstanceState(Bundle outState) { 439 super.onSaveInstanceState(outState); 440 outState.putInt(STATE_PHOTO_MODE, mPhotoMode); 441 outState.putInt(STATE_ACTION_BAR_TITLE, mActionBarTitleResId); 442 outState.putString(STATE_PHOTO_URI, 443 mPhotoUri != null ? mPhotoUri.toString() : Uri.EMPTY.toString()); 444 } 445 446 @Override 447 public void onActivityResult(int requestCode, int resultCode, Intent data) { 448 if (mPhotoSelectionHandler == null) { 449 mPhotoSelectionHandler = (EditorPhotoSelectionHandler) getPhotoSelectionHandler(); 450 } 451 if (mPhotoSelectionHandler.handlePhotoActivityResult(requestCode, resultCode, data)) { 452 return; 453 } 454 super.onActivityResult(requestCode, resultCode, data); 455 } 456 457 @Override 458 public void onBackPressed() { 459 if (mFragment != null) { 460 mFragment.revert(); 461 } 462 } 463 464 /** 465 * Opens a dialog showing options for the user to change their photo (take, choose, or remove 466 * photo). 467 */ 468 public void changePhoto(int photoMode) { 469 mPhotoMode = photoMode; 470 // This method is called from an onClick handler in the PhotoEditorView. It's possible for 471 // onClick methods to run after onSaveInstanceState is called for the activity, so check 472 // if it's safe to commit transactions before trying. 473 if (isSafeToCommitTransactions()) { 474 PhotoSourceDialogFragment.show(this, mPhotoMode); 475 } 476 } 477 478 public Toolbar getToolbar() { 479 return mToolbar; 480 } 481 482 @Override 483 public void onRemovePictureChosen() { 484 getPhotoSelectionHandler().getListener().onRemovePictureChosen(); 485 } 486 487 @Override 488 public void onTakePhotoChosen() { 489 getPhotoSelectionHandler().getListener().onTakePhotoChosen(); 490 } 491 492 @Override 493 public void onPickFromGalleryChosen() { 494 getPhotoSelectionHandler().getListener().onPickFromGalleryChosen(); 495 } 496 497 private PhotoSelectionHandler getPhotoSelectionHandler() { 498 if (mPhotoSelectionHandler == null) { 499 mPhotoSelectionHandler = new EditorPhotoSelectionHandler(mPhotoMode); 500 } 501 return mPhotoSelectionHandler; 502 } 503 504 private ContactEditorFragment getEditorFragment() { 505 return (ContactEditorFragment) mFragment; 506 } 507 } 508