1 /*
2  * Copyright (C) 2016 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 package com.android.emergency.preferences;
17 
18 import android.content.Context;
19 import android.content.SharedPreferences;
20 import android.content.res.TypedArray;
21 import android.net.Uri;
22 import androidx.annotation.NonNull;
23 import android.os.UserManager;
24 import androidx.preference.Preference;
25 import androidx.preference.PreferenceCategory;
26 import androidx.preference.PreferenceManager;
27 import android.util.AttributeSet;
28 import android.util.Log;
29 import android.widget.Toast;
30 
31 import com.android.emergency.EmergencyContactManager;
32 import com.android.emergency.R;
33 import com.android.emergency.ReloadablePreferenceInterface;
34 import com.android.emergency.util.PreferenceUtils;
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.logging.MetricsLogger;
37 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
38 
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.Iterator;
42 import java.util.List;
43 import java.util.regex.Pattern;
44 
45 /**
46  * Custom {@link PreferenceCategory} that deals with contacts being deleted from the contacts app.
47  *
48  * <p>Contacts are stored internally using their ContactsContract.CommonDataKinds.Phone.CONTENT_URI.
49  */
50 public class EmergencyContactsPreference extends PreferenceCategory
51         implements ReloadablePreferenceInterface,
52         ContactPreference.RemoveContactPreferenceListener {
53 
54     private static final String TAG = "EmergencyContactsPreference";
55 
56     private static final String CONTACT_SEPARATOR = "|";
57     private static final String QUOTE_CONTACT_SEPARATOR = Pattern.quote(CONTACT_SEPARATOR);
58     private static final ContactValidator DEFAULT_CONTACT_VALIDATOR = new ContactValidator() {
59         @Override
60         public boolean isValidEmergencyContact(Context context, Uri phoneUri) {
61             return EmergencyContactManager.isValidEmergencyContact(context, phoneUri);
62         }
63     };
64 
65     private final ContactValidator mContactValidator;
66     private final ContactPreference.ContactFactory mContactFactory;
67     /** Stores the emergency contact's ContactsContract.CommonDataKinds.Phone.CONTENT_URI */
68     private List<Uri> mEmergencyContacts = new ArrayList<Uri>();
69     private boolean mEmergencyContactsSet = false;
70 
71     /**
72      * Interface for getting a contact for a phone number Uri.
73      */
74     public interface ContactValidator {
75         /**
76          * Checks whether a given phone Uri represents a valid emergency contact.
77          *
78          * @param context The context to use.
79          * @param phoneUri The phone uri.
80          * @return whether the given phone Uri is a valid emergency contact.
81          */
isValidEmergencyContact(Context context, Uri phoneUri)82         boolean isValidEmergencyContact(Context context, Uri phoneUri);
83     }
84 
EmergencyContactsPreference(Context context, AttributeSet attrs)85     public EmergencyContactsPreference(Context context, AttributeSet attrs) {
86         this(context, attrs, DEFAULT_CONTACT_VALIDATOR, ContactPreference.DEFAULT_CONTACT_FACTORY);
87     }
88 
89     @VisibleForTesting
EmergencyContactsPreference(Context context, AttributeSet attrs, @NonNull ContactValidator contactValidator, @NonNull ContactPreference.ContactFactory contactFactory)90     EmergencyContactsPreference(Context context, AttributeSet attrs,
91             @NonNull ContactValidator contactValidator,
92             @NonNull ContactPreference.ContactFactory contactFactory) {
93         super(context, attrs);
94         mContactValidator = contactValidator;
95         mContactFactory = contactFactory;
96     }
97 
98     @Override
onSetInitialValue(boolean restorePersistedValue, Object defaultValue)99     protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
100         setEmergencyContacts(restorePersistedValue ?
101                 getPersistedEmergencyContacts() :
102                 deserializeAndFilter(getKey(),
103                         getContext(),
104                         (String) defaultValue,
105                         mContactValidator));
106     }
107 
108     @Override
onGetDefaultValue(TypedArray a, int index)109     protected Object onGetDefaultValue(TypedArray a, int index) {
110         return a.getString(index);
111     }
112 
113     @Override
reloadFromPreference()114     public void reloadFromPreference() {
115         setEmergencyContacts(getPersistedEmergencyContacts());
116     }
117 
118     @Override
isNotSet()119     public boolean isNotSet() {
120         return mEmergencyContacts.isEmpty();
121     }
122 
123     @Override
onRemoveContactPreference(ContactPreference contactPreference)124     public void onRemoveContactPreference(ContactPreference contactPreference) {
125         Uri phoneUriToRemove = contactPreference.getPhoneUri();
126         if (mEmergencyContacts.contains(phoneUriToRemove)) {
127             List<Uri> updatedContacts = new ArrayList<Uri>(mEmergencyContacts);
128             if (updatedContacts.remove(phoneUriToRemove) && callChangeListener(updatedContacts)) {
129                 MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETE_EMERGENCY_CONTACT);
130                 setEmergencyContacts(updatedContacts);
131             }
132         }
133     }
134 
135     /**
136      * Adds a new emergency contact. The {@code phoneUri} is the
137      * ContactsContract.CommonDataKinds.Phone.CONTENT_URI corresponding to the
138      * contact's selected phone number.
139      */
addNewEmergencyContact(Uri phoneUri)140     public void addNewEmergencyContact(Uri phoneUri) {
141         if (mEmergencyContacts.contains(phoneUri)) {
142             return;
143         }
144         if (!mContactValidator.isValidEmergencyContact(getContext(), phoneUri)) {
145             Toast.makeText(getContext(), getContext().getString(R.string.fail_add_contact),
146                 Toast.LENGTH_LONG).show();
147             return;
148         }
149         List<Uri> updatedContacts = new ArrayList<Uri>(mEmergencyContacts);
150         if (updatedContacts.add(phoneUri) && callChangeListener(updatedContacts)) {
151             MetricsLogger.action(getContext(), MetricsEvent.ACTION_ADD_EMERGENCY_CONTACT);
152             setEmergencyContacts(updatedContacts);
153         }
154     }
155 
156     @VisibleForTesting
getEmergencyContacts()157     public List<Uri> getEmergencyContacts() {
158         return mEmergencyContacts;
159     }
160 
setEmergencyContacts(List<Uri> emergencyContacts)161     public void setEmergencyContacts(List<Uri> emergencyContacts) {
162         final boolean changed = !mEmergencyContacts.equals(emergencyContacts);
163         if (changed || !mEmergencyContactsSet) {
164             mEmergencyContacts = emergencyContacts;
165             mEmergencyContactsSet = true;
166             persistEmergencyContacts(emergencyContacts);
167             if (changed) {
168                 notifyChanged();
169             }
170         }
171 
172         while (getPreferenceCount() - emergencyContacts.size() > 0) {
173             removePreference(getPreference(0));
174         }
175 
176         // Reload the preferences or add new ones if necessary
177         Iterator<Uri> it = emergencyContacts.iterator();
178         int i = 0;
179         Uri phoneUri = null;
180         List<Uri> updatedEmergencyContacts = null;
181         while (it.hasNext()) {
182             ContactPreference contactPreference = null;
183             phoneUri = it.next();
184             // setPhoneUri may throw an IllegalArgumentException (also called in the constructor
185             // of ContactPreference)
186             try {
187                 if (i < getPreferenceCount()) {
188                     contactPreference = (ContactPreference) getPreference(i);
189                     contactPreference.setPhoneUri(phoneUri);
190                 } else {
191                     contactPreference =
192                             new ContactPreference(getContext(), phoneUri, mContactFactory);
193                     onBindContactView(contactPreference);
194                     addPreference(contactPreference);
195                 }
196                 i++;
197                 MetricsLogger.action(getContext(), MetricsEvent.ACTION_GET_CONTACT, 0);
198             } catch (IllegalArgumentException e) {
199                 Log.w(TAG, "Caught IllegalArgumentException for phoneUri:"
200                     + phoneUri == null ? "" : phoneUri.toString(), e);
201                 MetricsLogger.action(getContext(), MetricsEvent.ACTION_GET_CONTACT, 1);
202                 if (updatedEmergencyContacts == null) {
203                     updatedEmergencyContacts = new ArrayList<>(emergencyContacts);
204                 }
205                 updatedEmergencyContacts.remove(phoneUri);
206             }
207         }
208         if (updatedEmergencyContacts != null) {
209             // Set the contacts again: something went wrong when retrieving information about the
210             // stored phone Uris.
211             setEmergencyContacts(updatedEmergencyContacts);
212         }
213         // Enable or disable the settings suggestion, as appropriate.
214         PreferenceUtils.updateSettingsSuggestionState(getContext());
215         MetricsLogger.histogram(getContext(),
216                                 "num_emergency_contacts",
217                                 Math.min(3, emergencyContacts.size()));
218     }
219 
220     /**
221      * Called when {@code contactPreference} has been added to this category. You may now set
222      * listeners.
223      */
onBindContactView(final ContactPreference contactPreference)224     protected void onBindContactView(final ContactPreference contactPreference) {
225         contactPreference.setRemoveContactPreferenceListener(this);
226         contactPreference
227                 .setOnPreferenceClickListener(
228                         new Preference.OnPreferenceClickListener() {
229                             @Override
230                             public boolean onPreferenceClick(Preference preference) {
231                                 contactPreference.displayContact();
232                                 return true;
233                             }
234                         }
235                 );
236     }
237 
getPersistedEmergencyContacts()238     private List<Uri> getPersistedEmergencyContacts() {
239         return deserializeAndFilter(getKey(), getContext(), getPersistedString(""),
240                 mContactValidator);
241     }
242 
243     @Override
getPersistedString(String defaultReturnValue)244     protected String getPersistedString(String defaultReturnValue) {
245         try {
246             return super.getPersistedString(defaultReturnValue);
247         } catch (ClassCastException e) {
248             // Protect against b/28194605: We used to store the contacts using a string set.
249             // If it was a string set, a ClassCastException would have been thrown, and we can
250             // ignore its value. If it is stored as a value of another type, we are potentially
251             // squelching an exception here, but returning the default return value seems reasonable
252             // in either case.
253             return defaultReturnValue;
254         }
255     }
256 
257     /**
258      * Converts the string representing the emergency contacts to a list of Uris and only keeps
259      * those corresponding to still existing contacts. It persists the contacts if at least one
260      * contact was does not exist anymore.
261      */
deserializeAndFilter(String key, Context context, String emergencyContactString)262     public static List<Uri> deserializeAndFilter(String key, Context context,
263                                                  String emergencyContactString) {
264         return deserializeAndFilter(key, context, emergencyContactString,
265                 DEFAULT_CONTACT_VALIDATOR);
266     }
267 
268     /** Converts the Uris to a string representation. */
serialize(List<Uri> emergencyContacts)269     public static String serialize(List<Uri> emergencyContacts) {
270         StringBuilder sb = new StringBuilder();
271         for (int i = 0; i < emergencyContacts.size(); i++) {
272             sb.append(emergencyContacts.get(i).toString());
273             sb.append(CONTACT_SEPARATOR);
274         }
275 
276         if (sb.length() > 0) {
277             sb.setLength(sb.length() - 1);
278         }
279         return sb.toString();
280     }
281 
282     @VisibleForTesting
persistEmergencyContacts(List<Uri> emergencyContacts)283     void persistEmergencyContacts(List<Uri> emergencyContacts) {
284         // Avoid persisting emergency contacts in direct boot mode.
285         if (isUserUnlocked(getContext())) {
286             persistString(serialize(emergencyContacts));
287         }
288     }
289 
deserializeAndFilter(String key, Context context, String emergencyContactString, ContactValidator contactValidator)290     private static List<Uri> deserializeAndFilter(String key, Context context,
291                                                   String emergencyContactString,
292                                                   ContactValidator contactValidator) {
293         String[] emergencyContactsArray =
294                 emergencyContactString.split(QUOTE_CONTACT_SEPARATOR);
295         List<Uri> filteredEmergencyContacts = new ArrayList<Uri>(emergencyContactsArray.length);
296         for (String emergencyContact : emergencyContactsArray) {
297             Uri phoneUri = Uri.parse(emergencyContact);
298             if (contactValidator.isValidEmergencyContact(context, phoneUri)) {
299                 filteredEmergencyContacts.add(phoneUri);
300             }
301         }
302         // If not all contacts were added, then we need to overwrite the emergency contacts stored
303         // in shared preferences. This deals with emergency contacts being deleted from contacts:
304         // currently we have no way to being notified when this happens.
305         if (filteredEmergencyContacts.size() != emergencyContactsArray.length) {
306             // Avoid updating emergency contacts in direct boot mode.
307             if (isUserUnlocked(context)) {
308                 String emergencyContactStrings = serialize(filteredEmergencyContacts);
309                 SharedPreferences sharedPreferences =
310                         PreferenceManager.getDefaultSharedPreferences(context);
311                 sharedPreferences.edit().putString(key, emergencyContactStrings).commit();
312             }
313         }
314         return filteredEmergencyContacts;
315     }
316 
isUserUnlocked(Context context)317     private static boolean isUserUnlocked(Context context) {
318         UserManager userManager = context.getSystemService(UserManager.class);
319         return userManager != null && userManager.isUserUnlocked();
320     }
321 
322 }
323