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