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