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