| /* |
| * 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 android.os.UserManager; |
| 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. |
| * |
| * <p>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<Uri> mEmergencyContacts = new ArrayList<Uri>(); |
| 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<Uri> updatedContacts = new ArrayList<Uri>(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<Uri> updatedContacts = new ArrayList<Uri>(mEmergencyContacts); |
| if (updatedContacts.add(phoneUri) && callChangeListener(updatedContacts)) { |
| MetricsLogger.action(getContext(), MetricsEvent.ACTION_ADD_EMERGENCY_CONTACT); |
| setEmergencyContacts(updatedContacts); |
| } |
| } |
| |
| @VisibleForTesting |
| public List<Uri> getEmergencyContacts() { |
| return mEmergencyContacts; |
| } |
| |
| public void setEmergencyContacts(List<Uri> 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<Uri> it = emergencyContacts.iterator(); |
| int i = 0; |
| Uri phoneUri = null; |
| List<Uri> 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<Uri> 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<Uri> 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<Uri> 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<Uri> emergencyContacts) { |
| // Avoid persisting emergency contacts in direct boot mode. |
| if (isUserUnlocked(getContext())) { |
| persistString(serialize(emergencyContacts)); |
| } |
| } |
| |
| private static List<Uri> deserializeAndFilter(String key, Context context, |
| String emergencyContactString, |
| ContactValidator contactValidator) { |
| String[] emergencyContactsArray = |
| emergencyContactString.split(QUOTE_CONTACT_SEPARATOR); |
| List<Uri> filteredEmergencyContacts = new ArrayList<Uri>(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) { |
| // Avoid updating emergency contacts in direct boot mode. |
| if (isUserUnlocked(context)) { |
| String emergencyContactStrings = serialize(filteredEmergencyContacts); |
| SharedPreferences sharedPreferences = |
| PreferenceManager.getDefaultSharedPreferences(context); |
| sharedPreferences.edit().putString(key, emergencyContactStrings).commit(); |
| } |
| } |
| return filteredEmergencyContacts; |
| } |
| |
| private static boolean isUserUnlocked(Context context) { |
| UserManager userManager = context.getSystemService(UserManager.class); |
| return userManager != null && userManager.isUserUnlocked(); |
| } |
| |
| } |