1 /* 2 * Copyright (C) 2010 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.common.interactions; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.Dialog; 22 import android.app.DialogFragment; 23 import android.app.FragmentManager; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.content.res.Resources; 28 import android.database.Cursor; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.provider.ContactsContract.Contacts; 32 import android.telephony.SubscriptionInfo; 33 import android.telephony.SubscriptionManager; 34 import android.telephony.TelephonyManager; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.widget.ArrayAdapter; 41 import android.widget.TextView; 42 import android.widget.Toast; 43 44 import com.android.contacts.common.R; 45 import com.android.contacts.common.compat.CompatUtils; 46 import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 47 import com.android.contacts.common.editor.SelectAccountDialogFragment; 48 import com.android.contacts.common.model.AccountTypeManager; 49 import com.android.contacts.common.model.account.AccountWithDataSet; 50 import com.android.contacts.common.util.AccountSelectionUtil; 51 import com.android.contacts.common.util.AccountsListAdapter.AccountListFilter; 52 import com.android.contacts.common.util.ImplicitIntentsUtil; 53 import com.android.contacts.common.vcard.ExportVCardActivity; 54 import com.android.contacts.common.vcard.VCardCommonArguments; 55 import com.android.contacts.common.vcard.ShareVCardActivity; 56 import com.android.contacts.commonbind.analytics.AnalyticsUtil; 57 58 import java.util.List; 59 60 /** 61 * An dialog invoked to import/export contacts. 62 */ 63 public class ImportExportDialogFragment extends DialogFragment 64 implements SelectAccountDialogFragment.Listener { 65 public static final String TAG = "ImportExportDialogFragment"; 66 67 public static final int EXPORT_MODE_FAVORITES = 0; 68 public static final int EXPORT_MODE_ALL_CONTACTS = 1; 69 public static final int EXPORT_MODE_DEFAULT = -1; 70 71 private static final String KEY_RES_ID = "resourceId"; 72 private static final String KEY_SUBSCRIPTION_ID = "subscriptionId"; 73 private static final String ARG_CONTACTS_ARE_AVAILABLE = "CONTACTS_ARE_AVAILABLE"; 74 75 private static int mExportMode = EXPORT_MODE_DEFAULT; 76 77 private final String[] LOOKUP_PROJECTION = new String[] { 78 Contacts.LOOKUP_KEY 79 }; 80 81 private SubscriptionManager mSubscriptionManager; 82 83 /** Preferred way to show this dialog */ show(FragmentManager fragmentManager, boolean contactsAreAvailable, Class callingActivity, int exportMode)84 public static void show(FragmentManager fragmentManager, boolean contactsAreAvailable, 85 Class callingActivity, int exportMode) { 86 final ImportExportDialogFragment fragment = new ImportExportDialogFragment(); 87 Bundle args = new Bundle(); 88 args.putBoolean(ARG_CONTACTS_ARE_AVAILABLE, contactsAreAvailable); 89 args.putString(VCardCommonArguments.ARG_CALLING_ACTIVITY, callingActivity.getName()); 90 fragment.setArguments(args); 91 fragment.show(fragmentManager, ImportExportDialogFragment.TAG); 92 mExportMode = exportMode; 93 } 94 95 @Override getContext()96 public Context getContext() { 97 return getActivity(); 98 } 99 100 @Override onAttach(Activity activity)101 public void onAttach(Activity activity) { 102 super.onAttach(activity); 103 AnalyticsUtil.sendScreenView(this); 104 } 105 106 @Override onCreateDialog(Bundle savedInstanceState)107 public Dialog onCreateDialog(Bundle savedInstanceState) { 108 // Wrap our context to inflate list items using the correct theme 109 final Resources res = getActivity().getResources(); 110 final LayoutInflater dialogInflater = (LayoutInflater)getActivity() 111 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 112 final boolean contactsAreAvailable = getArguments().getBoolean(ARG_CONTACTS_ARE_AVAILABLE); 113 final String callingActivity = getArguments().getString( 114 VCardCommonArguments.ARG_CALLING_ACTIVITY); 115 116 // Adapter that shows a list of string resources 117 final ArrayAdapter<AdapterEntry> adapter = new ArrayAdapter<AdapterEntry>(getActivity(), 118 R.layout.select_dialog_item) { 119 @Override 120 public View getView(int position, View convertView, ViewGroup parent) { 121 final TextView result = (TextView)(convertView != null ? convertView : 122 dialogInflater.inflate(R.layout.select_dialog_item, parent, false)); 123 124 result.setText(getItem(position).mLabel); 125 return result; 126 } 127 }; 128 129 final TelephonyManager manager = 130 (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); 131 if (res.getBoolean(R.bool.config_allow_import_from_vcf_file)) { 132 adapter.add(new AdapterEntry(getString(R.string.import_from_vcf_file), 133 R.string.import_from_vcf_file)); 134 } 135 136 if (CompatUtils.isMSIMCompatible()) { 137 mSubscriptionManager = SubscriptionManager.from(getActivity()); 138 if (manager != null && res.getBoolean(R.bool.config_allow_sim_import)) { 139 List<SubscriptionInfo> subInfoRecords = null; 140 try { 141 subInfoRecords = mSubscriptionManager.getActiveSubscriptionInfoList(); 142 } catch (SecurityException e) { 143 Log.w(TAG, "SecurityException thrown, lack permission for" 144 + " getActiveSubscriptionInfoList", e); 145 } 146 if (subInfoRecords != null) { 147 if (subInfoRecords.size() == 1) { 148 adapter.add(new AdapterEntry(getString(R.string.import_from_sim), 149 R.string.import_from_sim, subInfoRecords.get(0).getSubscriptionId())); 150 } else { 151 for (SubscriptionInfo record : subInfoRecords) { 152 adapter.add(new AdapterEntry(getSubDescription(record), 153 R.string.import_from_sim, record.getSubscriptionId())); 154 } 155 } 156 } 157 } 158 } else { 159 if (manager != null && manager.hasIccCard() 160 && res.getBoolean(R.bool.config_allow_sim_import)) { 161 adapter.add(new AdapterEntry(getString(R.string.import_from_sim), 162 R.string.import_from_sim, -1)); 163 } 164 } 165 166 if (res.getBoolean(R.bool.config_allow_export)) { 167 if (contactsAreAvailable) { 168 adapter.add(new AdapterEntry(getString(R.string.export_to_vcf_file), 169 R.string.export_to_vcf_file)); 170 } 171 } 172 if (res.getBoolean(R.bool.config_allow_share_contacts) && contactsAreAvailable) { 173 if (mExportMode == EXPORT_MODE_FAVORITES) { 174 // share favorite and frequently contacted contacts from Favorites tab 175 adapter.add(new AdapterEntry(getString(R.string.share_favorite_contacts), 176 R.string.share_contacts)); 177 } else { 178 // share "all" contacts (in groups selected in "Customize") from All tab for now 179 // TODO: change the string to share_visible_contacts if implemented 180 adapter.add(new AdapterEntry(getString(R.string.share_contacts), 181 R.string.share_contacts)); 182 } 183 } 184 185 final DialogInterface.OnClickListener clickListener = 186 new DialogInterface.OnClickListener() { 187 @Override 188 public void onClick(DialogInterface dialog, int which) { 189 boolean dismissDialog; 190 final int resId = adapter.getItem(which).mChoiceResourceId; 191 if (resId == R.string.import_from_sim || resId == R.string.import_from_vcf_file) { 192 dismissDialog = handleImportRequest(resId, 193 adapter.getItem(which).mSubscriptionId); 194 } else if (resId == R.string.export_to_vcf_file) { 195 dismissDialog = true; 196 final Intent exportIntent = new Intent( 197 getActivity(), ExportVCardActivity.class); 198 exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY, 199 callingActivity); 200 getActivity().startActivity(exportIntent); 201 } else if (resId == R.string.share_contacts) { 202 dismissDialog = true; 203 if (mExportMode == EXPORT_MODE_FAVORITES) { 204 doShareFavoriteContacts(); 205 } else { // EXPORT_MODE_ALL_CONTACTS 206 final Intent exportIntent = new Intent( 207 getActivity(), ShareVCardActivity.class); 208 exportIntent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY, 209 callingActivity); 210 getActivity().startActivity(exportIntent); 211 } 212 } else { 213 dismissDialog = true; 214 Log.e(TAG, "Unexpected resource: " 215 + getActivity().getResources().getResourceEntryName(resId)); 216 } 217 if (dismissDialog) { 218 dialog.dismiss(); 219 } 220 } 221 }; 222 return new AlertDialog.Builder(getActivity()) 223 .setTitle(contactsAreAvailable 224 ? R.string.dialog_import_export 225 : R.string.dialog_import) 226 .setSingleChoiceItems(adapter, -1, clickListener) 227 .create(); 228 } 229 doShareFavoriteContacts()230 private void doShareFavoriteContacts() { 231 try{ 232 final Cursor cursor = getActivity().getContentResolver().query( 233 Contacts.CONTENT_STREQUENT_URI, LOOKUP_PROJECTION, null, null, 234 Contacts.DISPLAY_NAME + " COLLATE NOCASE ASC"); 235 if (cursor != null) { 236 try { 237 if (!cursor.moveToFirst()) { 238 Toast.makeText(getActivity(), R.string.no_contact_to_share, 239 Toast.LENGTH_SHORT).show(); 240 return; 241 } 242 243 // Build multi-vcard Uri for sharing 244 final StringBuilder uriListBuilder = new StringBuilder(); 245 int index = 0; 246 do { 247 if (index != 0) 248 uriListBuilder.append(':'); 249 uriListBuilder.append(cursor.getString(0)); 250 index++; 251 } while (cursor.moveToNext()); 252 final Uri uri = Uri.withAppendedPath( 253 Contacts.CONTENT_MULTI_VCARD_URI, 254 Uri.encode(uriListBuilder.toString())); 255 256 final Intent intent = new Intent(Intent.ACTION_SEND); 257 intent.setType(Contacts.CONTENT_VCARD_TYPE); 258 intent.putExtra(Intent.EXTRA_STREAM, uri); 259 ImplicitIntentsUtil.startActivityOutsideApp(getActivity(), intent); 260 } finally { 261 cursor.close(); 262 } 263 } 264 } catch (Exception e) { 265 Log.e(TAG, "Sharing contacts failed", e); 266 getActivity().runOnUiThread(new Runnable() { 267 @Override 268 public void run() { 269 Toast.makeText(getContext(), R.string.share_contacts_failure, 270 Toast.LENGTH_SHORT).show(); 271 } 272 }); 273 } 274 } 275 276 /** 277 * Handle "import from SIM" and "import from SD". 278 * 279 * @return {@code true} if the dialog show be closed. {@code false} otherwise. 280 */ handleImportRequest(int resId, int subscriptionId)281 private boolean handleImportRequest(int resId, int subscriptionId) { 282 // There are three possibilities: 283 // - more than one accounts -> ask the user 284 // - just one account -> use the account without asking the user 285 // - no account -> use phone-local storage without asking the user 286 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(getActivity()); 287 final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true); 288 final int size = accountList.size(); 289 if (size > 1) { 290 // Send over to the account selector 291 final Bundle args = new Bundle(); 292 args.putInt(KEY_RES_ID, resId); 293 args.putInt(KEY_SUBSCRIPTION_ID, subscriptionId); 294 SelectAccountDialogFragment.show( 295 getFragmentManager(), this, 296 R.string.dialog_new_contact_account, 297 AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, args); 298 299 // In this case, because this DialogFragment is used as a target fragment to 300 // SelectAccountDialogFragment, we can't close it yet. We close the dialog when 301 // we get a callback from it. 302 return false; 303 } 304 305 AccountSelectionUtil.doImport(getActivity(), resId, 306 (size == 1 ? accountList.get(0) : null), 307 (CompatUtils.isMSIMCompatible() ? subscriptionId : -1)); 308 return true; // Close the dialog. 309 } 310 311 /** 312 * Called when an account is selected on {@link SelectAccountDialogFragment}. 313 */ 314 @Override onAccountChosen(AccountWithDataSet account, Bundle extraArgs)315 public void onAccountChosen(AccountWithDataSet account, Bundle extraArgs) { 316 AccountSelectionUtil.doImport(getActivity(), extraArgs.getInt(KEY_RES_ID), 317 account, extraArgs.getInt(KEY_SUBSCRIPTION_ID)); 318 319 // At this point the dialog is still showing (which is why we can use getActivity() above) 320 // So close it. 321 dismiss(); 322 } 323 324 @Override onAccountSelectorCancelled()325 public void onAccountSelectorCancelled() { 326 // See onAccountChosen() -- at this point the dialog is still showing. Close it. 327 dismiss(); 328 } 329 getSubDescription(SubscriptionInfo record)330 private CharSequence getSubDescription(SubscriptionInfo record) { 331 CharSequence name = record.getDisplayName(); 332 if (TextUtils.isEmpty(record.getNumber())) { 333 // Don't include the phone number in the description, since we don't know the number. 334 return getString(R.string.import_from_sim_summary_no_number, name); 335 } 336 return TextUtils.expandTemplate( 337 getString(R.string.import_from_sim_summary), 338 name, 339 PhoneNumberUtilsCompat.createTtsSpannable(record.getNumber())); 340 } 341 342 private static class AdapterEntry { 343 public final CharSequence mLabel; 344 public final int mChoiceResourceId; 345 public final int mSubscriptionId; 346 AdapterEntry(CharSequence label, int resId, int subId)347 public AdapterEntry(CharSequence label, int resId, int subId) { 348 mLabel = label; 349 mChoiceResourceId = resId; 350 mSubscriptionId = subId; 351 } 352 AdapterEntry(String label, int resId)353 public AdapterEntry(String label, int resId) { 354 // Store a nonsense value for mSubscriptionId. If this constructor is used, 355 // the mSubscriptionId value should not be read later. 356 this(label, resId, /* subId = */ -1); 357 } 358 } 359 } 360