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