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