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.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.os.Bundle; 29 import androidx.core.text.BidiFormatter; 30 import androidx.core.text.TextDirectionHeuristicsCompat; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.widget.ArrayAdapter; 37 import android.widget.TextView; 38 39 import com.android.contacts.R; 40 import com.android.contacts.activities.SimImportActivity; 41 import com.android.contacts.compat.CompatUtils; 42 import com.android.contacts.compat.PhoneNumberUtilsCompat; 43 import com.android.contacts.database.SimContactDao; 44 import com.android.contacts.editor.SelectAccountDialogFragment; 45 import com.android.contacts.model.AccountTypeManager; 46 import com.android.contacts.model.SimCard; 47 import com.android.contacts.model.SimContact; 48 import com.android.contacts.model.account.AccountInfo; 49 import com.android.contacts.model.account.AccountWithDataSet; 50 import com.android.contacts.util.AccountSelectionUtil; 51 import com.google.common.util.concurrent.Futures; 52 53 import java.util.List; 54 import java.util.concurrent.Future; 55 56 /** 57 * An dialog invoked to import/export contacts. 58 */ 59 public class ImportDialogFragment extends DialogFragment { 60 public static final String TAG = "ImportDialogFragment"; 61 62 public static final String KEY_RES_ID = "resourceId"; 63 public static final String KEY_SUBSCRIPTION_ID = "subscriptionId"; 64 65 public static final String EXTRA_SIM_ONLY = "extraSimOnly"; 66 67 public static final String EXTRA_SIM_CONTACT_COUNT_PREFIX = "simContactCount_"; 68 69 private boolean mSimOnly = false; 70 private SimContactDao mSimDao; 71 72 private Future<List<AccountInfo>> mAccountsFuture; 73 74 private static BidiFormatter sBidiFormatter = BidiFormatter.getInstance(); 75 76 /** Preferred way to show this dialog */ show(FragmentManager fragmentManager)77 public static void show(FragmentManager fragmentManager) { 78 final ImportDialogFragment fragment = new ImportDialogFragment(); 79 fragment.show(fragmentManager, TAG); 80 } 81 show(FragmentManager fragmentManager, List<SimCard> sims, boolean includeVcf)82 public static void show(FragmentManager fragmentManager, List<SimCard> sims, 83 boolean includeVcf) { 84 final ImportDialogFragment fragment = new ImportDialogFragment(); 85 final Bundle args = new Bundle(); 86 args.putBoolean(EXTRA_SIM_ONLY, !includeVcf); 87 for (SimCard sim : sims) { 88 final List<SimContact> contacts = sim.getContacts(); 89 if (contacts == null) { 90 continue; 91 } 92 args.putInt(EXTRA_SIM_CONTACT_COUNT_PREFIX + sim.getSimId(), contacts.size()); 93 } 94 95 fragment.setArguments(args); 96 fragment.show(fragmentManager, TAG); 97 } 98 99 @Override onCreate(Bundle savedInstanceState)100 public void onCreate(Bundle savedInstanceState) { 101 super.onCreate(savedInstanceState); 102 103 setStyle(STYLE_NORMAL, R.style.ContactsAlertDialogTheme); 104 105 final Bundle args = getArguments(); 106 mSimOnly = args != null && args.getBoolean(EXTRA_SIM_ONLY, false); 107 mSimDao = SimContactDao.create(getContext()); 108 } 109 110 @Override onResume()111 public void onResume() { 112 super.onResume(); 113 114 // Start loading the accounts. This is done in onResume in case they were refreshed. 115 mAccountsFuture = AccountTypeManager.getInstance(getActivity()).filterAccountsAsync( 116 AccountTypeManager.writableFilter()); 117 } 118 119 @Override getContext()120 public Context getContext() { 121 return getActivity(); 122 } 123 124 @Override onAttach(Activity activity)125 public void onAttach(Activity activity) { 126 super.onAttach(activity); 127 } 128 129 @Override onCreateDialog(Bundle savedInstanceState)130 public Dialog onCreateDialog(Bundle savedInstanceState) { 131 final LayoutInflater dialogInflater = (LayoutInflater) 132 getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); 133 134 // Adapter that shows a list of string resources 135 final ArrayAdapter<AdapterEntry> adapter = new ArrayAdapter<AdapterEntry>(getActivity(), 136 R.layout.select_dialog_item) { 137 138 @Override 139 public View getView(int position, View convertView, ViewGroup parent) { 140 final View result = convertView != null ? convertView : 141 dialogInflater.inflate(R.layout.select_dialog_item, parent, false); 142 final TextView primaryText = (TextView) result.findViewById(R.id.primary_text); 143 final TextView secondaryText = (TextView) result.findViewById(R.id.secondary_text); 144 final AdapterEntry entry = getItem(position); 145 secondaryText.setVisibility(View.GONE); 146 if (entry.mChoiceResourceId == R.string.import_from_sim) { 147 final CharSequence secondary = getSimSecondaryText(entry.mSim); 148 if (TextUtils.isEmpty(secondary)) { 149 secondaryText.setVisibility(View.GONE); 150 } else { 151 secondaryText.setText(secondary); 152 secondaryText.setVisibility(View.VISIBLE); 153 } 154 } 155 primaryText.setText(entry.mLabel); 156 return result; 157 } 158 159 CharSequence getSimSecondaryText(SimCard sim) { 160 int count = getSimContactCount(sim); 161 162 CharSequence phone = sim.getFormattedPhone(); 163 if (phone == null) { 164 phone = sim.getPhone(); 165 } 166 if (phone != null) { 167 phone = sBidiFormatter.unicodeWrap( 168 PhoneNumberUtilsCompat.createTtsSpannable(phone), 169 TextDirectionHeuristicsCompat.LTR); 170 } 171 172 if (count != -1 && phone != null) { 173 // We use a template instead of format string so that the TTS span is preserved 174 final CharSequence template = getResources() 175 .getQuantityString(R.plurals.import_from_sim_secondary_template, count); 176 return TextUtils.expandTemplate(template, String.valueOf(count), phone); 177 } else if (phone != null) { 178 return phone; 179 } else if (count != -1) { 180 // count != -1 181 return getResources() 182 .getQuantityString( 183 R.plurals.import_from_sim_secondary_contact_count_fmt, count, 184 count); 185 } else { 186 return null; 187 } 188 } 189 }; 190 191 addItems(adapter); 192 193 final DialogInterface.OnClickListener clickListener = 194 new DialogInterface.OnClickListener() { 195 @Override 196 public void onClick(DialogInterface dialog, int which) { 197 final int resId = adapter.getItem(which).mChoiceResourceId; 198 if (resId == R.string.import_from_sim) { 199 handleSimImportRequest(adapter.getItem(which).mSim); 200 } else if (resId == R.string.import_from_vcf_file) { 201 handleImportRequest(resId, SimCard.NO_SUBSCRIPTION_ID); 202 } else { 203 Log.e(TAG, "Unexpected resource: " 204 + getActivity().getResources().getResourceEntryName(resId)); 205 } 206 dialog.dismiss(); 207 } 208 }; 209 210 final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), getTheme()) 211 .setTitle(R.string.dialog_import) 212 .setNegativeButton(android.R.string.cancel, null); 213 if (adapter.isEmpty()) { 214 // Handle edge case; e.g. SIM card was removed. 215 builder.setMessage(R.string.nothing_to_import_message); 216 } else { 217 builder.setSingleChoiceItems(adapter, -1, clickListener); 218 } 219 220 return builder.create(); 221 } 222 getSimContactCount(SimCard sim)223 private int getSimContactCount(SimCard sim) { 224 if (sim.getContacts() != null) { 225 return sim.getContacts().size(); 226 } 227 final Bundle args = getArguments(); 228 if (args == null) { 229 return -1; 230 } 231 return args.getInt(EXTRA_SIM_CONTACT_COUNT_PREFIX + sim.getSimId(), -1); 232 } 233 addItems(ArrayAdapter<AdapterEntry> adapter)234 private void addItems(ArrayAdapter<AdapterEntry> adapter) { 235 final Resources res = getActivity().getResources(); 236 if (res.getBoolean(R.bool.config_allow_import_from_vcf_file) && !mSimOnly) { 237 adapter.add(new AdapterEntry(getString(R.string.import_from_vcf_file), 238 R.string.import_from_vcf_file)); 239 } 240 final List<SimCard> sims = mSimDao.getSimCards(); 241 242 if (sims.size() == 1) { 243 adapter.add(new AdapterEntry(getString(R.string.import_from_sim), 244 R.string.import_from_sim, sims.get(0))); 245 return; 246 } 247 for (int i = 0; i < sims.size(); i++) { 248 final SimCard sim = sims.get(i); 249 adapter.add(new AdapterEntry(getSimDescription(sim, i), R.string.import_from_sim, sim)); 250 } 251 } 252 handleSimImportRequest(SimCard sim)253 private void handleSimImportRequest(SimCard sim) { 254 startActivity(new Intent(getActivity(), SimImportActivity.class) 255 .putExtra(SimImportActivity.EXTRA_SUBSCRIPTION_ID, sim.getSubscriptionId())); 256 } 257 258 /** 259 * Handle "import from SD". 260 */ handleImportRequest(int resId, int subscriptionId)261 private void handleImportRequest(int resId, int subscriptionId) { 262 // Get the accounts. Because this only happens after a user action this should pretty 263 // much never block since it will usually be at least several seconds before the user 264 // interacts with the view 265 final List<AccountWithDataSet> accountList = AccountInfo.extractAccounts( 266 Futures.getUnchecked(mAccountsFuture)); 267 268 // There are three possibilities: 269 // - more than one accounts -> ask the user 270 // - just one account -> use the account without asking the user 271 // - no account -> use phone-local storage without asking the user 272 final int size = accountList.size(); 273 if (size > 1) { 274 // Send over to the account selector 275 final Bundle args = new Bundle(); 276 args.putInt(KEY_RES_ID, resId); 277 args.putInt(KEY_SUBSCRIPTION_ID, subscriptionId); 278 SelectAccountDialogFragment.show( 279 getFragmentManager(), R.string.dialog_new_contact_account, 280 AccountTypeManager.AccountFilter.CONTACTS_WRITABLE, args); 281 } else { 282 AccountSelectionUtil.doImport(getActivity(), resId, 283 (size == 1 ? accountList.get(0) : null), 284 (CompatUtils.isMSIMCompatible() ? subscriptionId : -1)); 285 } 286 } 287 getSimDescription(SimCard sim, int index)288 private CharSequence getSimDescription(SimCard sim, int index) { 289 final CharSequence name = sim.getDisplayName(); 290 if (name != null) { 291 return getString(R.string.import_from_sim_summary_fmt, name); 292 } else { 293 return getString(R.string.import_from_sim_summary_fmt, String.valueOf(index)); 294 } 295 } 296 297 private static class AdapterEntry { 298 public final CharSequence mLabel; 299 public final int mChoiceResourceId; 300 public final SimCard mSim; 301 AdapterEntry(CharSequence label, int resId, SimCard sim)302 public AdapterEntry(CharSequence label, int resId, SimCard sim) { 303 mLabel = label; 304 mChoiceResourceId = resId; 305 mSim = sim; 306 } 307 AdapterEntry(String label, int resId)308 public AdapterEntry(String label, int resId) { 309 // Store a nonsense value for mSubscriptionId. If this constructor is used, 310 // the mSubscriptionId value should not be read later. 311 this(label, resId, /* sim= */ null); 312 } 313 } 314 } 315