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.preference;
18 
19 import android.accounts.Account;
20 import android.app.Activity;
21 import android.app.LoaderManager;
22 import android.content.BroadcastReceiver;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.CursorLoader;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.Loader;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.content.res.Resources;
32 import android.database.Cursor;
33 import android.icu.text.MessageFormat;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.os.StrictMode;
37 import android.preference.Preference;
38 import android.preference.PreferenceFragment;
39 import android.provider.BlockedNumberContract;
40 import android.provider.ContactsContract.Contacts;
41 import android.provider.ContactsContract.DisplayNameSources;
42 import android.provider.ContactsContract.Profile;
43 import android.provider.ContactsContract.Settings;
44 import com.google.android.material.snackbar.Snackbar;
45 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
46 import android.telecom.TelecomManager;
47 import android.telephony.TelephonyManager;
48 import android.text.BidiFormatter;
49 import android.text.TextDirectionHeuristics;
50 import android.view.LayoutInflater;
51 import android.view.View;
52 import android.view.ViewGroup;
53 import android.widget.FrameLayout;
54 
55 import com.android.contacts.ContactsUtils;
56 import com.android.contacts.R;
57 import com.android.contacts.SimImportService;
58 import com.android.contacts.compat.TelecomManagerUtil;
59 import com.android.contacts.compat.TelephonyManagerCompat;
60 import com.android.contacts.interactions.ExportDialogFragment;
61 import com.android.contacts.interactions.ImportDialogFragment;
62 import com.android.contacts.list.ContactListFilter;
63 import com.android.contacts.list.ContactListFilterController;
64 import com.android.contacts.logging.ScreenEvent.ScreenType;
65 import com.android.contacts.model.AccountTypeManager;
66 import com.android.contacts.model.account.AccountInfo;
67 import com.android.contacts.model.account.AccountWithDataSet;
68 import com.android.contacts.model.account.AccountsLoader;
69 import com.android.contacts.util.AccountFilterUtil;
70 import com.android.contacts.util.ImplicitIntentsUtil;
71 import com.android.contactsbind.HelpUtils;
72 
73 import java.util.Collections;
74 import java.util.HashMap;
75 import java.util.List;
76 import java.util.Locale;
77 import java.util.Map;
78 
79 /**
80  * This fragment shows the preferences for "display options"
81  */
82 public class DisplayOptionsPreferenceFragment extends PreferenceFragment
83         implements Preference.OnPreferenceClickListener, AccountsLoader.AccountsListener {
84 
85     private static final int REQUEST_CODE_CUSTOM_CONTACTS_FILTER = 0;
86     private static final int REQUEST_CODE_SET_DEFAULT_ACCOUNT_CP2 = 1;
87 
88     private static final String ARG_CONTACTS_AVAILABLE = "are_contacts_available";
89     private static final String ARG_NEW_LOCAL_PROFILE = "new_local_profile";
90 
91     private static final String KEY_ABOUT = "about";
92     private static final String KEY_ACCOUNTS = "accounts";
93     private static final String KEY_DEFAULT_ACCOUNT = "defaultAccount";
94     private static final String KEY_BLOCKED_NUMBERS = "blockedNumbers";
95     private static final String KEY_DISPLAY_ORDER = "displayOrder";
96     private static final String KEY_CUSTOM_CONTACTS_FILTER = "customContactsFilter";
97     private static final String KEY_IMPORT = "import";
98     private static final String KEY_EXPORT = "export";
99     private static final String KEY_MY_INFO = "myInfo";
100     private static final String KEY_SORT_ORDER = "sortOrder";
101     private static final String KEY_PHONETIC_NAME_DISPLAY = "phoneticNameDisplay";
102 
103     private static final int LOADER_PROFILE = 0;
104     private static final int LOADER_ACCOUNTS = 1;
105 
106     /**
107      * Callbacks for hosts of the {@link DisplayOptionsPreferenceFragment}.
108      */
109     public interface ProfileListener  {
110         /**
111          * Invoked after profile has been loaded.
112          */
onProfileLoaded(Cursor data)113         void onProfileLoaded(Cursor data);
114     }
115 
116     /**
117      * The projections that are used to obtain user profile
118      */
119     public static class ProfileQuery {
120         /**
121          * Not instantiable.
122          */
ProfileQuery()123         private ProfileQuery() {}
124 
125         private static final String[] PROFILE_PROJECTION_PRIMARY = new String[] {
126                 Contacts._ID,                           // 0
127                 Contacts.DISPLAY_NAME_PRIMARY,          // 1
128                 Contacts.IS_USER_PROFILE,               // 2
129                 Contacts.DISPLAY_NAME_SOURCE,           // 3
130         };
131 
132         private static final String[] PROFILE_PROJECTION_ALTERNATIVE = new String[] {
133                 Contacts._ID,                           // 0
134                 Contacts.DISPLAY_NAME_ALTERNATIVE,      // 1
135                 Contacts.IS_USER_PROFILE,               // 2
136                 Contacts.DISPLAY_NAME_SOURCE,           // 3
137         };
138 
139         public static final int CONTACT_ID               = 0;
140         public static final int CONTACT_DISPLAY_NAME     = 1;
141         public static final int CONTACT_IS_USER_PROFILE  = 2;
142         public static final int DISPLAY_NAME_SOURCE      = 3;
143     }
144 
145     private String mNewLocalProfileExtra;
146     private boolean mAreContactsAvailable;
147 
148     private boolean mHasProfile;
149     private long mProfileContactId;
150 
151     private Preference mMyInfoPreference;
152 
153     private ProfileListener mListener;
154 
155     private ViewGroup mRootView;
156     private SaveServiceResultListener mSaveServiceListener;
157 
158     private List<AccountInfo> accounts = Collections.emptyList();
159 
160     private final LoaderManager.LoaderCallbacks<Cursor> mProfileLoaderListener =
161             new LoaderManager.LoaderCallbacks<Cursor>() {
162 
163         @Override
164         public CursorLoader onCreateLoader(int id, Bundle args) {
165             final CursorLoader loader = createCursorLoader(getContext());
166             loader.setUri(Profile.CONTENT_URI);
167             loader.setProjection(getProjection(getContext()));
168             return loader;
169         }
170 
171         @Override
172         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
173             if (mListener != null) {
174                 mListener.onProfileLoaded(data);
175             }
176         }
177 
178         public void onLoaderReset(Loader<Cursor> loader) {
179         }
180     };
181 
newInstance(String newLocalProfileExtra, boolean areContactsAvailable)182     public static DisplayOptionsPreferenceFragment newInstance(String newLocalProfileExtra,
183             boolean areContactsAvailable) {
184         final DisplayOptionsPreferenceFragment fragment = new DisplayOptionsPreferenceFragment();
185         final Bundle args = new Bundle();
186         args.putString(ARG_NEW_LOCAL_PROFILE, newLocalProfileExtra);
187         args.putBoolean(ARG_CONTACTS_AVAILABLE, areContactsAvailable);
188         fragment.setArguments(args);
189         return fragment;
190     }
191 
192     @Override
onAttach(Activity activity)193     public void onAttach(Activity activity) {
194         super.onAttach(activity);
195         try {
196             mListener = (ProfileListener) activity;
197         } catch (ClassCastException e) {
198             throw new ClassCastException(activity.toString() + " must implement ProfileListener");
199         }
200     }
201 
202     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)203     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
204         // Wrap the preference view in a FrameLayout so we can show a snackbar
205         mRootView = new FrameLayout(getActivity());
206         final View list = super.onCreateView(inflater, mRootView, savedInstanceState);
207         mRootView.addView(list);
208         return mRootView;
209     }
210 
211     @Override
onViewCreated(View view, Bundle savedInstanceState)212     public void onViewCreated(View view, Bundle savedInstanceState) {
213         super.onViewCreated(view, savedInstanceState);
214 
215         mSaveServiceListener = new SaveServiceResultListener();
216         LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
217                 mSaveServiceListener,
218                 new IntentFilter(SimImportService.BROADCAST_SIM_IMPORT_COMPLETE));
219     }
220 
221     @Override
onCreate(Bundle savedInstanceState)222     public void onCreate(Bundle savedInstanceState) {
223         super.onCreate(savedInstanceState);
224 
225         // Load the preferences from an XML resource
226         addPreferencesFromResource(R.xml.preference_display_options);
227 
228         final Bundle args = getArguments();
229         mNewLocalProfileExtra = args.getString(ARG_NEW_LOCAL_PROFILE);
230         mAreContactsAvailable = args.getBoolean(ARG_CONTACTS_AVAILABLE);
231 
232         removeUnsupportedPreferences();
233 
234         mMyInfoPreference = findPreference(KEY_MY_INFO);
235 
236         final Preference accountsPreference = findPreference(KEY_ACCOUNTS);
237         accountsPreference.setOnPreferenceClickListener(this);
238 
239         final Preference importPreference = findPreference(KEY_IMPORT);
240         importPreference.setOnPreferenceClickListener(this);
241 
242         final Preference exportPreference = findPreference(KEY_EXPORT);
243         if (exportPreference != null) {
244             exportPreference.setOnPreferenceClickListener(this);
245         }
246 
247         final Preference blockedNumbersPreference = findPreference(KEY_BLOCKED_NUMBERS);
248         if (blockedNumbersPreference != null) {
249             blockedNumbersPreference.setOnPreferenceClickListener(this);
250         }
251 
252         final Preference aboutPreference = findPreference(KEY_ABOUT);
253         if (aboutPreference != null) {
254             aboutPreference.setOnPreferenceClickListener(this);
255         }
256 
257         final Preference customFilterPreference = findPreference(KEY_CUSTOM_CONTACTS_FILTER);
258         if (customFilterPreference != null) {
259             customFilterPreference.setOnPreferenceClickListener(this);
260             setCustomContactsFilterSummary();
261         }
262 
263         final Preference defaultAccountPreference = findPreference(KEY_DEFAULT_ACCOUNT);
264         if (defaultAccountPreference != null) {
265             defaultAccountPreference.setOnPreferenceClickListener(this);
266             defaultAccountPreference.setSummary(getDefaultAccountSummary());
267         }
268     }
269 
270     @Override
onActivityCreated(Bundle savedInstanceState)271     public void onActivityCreated(Bundle savedInstanceState) {
272         super.onActivityCreated(savedInstanceState);
273         getLoaderManager().initLoader(LOADER_PROFILE, null, mProfileLoaderListener);
274         AccountsLoader.loadAccounts(this, LOADER_ACCOUNTS, AccountTypeManager.writableFilter());
275     }
276 
277     @Override
onDestroyView()278     public void onDestroyView() {
279         super.onDestroyView();
280         LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mSaveServiceListener);
281         mRootView = null;
282     }
283 
updateMyInfoPreference(boolean hasProfile, String displayName, long contactId, int displayNameSource)284     public void updateMyInfoPreference(boolean hasProfile, String displayName, long contactId,
285             int displayNameSource) {
286         final CharSequence summary = !hasProfile ?
287                 getString(R.string.set_up_profile) :
288                 displayNameSource == DisplayNameSources.PHONE ?
289                 BidiFormatter.getInstance().unicodeWrap(displayName, TextDirectionHeuristics.LTR) :
290                 displayName;
291         mMyInfoPreference.setSummary(summary);
292         mHasProfile = hasProfile;
293         mProfileContactId = contactId;
294         mMyInfoPreference.setOnPreferenceClickListener(this);
295     }
296 
removeUnsupportedPreferences()297     private void removeUnsupportedPreferences() {
298         // Disable sort order for CJK locales where it is not supported
299         final Resources resources = getResources();
300         if (!resources.getBoolean(R.bool.config_sort_order_user_changeable)) {
301             getPreferenceScreen().removePreference(findPreference(KEY_SORT_ORDER));
302         }
303 
304         if (!resources.getBoolean(R.bool.config_phonetic_name_display_user_changeable)) {
305             getPreferenceScreen().removePreference(findPreference(KEY_PHONETIC_NAME_DISPLAY));
306         }
307 
308         if (HelpUtils.isHelpAndFeedbackAvailable()) {
309             getPreferenceScreen().removePreference(findPreference(KEY_ABOUT));
310         }
311 
312         // Disable display order for CJK locales as well
313         if (!resources.getBoolean(R.bool.config_display_order_user_changeable)) {
314             getPreferenceScreen().removePreference(findPreference(KEY_DISPLAY_ORDER));
315         }
316 
317         final boolean isPhone = TelephonyManagerCompat.isVoiceCapable(
318                 (TelephonyManager) getContext().getSystemService(Context.TELEPHONY_SERVICE));
319         final boolean showBlockedNumbers = isPhone && ContactsUtils.FLAG_N_FEATURE
320                 && BlockedNumberContract.canCurrentUserBlockNumbers(getContext());
321         if (!showBlockedNumbers) {
322             getPreferenceScreen().removePreference(findPreference(KEY_BLOCKED_NUMBERS));
323         }
324 
325         if (!mAreContactsAvailable) {
326             getPreferenceScreen().removePreference(findPreference(KEY_EXPORT));
327         }
328     }
329 
330     @Override
onAccountsLoaded(List<AccountInfo> accounts)331     public void onAccountsLoaded(List<AccountInfo> accounts) {
332         // Hide accounts preferences if no writable accounts exist
333         this.accounts = accounts;
334         final Preference defaultAccountPreference =
335                 findPreference(KEY_DEFAULT_ACCOUNT);
336         defaultAccountPreference.setSummary(getDefaultAccountSummary());
337     }
338 
339     @Override
getContext()340     public Context getContext() {
341         return getActivity();
342     }
343 
createCursorLoader(Context context)344     private CursorLoader createCursorLoader(Context context) {
345         return new CursorLoader(context) {
346             @Override
347             protected Cursor onLoadInBackground() {
348                 try {
349                     return super.onLoadInBackground();
350                 } catch (RuntimeException e) {
351                     return null;
352                 }
353             }
354         };
355     }
356 
357     private String[] getProjection(Context context) {
358         final ContactsPreferences contactsPrefs = new ContactsPreferences(context);
359         final int displayOrder = contactsPrefs.getDisplayOrder();
360         if (displayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
361             return ProfileQuery.PROFILE_PROJECTION_PRIMARY;
362         }
363         return ProfileQuery.PROFILE_PROJECTION_ALTERNATIVE;
364     }
365 
366     @Override
367     public boolean onPreferenceClick(Preference p) {
368         final String prefKey = p.getKey();
369 
370         if (KEY_ABOUT.equals(prefKey)) {
371             ((ContactsPreferenceActivity) getActivity()).showAboutFragment();
372             return true;
373         } else if (KEY_IMPORT.equals(prefKey)) {
374             ImportDialogFragment.show(getFragmentManager());
375             return true;
376         } else if (KEY_EXPORT.equals(prefKey)) {
377             ExportDialogFragment.show(getFragmentManager(), ContactsPreferenceActivity.class,
378                     ExportDialogFragment.EXPORT_MODE_ALL_CONTACTS);
379             return true;
380         } else if (KEY_MY_INFO.equals(prefKey)) {
381             if (mHasProfile) {
382                 final Uri uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, mProfileContactId);
383                 ImplicitIntentsUtil.startQuickContact(getActivity(), uri, ScreenType.ME_CONTACT);
384             } else {
385                 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
386                 intent.putExtra(mNewLocalProfileExtra, true);
387                 ImplicitIntentsUtil.startActivityInApp(getActivity(), intent);
388             }
389             return true;
390         } else if (KEY_ACCOUNTS.equals(prefKey)) {
391             ImplicitIntentsUtil.startActivityOutsideApp(getContext(),
392                     ImplicitIntentsUtil.getIntentForAddingAccount());
393             return true;
394         } else if (KEY_BLOCKED_NUMBERS.equals(prefKey)) {
395             final Intent intent = TelecomManagerUtil.createManageBlockedNumbersIntent(
396                     (TelecomManager) getContext().getSystemService(Context.TELECOM_SERVICE));
397             startActivity(intent);
398             return true;
399         } else if (KEY_CUSTOM_CONTACTS_FILTER.equals(prefKey)) {
400             final ContactListFilter filter =
401                     ContactListFilterController.getInstance(getContext()).getFilter();
402             AccountFilterUtil.startAccountFilterActivityForResult(
403                     this, REQUEST_CODE_CUSTOM_CONTACTS_FILTER, filter);
404         } else if (KEY_DEFAULT_ACCOUNT.equals(prefKey)) {
405             String packageName = getSetDefaultAccountActivityPackage();
406             Intent intent = new Intent(Settings.ACTION_SET_DEFAULT_ACCOUNT);
407             if (packageName != null) {
408                 intent.setPackage(packageName);
409                 startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_ACCOUNT_CP2);
410             }
411         }
412         return false;
413     }
414 
415     @Override
416     public void onActivityResult(int requestCode, int resultCode, Intent data) {
417         if (requestCode == REQUEST_CODE_CUSTOM_CONTACTS_FILTER
418                 && resultCode == Activity.RESULT_OK) {
419             AccountFilterUtil.handleAccountFilterResult(
420                     ContactListFilterController.getInstance(getContext()), resultCode, data);
421             setCustomContactsFilterSummary();
422         } else if (requestCode == REQUEST_CODE_SET_DEFAULT_ACCOUNT_CP2
423                 && resultCode == Activity.RESULT_OK) {
424             final Preference defaultAccountPreference = findPreference(KEY_DEFAULT_ACCOUNT);
425             if (defaultAccountPreference != null) {
426                 defaultAccountPreference.setSummary(getDefaultAccountSummary());
427             }
428         } else {
429             super.onActivityResult(requestCode, resultCode, data);
430         }
431     }
432 
433     private void setCustomContactsFilterSummary() {
434         final Preference customFilterPreference = findPreference(KEY_CUSTOM_CONTACTS_FILTER);
435         if (customFilterPreference != null) {
436             final ContactListFilter filter =
437                     ContactListFilterController.getInstance(getContext()).getPersistedFilter();
438             if (filter != null) {
439                 if (filter.filterType == ContactListFilter.FILTER_TYPE_DEFAULT ||
440                         filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
441                     customFilterPreference.setSummary(R.string.list_filter_all_accounts);
442                 } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
443                     customFilterPreference.setSummary(R.string.listCustomView);
444                 } else {
445                     customFilterPreference.setSummary(null);
446                 }
447             }
448         }
449     }
450 
451     private CharSequence getDefaultAccountSummary() {
452         ContactsPreferences preferences = new ContactsPreferences(getContext());
453         AccountWithDataSet defaultAccountWithDataSet = preferences.getDefaultAccount();
454         AccountInfo defaultAccountInfo = AccountInfo.getAccount(
455                 accounts, defaultAccountWithDataSet);
456         if (defaultAccountInfo != null) {
457             return defaultAccountInfo.getNameLabel();
458         } else {
459             return null;
460         }
461     }
462 
463     private String getSetDefaultAccountActivityPackage() {
464         // Only preloaded Contacts App has the permission to call setDefaultAccount.
465         Intent intent = new Intent(Settings.ACTION_SET_DEFAULT_ACCOUNT);
466         PackageManager packageManager = getContext().getPackageManager();
467         List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(intent, 0);
468         for (ResolveInfo resolveInfo : resolveInfos) {
469             String packageName = resolveInfo.activityInfo.packageName;
470             if (packageManager.checkPermission(
471                     android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS, packageName)
472                     == PackageManager.PERMISSION_GRANTED) {
473                 return packageName;
474             }
475         }
476         return null;
477     }
478 
479     private class SaveServiceResultListener extends BroadcastReceiver {
480         @Override
481         public void onReceive(Context context, Intent intent) {
482             final long now = System.currentTimeMillis();
483             final long opStart = intent.getLongExtra(
484                     SimImportService.EXTRA_OPERATION_REQUESTED_AT_TIME, now);
485 
486             // If it's been over 30 seconds the user is likely in a different context so suppress
487             // the toast message.
488             if (now - opStart > 30*1000) return;
489 
490             final int code = intent.getIntExtra(SimImportService.EXTRA_RESULT_CODE,
491                     SimImportService.RESULT_UNKNOWN);
492             final int count = intent.getIntExtra(SimImportService.EXTRA_RESULT_COUNT, -1);
493             if (code == SimImportService.RESULT_SUCCESS && count > 0) {
494                 MessageFormat msgFormat = new MessageFormat(
495                     getResources().getString(R.string.sim_import_success_toast_fmt),
496                     Locale.getDefault());
497                 Map<String, Object> arguments = new HashMap<>();
498                 arguments.put("count", count);
499                 Snackbar.make(mRootView, msgFormat.format(arguments),
500                         Snackbar.LENGTH_LONG).show();
501             } else if (code == SimImportService.RESULT_FAILURE) {
502                 Snackbar.make(mRootView, R.string.sim_import_failed_toast,
503                         Snackbar.LENGTH_LONG).show();
504             }
505         }
506     }
507 }
508 
509