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.common.model.Contact;
     48 import com.android.contacts.common.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 /* load invitable account types */, true /* postViewNotification */,
    190                     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         if (editMenu != null) {
    263             editMenu.setVisible(mOptionsMenuEditable);
    264         }
    265 
    266         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete);
    267         if (deleteMenu != null) {
    268             deleteMenu.setVisible(mOptionsMenuEditable);
    269         }
    270 
    271         final MenuItem shareMenu = menu.findItem(R.id.menu_share);
    272         if (shareMenu != null) {
    273             shareMenu.setVisible(mOptionsMenuShareable);
    274         }
    275 
    276         final MenuItem createContactShortcutMenu = menu.findItem(R.id.menu_create_contact_shortcut);
    277         if (createContactShortcutMenu != null) {
    278             createContactShortcutMenu.setVisible(mOptionsMenuCanCreateShortcut);
    279         }
    280     }
    281 
    282     public boolean isContactOptionsChangeEnabled() {
    283         return mContactData != null && !mContactData.isDirectoryEntry()
    284                 && PhoneCapabilityTester.isPhone(mContext);
    285     }
    286 
    287     public boolean isContactEditable() {
    288         return mContactData != null && !mContactData.isDirectoryEntry();
    289     }
    290 
    291     public boolean isContactShareable() {
    292         return mContactData != null && !mContactData.isDirectoryEntry();
    293     }
    294 
    295     public boolean isContactCanCreateShortcut() {
    296         return mContactData != null && !mContactData.isUserProfile()
    297                 && !mContactData.isDirectoryEntry();
    298     }
    299 
    300     @Override
    301     public boolean onOptionsItemSelected(MenuItem item) {
    302         switch (item.getItemId()) {
    303             case R.id.menu_edit: {
    304                 if (mListener != null) mListener.onEditRequested(mLookupUri);
    305                 break;
    306             }
    307             case R.id.menu_delete: {
    308                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
    309                 return true;
    310             }
    311             case R.id.menu_set_ringtone: {
    312                 if (mContactData == null) return false;
    313                 doPickRingtone();
    314                 return true;
    315             }
    316             case R.id.menu_share: {
    317                 if (mContactData == null) return false;
    318 
    319                 final String lookupKey = mContactData.getLookupKey();
    320                 Uri shareUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
    321                 if (mContactData.isUserProfile()) {
    322                     // User is sharing the profile.  We don't want to force the receiver to have
    323                     // the highly-privileged READ_PROFILE permission, so we need to request a
    324                     // pre-authorized URI from the provider.
    325                     shareUri = getPreAuthorizedUri(shareUri);
    326                 }
    327 
    328                 final Intent intent = new Intent(Intent.ACTION_SEND);
    329                 intent.setType(Contacts.CONTENT_VCARD_TYPE);
    330                 intent.putExtra(Intent.EXTRA_STREAM, shareUri);
    331 
    332                 // Launch chooser to share contact via
    333                 final CharSequence chooseTitle = mContext.getText(R.string.share_via);
    334                 final Intent chooseIntent = Intent.createChooser(intent, chooseTitle);
    335 
    336                 try {
    337                     mContext.startActivity(chooseIntent);
    338                 } catch (ActivityNotFoundException ex) {
    339                     Toast.makeText(mContext, R.string.share_error, Toast.LENGTH_SHORT).show();
    340                 }
    341                 return true;
    342             }
    343             case R.id.menu_send_to_voicemail: {
    344                 // Update state and save
    345                 mSendToVoicemailState = !mSendToVoicemailState;
    346                 item.setChecked(mSendToVoicemailState);
    347                 Intent intent = ContactSaveService.createSetSendToVoicemail(
    348                         mContext, mLookupUri, mSendToVoicemailState);
    349                 mContext.startService(intent);
    350                 return true;
    351             }
    352             case R.id.menu_create_contact_shortcut: {
    353                 // Create a launcher shortcut with this contact
    354                 createLauncherShortcutWithContact();
    355                 return true;
    356             }
    357         }
    358         return false;
    359     }
    360 
    361     /**
    362      * Creates a launcher shortcut with the current contact.
    363      */
    364     private void createLauncherShortcutWithContact() {
    365         // Hold the parent activity of this fragment in case this fragment is destroyed
    366         // before the callback to onShortcutIntentCreated(...)
    367         final Activity parentActivity = getActivity();
    368 
    369         ShortcutIntentBuilder builder = new ShortcutIntentBuilder(parentActivity,
    370                 new OnShortcutIntentCreatedListener() {
    371 
    372             @Override
    373             public void onShortcutIntentCreated(Uri uri, Intent shortcutIntent) {
    374                 // Broadcast the shortcutIntent to the launcher to create a
    375                 // shortcut to this contact
    376                 shortcutIntent.setAction(ACTION_INSTALL_SHORTCUT);
    377                 parentActivity.sendBroadcast(shortcutIntent);
    378 
    379                 // Send a toast to give feedback to the user that a shortcut to this
    380                 // contact was added to the launcher.
    381                 Toast.makeText(parentActivity,
    382                         R.string.createContactShortcutSuccessful,
    383                         Toast.LENGTH_SHORT).show();
    384             }
    385 
    386         });
    387         builder.createContactShortcutIntent(mLookupUri);
    388     }
    389 
    390     /**
    391      * Calls into the contacts provider to get a pre-authorized version of the given URI.
    392      */
    393     private Uri getPreAuthorizedUri(Uri uri) {
    394         Bundle uriBundle = new Bundle();
    395         uriBundle.putParcelable(ContactsContract.Authorization.KEY_URI_TO_AUTHORIZE, uri);
    396         Bundle authResponse = mContext.getContentResolver().call(
    397                 ContactsContract.AUTHORITY_URI,
    398                 ContactsContract.Authorization.AUTHORIZATION_METHOD,
    399                 null,
    400                 uriBundle);
    401         if (authResponse != null) {
    402             return (Uri) authResponse.getParcelable(
    403                     ContactsContract.Authorization.KEY_AUTHORIZED_URI);
    404         } else {
    405             return uri;
    406         }
    407     }
    408 
    409     @Override
    410     public boolean handleKeyDown(int keyCode) {
    411         switch (keyCode) {
    412             case KeyEvent.KEYCODE_DEL: {
    413                 if (mListener != null) mListener.onDeleteRequested(mLookupUri);
    414                 return true;
    415             }
    416         }
    417         return false;
    418     }
    419 
    420     private void doPickRingtone() {
    421 
    422         Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER);
    423         // Allow user to pick 'Default'
    424         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
    425         // Show only ringtones
    426         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE);
    427         // Allow the user to pick a silent ringtone
    428         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
    429 
    430         Uri ringtoneUri;
    431         if (mCustomRingtone != null) {
    432             ringtoneUri = Uri.parse(mCustomRingtone);
    433         } else {
    434             // Otherwise pick default ringtone Uri so that something is selected.
    435             ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
    436         }
    437 
    438         // Put checkmark next to the current ringtone for this contact
    439         intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, ringtoneUri);
    440 
    441         // Launch!
    442         startActivityForResult(intent, REQUEST_CODE_PICK_RINGTONE);
    443     }
    444 
    445     @Override
    446     public void onActivityResult(int requestCode, int resultCode, Intent data) {
    447         if (resultCode != Activity.RESULT_OK) {
    448             return;
    449         }
    450 
    451         switch (requestCode) {
    452             case REQUEST_CODE_PICK_RINGTONE: {
    453                 Uri pickedUri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
    454                 handleRingtonePicked(pickedUri);
    455                 break;
    456             }
    457         }
    458     }
    459 
    460     private void handleRingtonePicked(Uri pickedUri) {
    461         if (pickedUri == null || RingtoneManager.isDefault(pickedUri)) {
    462             mCustomRingtone = null;
    463         } else {
    464             mCustomRingtone = pickedUri.toString();
    465         }
    466         Intent intent = ContactSaveService.createSetRingtone(
    467                 mContext, mLookupUri, mCustomRingtone);
    468         mContext.startService(intent);
    469     }
    470 }
    471