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 */ onContactNotFound()82 public void onContactNotFound(); 83 84 /** 85 * Contact details have finished loading. 86 */ onDetailsLoaded(ContactLoader.Result result)87 public void onDetailsLoaded(ContactLoader.Result result); 88 89 /** 90 * User decided to go to Edit-Mode 91 */ onEditRequested(Uri lookupUri)92 public void onEditRequested(Uri lookupUri); 93 94 /** 95 * User decided to delete the contact 96 */ onDeleteRequested(Uri lookupUri)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 ContactLoaderFragment()112 public ContactLoaderFragment() { 113 } 114 115 @Override onCreate(Bundle savedInstanceState)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 onSaveInstanceState(Bundle outState)124 public void onSaveInstanceState(Bundle outState) { 125 super.onSaveInstanceState(outState); 126 outState.putParcelable(KEY_CONTACT_URI, mLookupUri); 127 } 128 129 @Override onAttach(Activity activity)130 public void onAttach(Activity activity) { 131 super.onAttach(activity); 132 mContext = activity; 133 } 134 135 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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 onActivityCreated(Bundle savedInstanceState)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 loadUri(Uri lookupUri)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 setListener(ContactLoaderFragmentListener value)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 onCreateOptionsMenu(Menu menu, final MenuInflater inflater)226 public void onCreateOptionsMenu(Menu menu, final MenuInflater inflater) { 227 inflater.inflate(R.menu.view_contact, menu); 228 } 229 isOptionsMenuChanged()230 public boolean isOptionsMenuChanged() { 231 return mOptionsMenuOptions != isContactOptionsChangeEnabled() 232 || mOptionsMenuEditable != isContactEditable() 233 || mOptionsMenuShareable != isContactShareable() 234 || mOptionsMenuCanCreateShortcut != isContactCanCreateShortcut(); 235 } 236 237 @Override onPrepareOptionsMenu(Menu menu)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 isContactOptionsChangeEnabled()273 public boolean isContactOptionsChangeEnabled() { 274 return mContactData != null && !mContactData.isDirectoryEntry() 275 && PhoneCapabilityTester.isPhone(mContext); 276 } 277 isContactEditable()278 public boolean isContactEditable() { 279 return mContactData != null && !mContactData.isDirectoryEntry(); 280 } 281 isContactShareable()282 public boolean isContactShareable() { 283 return mContactData != null && !mContactData.isDirectoryEntry(); 284 } 285 isContactCanCreateShortcut()286 public boolean isContactCanCreateShortcut() { 287 return mContactData != null && !mContactData.isUserProfile() 288 && !mContactData.isDirectoryEntry(); 289 } 290 291 @Override onOptionsItemSelected(MenuItem item)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 */ createLauncherShortcutWithContact()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 */ getPreAuthorizedUri(Uri uri)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 handleKeyDown(int keyCode)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 doPickRingtone()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 onActivityResult(int requestCode, int resultCode, Intent data)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 handleRingtonePicked(Uri pickedUri)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 */ toggleLoadStreamItems()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 */ getLoadStreamItems()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