• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 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.detail;
18 
19 import android.app.Activity;
20 import android.app.Fragment;
21 import android.app.LoaderManager;
22 import android.app.LoaderManager.LoaderCallbacks;
23 import android.content.ActivityNotFoundException;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.Loader;
27 import android.media.RingtoneManager;
28 import android.net.Uri;
29 import android.os.Bundle;
30 import android.provider.ContactsContract;
31 import android.provider.ContactsContract.Contacts;
32 import android.util.Log;
33 import android.view.KeyEvent;
34 import android.view.LayoutInflater;
35 import android.view.Menu;
36 import android.view.MenuInflater;
37 import android.view.MenuItem;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.Toast;
41 
42 import com.android.contacts.ContactSaveService;
43 import com.android.contacts.R;
44 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
45 import com.android.contacts.common.list.ShortcutIntentBuilder;
46 import com.android.contacts.common.list.ShortcutIntentBuilder.OnShortcutIntentCreatedListener;
47 import com.android.contacts.model.Contact;
48 import com.android.contacts.model.ContactLoader;
49 import com.android.contacts.util.PhoneCapabilityTester;
50 import com.google.common.base.Objects;
51 
52 /**
53  * This is an invisible worker {@link Fragment} that loads the contact details for the contact card.
54  * The data is then passed to the listener, who can then pass the data to other {@link View}s.
55  */
56 public class ContactLoaderFragment extends Fragment implements FragmentKeyListener {
57 
58     private static final String TAG = ContactLoaderFragment.class.getSimpleName();
59 
60     /** The launch code when picking a ringtone */
61     private static final int REQUEST_CODE_PICK_RINGTONE = 1;
62 
63     /** This is the Intent action to install a shortcut in the launcher. */
64     private static final String ACTION_INSTALL_SHORTCUT =
65             "com.android.launcher.action.INSTALL_SHORTCUT";
66 
67     private boolean mOptionsMenuOptions;
68     private boolean mOptionsMenuEditable;
69     private boolean mOptionsMenuShareable;
70     private boolean mOptionsMenuCanCreateShortcut;
71     private boolean mSendToVoicemailState;
72     private String mCustomRingtone;
73 
74     /**
75      * This is a listener to the {@link ContactLoaderFragment} and will be notified when the
76      * contact details have finished loading or if the user selects any menu options.
77      */
78     public static interface ContactLoaderFragmentListener {
79         /**
80          * Contact was not found, so somehow close this fragment. This is raised after a contact
81          * is removed via Menu/Delete
82          */
onContactNotFound()83         public void onContactNotFound();
84 
85         /**
86          * Contact details have finished loading.
87          */
onDetailsLoaded(Contact result)88         public void onDetailsLoaded(Contact result);
89 
90         /**
91          * User decided to go to Edit-Mode
92          */
onEditRequested(Uri lookupUri)93         public void onEditRequested(Uri lookupUri);
94 
95         /**
96          * User decided to delete the contact
97          */
onDeleteRequested(Uri lookupUri)98         public void onDeleteRequested(Uri lookupUri);
99 
100     }
101 
102     private static final int LOADER_DETAILS = 1;
103 
104     private static final String KEY_CONTACT_URI = "contactUri";
105     private static final String LOADER_ARG_CONTACT_URI = "contactUri";
106 
107     private Context mContext;
108     private Uri mLookupUri;
109     private ContactLoaderFragmentListener mListener;
110 
111     private Contact mContactData;
112 
ContactLoaderFragment()113     public ContactLoaderFragment() {
114     }
115 
116     @Override
onCreate(Bundle savedInstanceState)117     public void onCreate(Bundle savedInstanceState) {
118         super.onCreate(savedInstanceState);
119         if (savedInstanceState != null) {
120             mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
121         }
122     }
123 
124     @Override
onSaveInstanceState(Bundle outState)125     public void onSaveInstanceState(Bundle outState) {
126         super.onSaveInstanceState(outState);
127         outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
128     }
129 
130     @Override
onAttach(Activity activity)131     public void onAttach(Activity activity) {
132         super.onAttach(activity);
133         mContext = activity;
134     }
135 
136     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)137     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
138         setHasOptionsMenu(true);
139         // This is an invisible view.  This fragment is declared in a layout, so it can't be
140         // "viewless".  (i.e. can't return null here.)
141         // See also the comment in the layout file.
142         return inflater.inflate(R.layout.contact_detail_loader_fragment, container, false);
143     }
144 
145     @Override
onActivityCreated(Bundle savedInstanceState)146     public void onActivityCreated(Bundle savedInstanceState) {
147         super.onActivityCreated(savedInstanceState);
148 
149         if (mLookupUri != null) {
150             Bundle args = new Bundle();
151             args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
152             getLoaderManager().initLoader(LOADER_DETAILS, args, mDetailLoaderListener);
153         }
154     }
155 
loadUri(Uri lookupUri)156     public void loadUri(Uri lookupUri) {
157         if (Objects.equal(lookupUri, mLookupUri)) {
158             // Same URI, no need to load the data again
159             return;
160         }
161 
162         mLookupUri = lookupUri;
163         if (mLookupUri == null) {
164             getLoaderManager().destroyLoader(LOADER_DETAILS);
165             mContactData = null;
166             if (mListener != null) {
167                 mListener.onDetailsLoaded(mContactData);
168             }
169         } else if (getActivity() != null) {
170             Bundle args = new Bundle();
171             args.putParcelable(LOADER_ARG_CONTACT_URI, mLookupUri);
172             getLoaderManager().restartLoader(LOADER_DETAILS, args, mDetailLoaderListener);
173         }
174     }
175 
setListener(ContactLoaderFragmentListener value)176     public void setListener(ContactLoaderFragmentListener value) {
177         mListener = value;
178     }
179 
180     /**
181      * The listener for the detail loader
182      */
183     private final LoaderManager.LoaderCallbacks<Contact> mDetailLoaderListener =
184             new LoaderCallbacks<Contact>() {
185         @Override
186         public Loader<Contact> onCreateLoader(int id, Bundle args) {
187             Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
188             return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */,
189                     true /* loadStreamItems */, true /* load invitable account types */,
190                     true /* postViewNotification */, true /* computeFormattedPhoneNumber */);
191         }
192 
193         @Override
194         public void onLoadFinished(Loader<Contact> loader, Contact data) {
195             if (!mLookupUri.equals(data.getRequestedUri())) {
196                 Log.e(TAG, "Different URI: requested=" + mLookupUri + "  actual=" + data);
197                 return;
198             }
199 
200             if (data.isError()) {
201                 // This shouldn't ever happen, so throw an exception. The {@link ContactLoader}
202                 // should log the actual exception.
203                 throw new IllegalStateException("Failed to load contact", data.getException());
204             } else if (data.isNotFound()) {
205                 Log.i(TAG, "No contact found: " + ((ContactLoader)loader).getLookupUri());
206                 mContactData = null;
207             } else {
208                 mContactData = data;
209             }
210 
211             if (mListener != null) {
212                 if (mContactData == null) {
213                     mListener.onContactNotFound();
214                 } else {
215                     mListener.onDetailsLoaded(mContactData);
216                 }
217             }
218             // Make sure the options menu is setup correctly with the loaded data.
219             if (getActivity() != null) getActivity().invalidateOptionsMenu();
220         }
221 
222         @Override
223         public void onLoaderReset(Loader<Contact> loader) {}
224     };
225 
226     @Override
onCreateOptionsMenu(Menu menu, final MenuInflater inflater)227     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
228         inflater.inflate(R.menu.view_contact, menu);
229     }
230 
isOptionsMenuChanged()231     public boolean isOptionsMenuChanged() {
232         return mOptionsMenuOptions != isContactOptionsChangeEnabled()
233                 || mOptionsMenuEditable != isContactEditable()
234                 || mOptionsMenuShareable != isContactShareable()
235                 || mOptionsMenuCanCreateShortcut != isContactCanCreateShortcut();
236     }
237 
238     @Override
onPrepareOptionsMenu(Menu menu)239     public void onPrepareOptionsMenu(Menu menu) {
240         mOptionsMenuOptions = isContactOptionsChangeEnabled();
241         mOptionsMenuEditable = isContactEditable();
242         mOptionsMenuShareable = isContactShareable();
243         mOptionsMenuCanCreateShortcut = isContactCanCreateShortcut();
244         if (mContactData != null) {
245             mSendToVoicemailState = mContactData.isSendToVoicemail();
246             mCustomRingtone = mContactData.getCustomRingtone();
247         }
248 
249         // Hide telephony-related settings (ringtone, send to voicemail)
250         // if we don't have a telephone
251         final MenuItem optionsSendToVoicemail = menu.findItem(R.id.menu_send_to_voicemail);
252         if (optionsSendToVoicemail != null) {
253             optionsSendToVoicemail.setChecked(mSendToVoicemailState);
254             optionsSendToVoicemail.setVisible(mOptionsMenuOptions);
255         }
256         final MenuItem optionsRingtone = menu.findItem(R.id.menu_set_ringtone);
257         if (optionsRingtone != null) {
258             optionsRingtone.setVisible(mOptionsMenuOptions);
259         }
260 
261         final MenuItem editMenu = menu.findItem(R.id.menu_edit);
262         editMenu.setVisible(mOptionsMenuEditable);
263 
264         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
265         deleteMenu.setVisible(mOptionsMenuEditable);
266 
267         final MenuItem shareMenu = menu.findItem(R.id.menu_share);
268         shareMenu.setVisible(mOptionsMenuShareable);
269 
270         final MenuItem createContactShortcutMenu = menu.findItem(R.id.menu_create_contact_shortcut);
271         createContactShortcutMenu.setVisible(mOptionsMenuCanCreateShortcut);
272     }
273 
isContactOptionsChangeEnabled()274     public boolean isContactOptionsChangeEnabled() {
275         return mContactData != null && !mContactData.isDirectoryEntry()
276                 && PhoneCapabilityTester.isPhone(mContext);
277     }
278 
isContactEditable()279     public boolean isContactEditable() {
280         return mContactData != null && !mContactData.isDirectoryEntry();
281     }
282 
isContactShareable()283     public boolean isContactShareable() {
284         return mContactData != null && !mContactData.isDirectoryEntry();
285     }
286 
isContactCanCreateShortcut()287     public boolean isContactCanCreateShortcut() {
288         return mContactData != null && !mContactData.isUserProfile()
289                 && !mContactData.isDirectoryEntry();
290     }
291 
292     @Override
onOptionsItemSelected(MenuItem item)293     public boolean onOptionsItemSelected(MenuItem item) {
294         switch (item.getItemId()) {
295             case R.id.menu_edit: {
296                 if (mListener != null) mListener.onEditRequested(mLookupUri);
297                 break;
298             }
299             case R.id.menu_delete: {
300                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
301                 return true;
302             }
303             case R.id.menu_set_ringtone: {
304                 if (mContactData == null) return false;
305                 doPickRingtone();
306                 return true;
307             }
308             case R.id.menu_share: {
309                 if (mContactData == null) return false;
310 
311                 final String lookupKey = mContactData.getLookupKey();
312                 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
313                 if (mContactData.isUserProfile()) {
314                     // User is sharing the profile.  We don't want to force the receiver to have
315                     // the highly-privileged READ_PROFILE permission, so we need to request a
316                     // pre-authorized URI from the provider.
317                     shareUri = getPreAuthorizedUri(shareUri);
318                 }
319 
320                 final Intent intent = new Intent(Intent.ACTION_SEND);
321                 intent.setType(Contacts.CONTENT_VCARD_TYPE);
322                 intent.putExtra(Intent.EXTRA_STREAM, shareUri);
323 
324                 // Launch chooser to share contact via
325                 final CharSequence chooseTitle = mContext.getText(R.string.share_via);
326                 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
327 
328                 try {
329                     mContext.startActivity(chooseIntent);
330                 } catch (ActivityNotFoundException ex) {
331                     Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
332                 }
333                 return true;
334             }
335             case R.id.menu_send_to_voicemail: {
336                 // Update state and save
337                 mSendToVoicemailState = !mSendToVoicemailState;
338                 item.setChecked(mSendToVoicemailState);
339                 Intent intent = ContactSaveService.createSetSendToVoicemail(
340                         mContext, mLookupUri, mSendToVoicemailState);
341                 mContext.startService(intent);
342                 return true;
343             }
344             case R.id.menu_create_contact_shortcut: {
345                 // Create a launcher shortcut with this contact
346                 createLauncherShortcutWithContact();
347                 return true;
348             }
349         }
350         return false;
351     }
352 
353     /**
354      * Creates a launcher shortcut with the current contact.
355      */
createLauncherShortcutWithContact()356     private void createLauncherShortcutWithContact() {
357         // Hold the parent activity of this fragment in case this fragment is destroyed
358         // before the callback to onShortcutIntentCreated(...)
359         final Activity parentActivity = getActivity();
360 
361         ShortcutIntentBuilder builder = new ShortcutIntentBuilder(parentActivity,
362                 new OnShortcutIntentCreatedListener() {
363 
364             @Override
365             public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
366                 // Broadcast the shortcutIntent to the launcher to create a
367                 // shortcut to this contact
368                 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
369                 parentActivity.sendBroadcast(shortcutIntent);
370 
371                 // Send a toast to give feedback to the user that a shortcut to this
372                 // contact was added to the launcher.
373                 Toast.makeText(parentActivity,
374                         R.string.createContactShortcutSuccessful,
375                         Toast.LENGTH_SHORT).show();
376             }
377 
378         });
379         builder.createContactShortcutIntent(mLookupUri);
380     }
381 
382     /**
383      * Calls into the contacts provider to get a pre-authorized version of the given URI.
384      */
getPreAuthorizedUri(Uri uri)385     private Uri getPreAuthorizedUri(Uri uri) {
386         Bundle uriBundle = new Bundle();
387         uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri);
388         Bundle authResponse = mContext.getContentResolver().call(
389                 ContactsContract.AUTHORITY_URI,
390                 ContactsContract.Authorization.AUTHORIZATION_METHOD,
391                 null,
392                 uriBundle);
393         if (authResponse != null) {
394             return (Uri) authResponse.getParcelable(
395                     ContactsContract.Authorization.KEY_AUTHORIZED_URI);
396         } else {
397             return uri;
398         }
399     }
400 
401     @Override
handleKeyDown(int keyCode)402     public boolean handleKeyDown(int keyCode) {
403         switch (keyCode) {
404             case KeyEvent.KEYCODE_DEL: {
405                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
406                 return true;
407             }
408         }
409         return false;
410     }
411 
doPickRingtone()412     private void doPickRingtone() {
413 
414         Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
415         // Allow user to pick 'Default'
416         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
417         // Show only ringtones
418         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
419         // Allow the user to pick a silent ringtone
420         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
421 
422         Uri ringtoneUri;
423         if (mCustomRingtone != null) {
424             ringtoneUri = Uri.parse(mCustomRingtone);
425         } else {
426             // Otherwise pick default ringtone Uri so that something is selected.
427             ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
428         }
429 
430         // Put checkmark next to the current ringtone for this contact
431         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
432 
433         // Launch!
434         startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
435     }
436 
437     @Override
onActivityResult(int requestCode, int resultCode, Intent data)438     public void onActivityResult(int requestCode, int resultCode, Intent data) {
439         if (resultCode != Activity.RESULT_OK) {
440             return;
441         }
442 
443         switch (requestCode) {
444             case REQUEST_CODE_PICK_RINGTONE: {
445                 Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
446                 handleRingtonePicked(pickedUri);
447                 break;
448             }
449         }
450     }
451 
handleRingtonePicked(Uri pickedUri)452     private void handleRingtonePicked(Uri pickedUri) {
453         if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
454             mCustomRingtone = null;
455         } else {
456             mCustomRingtone = pickedUri.toString();
457         }
458         Intent intent = ContactSaveService.createSetRingtone(
459                 mContext, mLookupUri, mCustomRingtone);
460         mContext.startService(intent);
461     }
462 
463     /** Toggles whether to load stream items. Just for debugging */
toggleLoadStreamItems()464     public void toggleLoadStreamItems() {
465         Loader<Contact> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
466         ContactLoader loader = (ContactLoader) loaderObj;
467         loader.setLoadStreamItems(!loader.getLoadStreamItems());
468     }
469 
470     /** Returns whether to load stream items. Just for debugging */
getLoadStreamItems()471     public boolean getLoadStreamItems() {
472         Loader<Contact> loaderObj = getLoaderManager().getLoader(LOADER_DETAILS);
473         ContactLoader loader = (ContactLoader) loaderObj;
474         return loader != null && loader.getLoadStreamItems();
475     }
476 }
477