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