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