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