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