Home | History | Annotate | Download | only in preferences
      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.support.annotation.NonNull;
     23 import android.support.v7.preference.Preference;
     24 import android.support.v7.preference.PreferenceCategory;
     25 import android.support.v7.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          */
     81         boolean isValidEmergencyContact(Context context, Uri phoneUri);
     82     }
     83 
     84     public EmergencyContactsPreference(Context context, AttributeSet attrs) {
     85         this(context, attrs, DEFAULT_CONTACT_VALIDATOR, ContactPreference.DEFAULT_CONTACT_FACTORY);
     86     }
     87 
     88     @VisibleForTesting
     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
     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
    108     protected Object onGetDefaultValue(TypedArray a, int index) {
    109         return a.getString(index);
    110     }
    111 
    112     @Override
    113     public void reloadFromPreference() {
    114         setEmergencyContacts(getPersistedEmergencyContacts());
    115     }
    116 
    117     @Override
    118     public boolean isNotSet() {
    119         return mEmergencyContacts.isEmpty();
    120     }
    121 
    122     @Override
    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      */
    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
    156     public List<Uri> getEmergencyContacts() {
    157         return mEmergencyContacts;
    158     }
    159 
    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      */
    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 
    237     private List<Uri> getPersistedEmergencyContacts() {
    238         return deserializeAndFilter(getKey(), getContext(), getPersistedString(""),
    239                 mContactValidator);
    240     }
    241 
    242     @Override
    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      */
    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. */
    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
    282     void persistEmergencyContacts(List<Uri> emergencyContacts) {
    283         persistString(serialize(emergencyContacts));
    284     }
    285 
    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