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         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         mFragment =
359             (ContactEditor) getFragmentManager().findFragmentById(R.id.contact_editor_fragment);
360 
361         if (savedState != null) {
362             // Restore state
363             mPhotoMode = savedState.getInt(STATE_PHOTO_MODE);
364             mActionBarTitleResId = savedState.getInt(STATE_ACTION_BAR_TITLE);
365             mPhotoUri = Uri.parse(savedState.getString(STATE_PHOTO_URI));
366 
367             mToolbar.setTitle(mActionBarTitleResId);
368         }
369 
370         // Set listeners
371         mFragment.setListener(mFragmentListener);
372 
373         // Load editor data (even if it's hidden)
374         final Uri uri = Intent.ACTION_EDIT.equals(action) ? getIntent().getData() : null;
375         mFragment.load(action, uri, getIntent().getExtras());
376 
377         if (Intent.ACTION_INSERT.equals(action)) {
378             DynamicShortcuts.reportShortcutUsed(this, DynamicShortcuts.SHORTCUT_ADD_CONTACT);
379         }
380     }
381 
382     @Override
onNewIntent(Intent intent)383     protected void onNewIntent(Intent intent) {
384         super.onNewIntent(intent);
385 
386         if (mFragment == null) {
387             return;
388         }
389 
390         final String action = intent.getAction();
391         if (Intent.ACTION_EDIT.equals(action)) {
392             mFragment.setIntentExtras(intent.getExtras());
393         } else if (ACTION_SAVE_COMPLETED.equals(action)) {
394             mFragment.onSaveCompleted(true,
395                     intent.getIntExtra(ContactEditorFragment.SAVE_MODE_EXTRA_KEY,
396                             ContactEditor.SaveMode.CLOSE),
397                     intent.getBooleanExtra(ContactSaveService.EXTRA_SAVE_SUCCEEDED, false),
398                     intent.getData(),
399                     intent.getLongExtra(ContactEditorFragment.JOIN_CONTACT_ID_EXTRA_KEY, -1));
400         } else if (ACTION_JOIN_COMPLETED.equals(action)) {
401             mFragment.onJoinCompleted(intent.getData());
402         }
403     }
404 
405     @Override
onCreateDialog(int id, Bundle args)406     protected Dialog onCreateDialog(int id, Bundle args) {
407         if (DialogManager.isManagedId(id)) return mDialogManager.onCreateDialog(id, args);
408 
409         // Nobody knows about the Dialog
410         Log.w(TAG, "Unknown dialog requested, id: " + id + ", args: " + args);
411         return null;
412     }
413 
414     @Override
getDialogManager()415     public DialogManager getDialogManager() {
416         return mDialogManager;
417     }
418 
419     @Override
onSaveInstanceState(Bundle outState)420     protected void onSaveInstanceState(Bundle outState) {
421         super.onSaveInstanceState(outState);
422         outState.putInt(STATE_PHOTO_MODE, mPhotoMode);
423         outState.putInt(STATE_ACTION_BAR_TITLE, mActionBarTitleResId);
424         outState.putString(STATE_PHOTO_URI,
425                 mPhotoUri != null ? mPhotoUri.toString() : Uri.EMPTY.toString());
426     }
427 
428     @Override
onActivityResult(int requestCode, int resultCode, Intent data)429     public void onActivityResult(int requestCode, int resultCode, Intent data) {
430         if (mPhotoSelectionHandler == null) {
431             mPhotoSelectionHandler = (EditorPhotoSelectionHandler) getPhotoSelectionHandler();
432         }
433         if (mPhotoSelectionHandler.handlePhotoActivityResult(requestCode, resultCode, data)) {
434             return;
435         }
436         super.onActivityResult(requestCode, resultCode, data);
437     }
438 
439     @Override
onBackPressed()440     public void onBackPressed() {
441         if (mFragment != null) {
442             mFragment.revert();
443         }
444     }
445 
446     /**
447      * Opens a dialog showing options for the user to change their photo (take, choose, or remove
448      * photo).
449      */
changePhoto(int photoMode)450     public void changePhoto(int photoMode) {
451         mPhotoMode = photoMode;
452         // This method is called from an onClick handler in the PhotoEditorView. It's possible for
453         // onClick methods to run after onSaveInstanceState is called for the activity, so check
454         // if it's safe to commit transactions before trying.
455         if (isSafeToCommitTransactions()) {
456             PhotoSourceDialogFragment.show(this, mPhotoMode);
457         }
458     }
459 
getToolbar()460     public Toolbar getToolbar() {
461         return mToolbar;
462     }
463 
464     @Override
onRemovePictureChosen()465     public void onRemovePictureChosen() {
466         getPhotoSelectionHandler().getListener().onRemovePictureChosen();
467     }
468 
469     @Override
onTakePhotoChosen()470     public void onTakePhotoChosen() {
471         getPhotoSelectionHandler().getListener().onTakePhotoChosen();
472     }
473 
474     @Override
onPickFromGalleryChosen()475     public void onPickFromGalleryChosen() {
476         getPhotoSelectionHandler().getListener().onPickFromGalleryChosen();
477     }
478 
getPhotoSelectionHandler()479     private PhotoSelectionHandler getPhotoSelectionHandler() {
480         if (mPhotoSelectionHandler == null) {
481             mPhotoSelectionHandler = new EditorPhotoSelectionHandler(mPhotoMode);
482         }
483         return mPhotoSelectionHandler;
484     }
485 
getEditorFragment()486     private ContactEditorFragment getEditorFragment() {
487         return (ContactEditorFragment) mFragment;
488     }
489 }
490