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