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          */
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         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
onPause()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
onNewIntent(Intent intent)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
onCreateDialog(int id, Bundle args)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
getDialogManager()433     public DialogManager getDialogManager() {
434         return mDialogManager;
435     }
436 
437     @Override
onSaveInstanceState(Bundle outState)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
onActivityResult(int requestCode, int resultCode, Intent data)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
onBackPressed()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      */
changePhoto(int photoMode)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 
getToolbar()478     public Toolbar getToolbar() {
479         return mToolbar;
480     }
481 
482     @Override
onRemovePictureChosen()483     public void onRemovePictureChosen() {
484         getPhotoSelectionHandler().getListener().onRemovePictureChosen();
485     }
486 
487     @Override
onTakePhotoChosen()488     public void onTakePhotoChosen() {
489         getPhotoSelectionHandler().getListener().onTakePhotoChosen();
490     }
491 
492     @Override
onPickFromGalleryChosen()493     public void onPickFromGalleryChosen() {
494         getPhotoSelectionHandler().getListener().onPickFromGalleryChosen();
495     }
496 
getPhotoSelectionHandler()497     private PhotoSelectionHandler getPhotoSelectionHandler() {
498         if (mPhotoSelectionHandler == null) {
499             mPhotoSelectionHandler = new EditorPhotoSelectionHandler(mPhotoMode);
500         }
501         return mPhotoSelectionHandler;
502     }
503 
getEditorFragment()504     private ContactEditorFragment getEditorFragment() {
505         return (ContactEditorFragment) mFragment;
506     }
507 }
508