Home | History | Annotate | Download | only in detail
      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          */
     83         public void onContactNotFound();
     84 
     85         /**
     86          * Contact details have finished loading.
     87          */
     88         public void onDetailsLoaded(Contact result);
     89 
     90         /**
     91          * User decided to go to Edit-Mode
     92          */
     93         public void onEditRequested(Uri lookupUri);
     94 
     95         /**
     96          * User decided to delete the contact
     97          */
     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 
    113     public ContactLoaderFragment() {
    114     }
    115 
    116     @Override
    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
    125     public void onSaveInstanceState(Bundle outState) {
    126         super.onSaveInstanceState(outState);
    127         outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
    128     }
    129 
    130     @Override
    131     public void onAttach(Activity activity) {
    132         super.onAttach(activity);
    133         mContext = activity;
    134     }
    135 
    136     @Override
    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
    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 
    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 
    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
    227     public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) {
    228         inflater.inflate(R.menu.view_contact, menu);
    229     }
    230 
    231     public boolean isOptionsMenuChanged() {
    232         return mOptionsMenuOptions != isContactOptionsChangeEnabled()
    233                 || mOptionsMenuEditable != isContactEditable()
    234                 || mOptionsMenuShareable != isContactShareable()
    235                 || mOptionsMenuCanCreateShortcut != isContactCanCreateShortcut();
    236     }
    237 
    238     @Override
    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 
    274     public boolean isContactOptionsChangeEnabled() {
    275         return mContactData != null && !mContactData.isDirectoryEntry()
    276                 && PhoneCapabilityTester.isPhone(mContext);
    277     }
    278 
    279     public boolean isContactEditable() {
    280         return mContactData != null && !mContactData.isDirectoryEntry();
    281     }
    282 
    283     public boolean isContactShareable() {
    284         return mContactData != null && !mContactData.isDirectoryEntry();
    285     }
    286 
    287     public boolean isContactCanCreateShortcut() {
    288         return mContactData != null && !mContactData.isUserProfile()
    289                 && !mContactData.isDirectoryEntry();
    290     }
    291 
    292     @Override
    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      */
    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      */
    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
    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 
    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
    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 
    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 */
    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 */
    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