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