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