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