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