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 androidx.appcompat.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 */ setListener(ContactEditorFragment.Listener listener)159 void setListener(ContactEditorFragment.Listener listener); 160 161 /** 162 * Initialize the contact editor. 163 */ load(String action, Uri lookupUri, Bundle intentExtras)164 void load(String action, Uri lookupUri, Bundle intentExtras); 165 166 /** 167 * Applies extras from the hosting Activity to the writable raw contact. 168 */ setIntentExtras(Bundle extras)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 */ save(int saveMode)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 */ revert()181 boolean revert(); 182 183 /** 184 * Invoked after the contact is saved. 185 */ onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, Uri contactLookupUri, Long joinContactId)186 void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded, 187 Uri contactLookupUri, Long joinContactId); 188 189 /** 190 * Invoked after the contact is joined. 191 */ onJoinCompleted(Uri uri)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 onRemovePictureChosen()206 public void onRemovePictureChosen() { 207 getEditorFragment().removePhoto(); 208 } 209 210 @Override onPhotoSelected(Uri uri)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 getCurrentPhotoUri()222 public Uri getCurrentPhotoUri() { 223 return mPhotoUri; 224 } 225 226 @Override onPhotoSelectionDismissed()227 public void onPhotoSelectionDismissed() { 228 } 229 } 230 231 private final EditorPhotoActionListener mPhotoActionListener; 232 EditorPhotoSelectionHandler(int photoMode)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 getListener()245 public PhotoActionListener getListener() { 246 return mPhotoActionListener; 247 } 248 249 @Override startPhotoActivity(Intent intent, int requestCode, Uri photoUri)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 onCreate(Bundle savedState)314 public void onCreate(Bundle savedState) { 315 super.onCreate(savedState); 316 317 getWindow().setHideOverlayWindows(true); 318 319 RequestPermissionsActivity.startPermissionActivityIfNeeded(this); 320 321 final Intent intent = getIntent(); 322 final String action = intent.getAction(); 323 324 // Update the component name of our intent to be this class to clear out any activity 325 // aliases. Otherwise ContactSaveService won't notify this activity once a save is finished. 326 // See b/34154706 for more info. 327 intent.setComponent(new ComponentName(this, ContactEditorActivity.class)); 328 329 // Determine whether or not this activity should be finished after the user is done 330 // editing the contact or if this activity should launch another activity to view the 331 // contact's details. 332 mFinishActivityOnSaveCompleted = intent.getBooleanExtra( 333 INTENT_KEY_FINISH_ACTIVITY_ON_SAVE_COMPLETED, false); 334 335 // The only situation where action could be ACTION_JOIN_COMPLETED is if the 336 // user joined the contact with another and closed the activity before 337 // the save operation was completed. The activity should remain closed then. 338 if (ACTION_JOIN_COMPLETED.equals(action)) { 339 finish(); 340 return; 341 } 342 343 if (ACTION_SAVE_COMPLETED.equals(action)) { 344 finish(); 345 return; 346 } 347 348 setContentView(R.layout.contact_editor_activity); 349 mToolbar = (Toolbar) findViewById(R.id.toolbar); 350 setSupportActionBar(mToolbar); 351 if (Intent.ACTION_EDIT.equals(action)) { 352 mActionBarTitleResId = R.string.contact_editor_title_existing_contact; 353 } else { 354 mActionBarTitleResId = R.string.contact_editor_title_new_contact; 355 } 356 mToolbar.setTitle(mActionBarTitleResId); 357 // Set activity title for Talkback 358 setTitle(mActionBarTitleResId); 359 360 mFragment = 361 (ContactEditor) getFragmentManager().findFragmentById(R.id.contact_editor_fragment); 362 363 if (savedState != null) { 364 // Restore state 365 mPhotoMode = savedState.getInt(STATE_PHOTO_MODE); 366 mActionBarTitleResId = savedState.getInt(STATE_ACTION_BAR_TITLE); 367 mPhotoUri = Uri.parse(savedState.getString(STATE_PHOTO_URI)); 368 369 mToolbar.setTitle(mActionBarTitleResId); 370 } 371 372 // Set listeners 373 mFragment.setListener(mFragmentListener); 374 375 // Load editor data (even if it's hidden) 376 final Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null; 377 mFragment.load(action, uri, getIntent().getExtras()); 378 379 if (Intent.ACTION_INSERT.equals(action)) { 380 DynamicShortcuts.reportShortcutUsed(this, DynamicShortcuts.SHORTCUT_ADD_CONTACT); 381 } 382 } 383 384 @Override onNewIntent(Intent intent)385 protected void onNewIntent(Intent intent) { 386 super.onNewIntent(intent); 387 388 if (mFragment == null) { 389 return; 390 } 391 392 final String action = intent.getAction(); 393 if (Intent.ACTION_EDIT.equals(action)) { 394 mFragment.setIntentExtras(intent.getExtras()); 395 } else if (ACTION_SAVE_COMPLETED.equals(action)) { 396 mFragment.onSaveCompleted(true, 397 intent.getIntExtra(ContactEditorFragment.SAVE_MODE_EXTRA_KEY, 398 ContactEditor.SaveMode.CLOSE), 399 intent.getBooleanExtra(ContactSaveService.EXTRA_SAVE_SUCCEEDED, false), 400 intent.getData(), 401 intent.getLongExtra(ContactEditorFragment.JOIN_CONTACT_ID_EXTRA_KEY, -1)); 402 } else if (ACTION_JOIN_COMPLETED.equals(action)) { 403 mFragment.onJoinCompleted(intent.getData()); 404 } 405 } 406 407 @Override onCreateDialog(int id, Bundle args)408 protected Dialog onCreateDialog(int id, Bundle args) { 409 if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args); 410 411 // Nobody knows about the Dialog 412 Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args); 413 return null; 414 } 415 416 @Override getDialogManager()417 public DialogManager getDialogManager() { 418 return mDialogManager; 419 } 420 421 @Override onSaveInstanceState(Bundle outState)422 protected void onSaveInstanceState(Bundle outState) { 423 super.onSaveInstanceState(outState); 424 outState.putInt(STATE_PHOTO_MODE, mPhotoMode); 425 outState.putInt(STATE_ACTION_BAR_TITLE, mActionBarTitleResId); 426 outState.putString(STATE_PHOTO_URI, 427 mPhotoUri != null ? mPhotoUri.toString() : Uri.EMPTY.toString()); 428 } 429 430 @Override onActivityResult(int requestCode, int resultCode, Intent data)431 public void onActivityResult(int requestCode, int resultCode, Intent data) { 432 if (mPhotoSelectionHandler == null) { 433 mPhotoSelectionHandler = (EditorPhotoSelectionHandler) getPhotoSelectionHandler(); 434 } 435 if (mPhotoSelectionHandler.handlePhotoActivityResult(requestCode, resultCode, data)) { 436 return; 437 } 438 super.onActivityResult(requestCode, resultCode, data); 439 } 440 441 @Override onBackPressed()442 public void onBackPressed() { 443 if (mFragment != null) { 444 mFragment.revert(); 445 } 446 } 447 448 /** 449 * Opens a dialog showing options for the user to change their photo (take, choose, or remove 450 * photo). 451 */ changePhoto(int photoMode)452 public void changePhoto(int photoMode) { 453 mPhotoMode = photoMode; 454 // This method is called from an onClick handler in the PhotoEditorView. It's possible for 455 // onClick methods to run after onSaveInstanceState is called for the activity, so check 456 // if it's safe to commit transactions before trying. 457 if (isSafeToCommitTransactions()) { 458 PhotoSourceDialogFragment.show(this, mPhotoMode); 459 } 460 } 461 getToolbar()462 public Toolbar getToolbar() { 463 return mToolbar; 464 } 465 466 @Override onRemovePictureChosen()467 public void onRemovePictureChosen() { 468 getPhotoSelectionHandler().getListener().onRemovePictureChosen(); 469 } 470 471 @Override onTakePhotoChosen()472 public void onTakePhotoChosen() { 473 getPhotoSelectionHandler().getListener().onTakePhotoChosen(); 474 } 475 476 @Override onPickFromGalleryChosen()477 public void onPickFromGalleryChosen() { 478 getPhotoSelectionHandler().getListener().onPickFromGalleryChosen(); 479 } 480 getPhotoSelectionHandler()481 private PhotoSelectionHandler getPhotoSelectionHandler() { 482 if (mPhotoSelectionHandler == null) { 483 mPhotoSelectionHandler = new EditorPhotoSelectionHandler(mPhotoMode); 484 } 485 return mPhotoSelectionHandler; 486 } 487 getEditorFragment()488 private ContactEditorFragment getEditorFragment() { 489 return (ContactEditorFragment) mFragment; 490 } 491 } 492