/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.emergency.preferences; import android.content.Context; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.net.Uri; import androidx.annotation.NonNull; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceManager; import android.util.AttributeSet; import android.util.Log; import android.widget.Toast; import com.android.emergency.EmergencyContactManager; import com.android.emergency.R; import com.android.emergency.ReloadablePreferenceInterface; import com.android.emergency.util.PreferenceUtils; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; /** * Custom {@link PreferenceCategory} that deals with contacts being deleted from the contacts app. * *

Contacts are stored internally using their ContactsContract.CommonDataKinds.Phone.CONTENT_URI. */ public class EmergencyContactsPreference extends PreferenceCategory implements ReloadablePreferenceInterface, ContactPreference.RemoveContactPreferenceListener { private static final String TAG = "EmergencyContactsPreference"; private static final String CONTACT_SEPARATOR = "|"; private static final String QUOTE_CONTACT_SEPARATOR = Pattern.quote(CONTACT_SEPARATOR); private static final ContactValidator DEFAULT_CONTACT_VALIDATOR = new ContactValidator() { @Override public boolean isValidEmergencyContact(Context context, Uri phoneUri) { return EmergencyContactManager.isValidEmergencyContact(context, phoneUri); } }; private final ContactValidator mContactValidator; private final ContactPreference.ContactFactory mContactFactory; /** Stores the emergency contact's ContactsContract.CommonDataKinds.Phone.CONTENT_URI */ private List mEmergencyContacts = new ArrayList(); private boolean mEmergencyContactsSet = false; /** * Interface for getting a contact for a phone number Uri. */ public interface ContactValidator { /** * Checks whether a given phone Uri represents a valid emergency contact. * * @param context The context to use. * @param phoneUri The phone uri. * @return whether the given phone Uri is a valid emergency contact. */ boolean isValidEmergencyContact(Context context, Uri phoneUri); } public EmergencyContactsPreference(Context context, AttributeSet attrs) { this(context, attrs, DEFAULT_CONTACT_VALIDATOR, ContactPreference.DEFAULT_CONTACT_FACTORY); } @VisibleForTesting EmergencyContactsPreference(Context context, AttributeSet attrs, @NonNull ContactValidator contactValidator, @NonNull ContactPreference.ContactFactory contactFactory) { super(context, attrs); mContactValidator = contactValidator; mContactFactory = contactFactory; } @Override protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { setEmergencyContacts(restorePersistedValue ? getPersistedEmergencyContacts() : deserializeAndFilter(getKey(), getContext(), (String) defaultValue, mContactValidator)); } @Override protected Object onGetDefaultValue(TypedArray a, int index) { return a.getString(index); } @Override public void reloadFromPreference() { setEmergencyContacts(getPersistedEmergencyContacts()); } @Override public boolean isNotSet() { return mEmergencyContacts.isEmpty(); } @Override public void onRemoveContactPreference(ContactPreference contactPreference) { Uri phoneUriToRemove = contactPreference.getPhoneUri(); if (mEmergencyContacts.contains(phoneUriToRemove)) { List updatedContacts = new ArrayList(mEmergencyContacts); if (updatedContacts.remove(phoneUriToRemove) && callChangeListener(updatedContacts)) { MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETE_EMERGENCY_CONTACT); setEmergencyContacts(updatedContacts); } } } /** * Adds a new emergency contact. The {@code phoneUri} is the * ContactsContract.CommonDataKinds.Phone.CONTENT_URI corresponding to the * contact's selected phone number. */ public void addNewEmergencyContact(Uri phoneUri) { if (mEmergencyContacts.contains(phoneUri)) { return; } if (!mContactValidator.isValidEmergencyContact(getContext(), phoneUri)) { Toast.makeText(getContext(), getContext().getString(R.string.fail_add_contact), Toast.LENGTH_LONG).show(); return; } List updatedContacts = new ArrayList(mEmergencyContacts); if (updatedContacts.add(phoneUri) && callChangeListener(updatedContacts)) { MetricsLogger.action(getContext(), MetricsEvent.ACTION_ADD_EMERGENCY_CONTACT); setEmergencyContacts(updatedContacts); } } @VisibleForTesting public List getEmergencyContacts() { return mEmergencyContacts; } public void setEmergencyContacts(List emergencyContacts) { final boolean changed = !mEmergencyContacts.equals(emergencyContacts); if (changed || !mEmergencyContactsSet) { mEmergencyContacts = emergencyContacts; mEmergencyContactsSet = true; persistEmergencyContacts(emergencyContacts); if (changed) { notifyChanged(); } } while (getPreferenceCount() - emergencyContacts.size() > 0) { removePreference(getPreference(0)); } // Reload the preferences or add new ones if necessary Iterator it = emergencyContacts.iterator(); int i = 0; Uri phoneUri = null; List updatedEmergencyContacts = null; while (it.hasNext()) { ContactPreference contactPreference = null; phoneUri = it.next(); // setPhoneUri may throw an IllegalArgumentException (also called in the constructor // of ContactPreference) try { if (i < getPreferenceCount()) { contactPreference = (ContactPreference) getPreference(i); contactPreference.setPhoneUri(phoneUri); } else { contactPreference = new ContactPreference(getContext(), phoneUri, mContactFactory); onBindContactView(contactPreference); addPreference(contactPreference); } i++; MetricsLogger.action(getContext(), MetricsEvent.ACTION_GET_CONTACT, 0); } catch (IllegalArgumentException e) { Log.w(TAG, "Caught IllegalArgumentException for phoneUri:" + phoneUri == null ? "" : phoneUri.toString(), e); MetricsLogger.action(getContext(), MetricsEvent.ACTION_GET_CONTACT, 1); if (updatedEmergencyContacts == null) { updatedEmergencyContacts = new ArrayList<>(emergencyContacts); } updatedEmergencyContacts.remove(phoneUri); } } if (updatedEmergencyContacts != null) { // Set the contacts again: something went wrong when retrieving information about the // stored phone Uris. setEmergencyContacts(updatedEmergencyContacts); } // Enable or disable the settings suggestion, as appropriate. PreferenceUtils.updateSettingsSuggestionState(getContext()); MetricsLogger.histogram(getContext(), "num_emergency_contacts", Math.min(3, emergencyContacts.size())); } /** * Called when {@code contactPreference} has been added to this category. You may now set * listeners. */ protected void onBindContactView(final ContactPreference contactPreference) { contactPreference.setRemoveContactPreferenceListener(this); contactPreference .setOnPreferenceClickListener( new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { contactPreference.displayContact(); return true; } } ); } private List getPersistedEmergencyContacts() { return deserializeAndFilter(getKey(), getContext(), getPersistedString(""), mContactValidator); } @Override protected String getPersistedString(String defaultReturnValue) { try { return super.getPersistedString(defaultReturnValue); } catch (ClassCastException e) { // Protect against b/28194605: We used to store the contacts using a string set. // If it was a string set, a ClassCastException would have been thrown, and we can // ignore its value. If it is stored as a value of another type, we are potentially // squelching an exception here, but returning the default return value seems reasonable // in either case. return defaultReturnValue; } } /** * Converts the string representing the emergency contacts to a list of Uris and only keeps * those corresponding to still existing contacts. It persists the contacts if at least one * contact was does not exist anymore. */ public static List deserializeAndFilter(String key, Context context, String emergencyContactString) { return deserializeAndFilter(key, context, emergencyContactString, DEFAULT_CONTACT_VALIDATOR); } /** Converts the Uris to a string representation. */ public static String serialize(List emergencyContacts) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < emergencyContacts.size(); i++) { sb.append(emergencyContacts.get(i).toString()); sb.append(CONTACT_SEPARATOR); } if (sb.length() > 0) { sb.setLength(sb.length() - 1); } return sb.toString(); } @VisibleForTesting void persistEmergencyContacts(List emergencyContacts) { persistString(serialize(emergencyContacts)); } private static List deserializeAndFilter(String key, Context context, String emergencyContactString, ContactValidator contactValidator) { String[] emergencyContactsArray = emergencyContactString.split(QUOTE_CONTACT_SEPARATOR); List filteredEmergencyContacts = new ArrayList(emergencyContactsArray.length); for (String emergencyContact : emergencyContactsArray) { Uri phoneUri = Uri.parse(emergencyContact); if (contactValidator.isValidEmergencyContact(context, phoneUri)) { filteredEmergencyContacts.add(phoneUri); } } // If not all contacts were added, then we need to overwrite the emergency contacts stored // in shared preferences. This deals with emergency contacts being deleted from contacts: // currently we have no way to being notified when this happens. if (filteredEmergencyContacts.size() != emergencyContactsArray.length) { String emergencyContactStrings = serialize(filteredEmergencyContacts); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); sharedPreferences.edit().putString(key, emergencyContactStrings).commit(); } return filteredEmergencyContacts; } }