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 */
16package com.android.emergency.preferences;
17
18import android.content.Context;
19import android.content.SharedPreferences;
20import android.content.res.TypedArray;
21import android.net.Uri;
22import android.preference.Preference;
23import android.preference.PreferenceCategory;
24import android.preference.PreferenceManager;
25import android.util.AttributeSet;
26
27import com.android.emergency.EmergencyContactManager;
28import com.android.emergency.ReloadablePreferenceInterface;
29import com.android.internal.annotations.VisibleForTesting;
30import com.android.internal.logging.MetricsLogger;
31import com.android.internal.logging.MetricsProto.MetricsEvent;
32
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.Iterator;
36import java.util.List;
37import java.util.regex.Pattern;
38
39/**
40 * Custom {@link PreferenceCategory} that deals with contacts being deleted from the contacts app.
41 *
42 * <p>Contacts are stored internally using their ContactsContract.CommonDataKinds.Phone.CONTENT_URI.
43 */
44public class EmergencyContactsPreference extends PreferenceCategory
45        implements ReloadablePreferenceInterface,
46        ContactPreference.RemoveContactPreferenceListener {
47
48    private static final String CONTACT_SEPARATOR = "|";
49    private static final String QUOTE_CONTACT_SEPARATOR = Pattern.quote(CONTACT_SEPARATOR);
50
51    /** Stores the emergency contact's ContactsContract.CommonDataKinds.Phone.CONTENT_URI */
52    private List<Uri> mEmergencyContacts = new ArrayList<Uri>();
53    private boolean mEmergencyContactsSet = false;
54
55    public EmergencyContactsPreference(Context context, AttributeSet attrs) {
56        super(context, attrs);
57    }
58
59    @Override
60    protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
61        setEmergencyContacts(restorePersistedValue ?
62                getPersistedEmergencyContacts() :
63                deserializeAndFilter(getKey(),
64                        getContext(),
65                        (String) defaultValue));
66    }
67
68    @Override
69    protected Object onGetDefaultValue(TypedArray a, int index) {
70        return a.getString(index);
71    }
72
73    @Override
74    public void reloadFromPreference() {
75        setEmergencyContacts(getPersistedEmergencyContacts());
76    }
77
78    @Override
79    public boolean isNotSet() {
80        return mEmergencyContacts.isEmpty();
81    }
82
83    @Override
84    public void onRemoveContactPreference(ContactPreference contactPreference) {
85        Uri newContact = contactPreference.getContactUri();
86        if (mEmergencyContacts.contains(newContact)) {
87            List<Uri> updatedContacts = new ArrayList<Uri>(mEmergencyContacts);
88            if (updatedContacts.remove(newContact) && callChangeListener(updatedContacts)) {
89                MetricsLogger.action(getContext(), MetricsEvent.ACTION_DELETE_EMERGENCY_CONTACT);
90                setEmergencyContacts(updatedContacts);
91            }
92        }
93    }
94
95    /**
96     * Adds a new emergency contact. The {@code contactUri} is the
97     * ContactsContract.CommonDataKinds.Phone.CONTENT_URI corresponding to the
98     * contact's selected phone number.
99     */
100    public void addNewEmergencyContact(Uri contactUri) {
101        if (!mEmergencyContacts.contains(contactUri)) {
102            List<Uri> updatedContacts = new ArrayList<Uri>(mEmergencyContacts);
103            if (updatedContacts.add(contactUri) && callChangeListener(updatedContacts)) {
104                MetricsLogger.action(getContext(), MetricsEvent.ACTION_ADD_EMERGENCY_CONTACT);
105                setEmergencyContacts(updatedContacts);
106            }
107        }
108    }
109
110    @VisibleForTesting
111    public List<Uri> getEmergencyContacts() {
112        return mEmergencyContacts;
113    }
114
115    public void setEmergencyContacts(List<Uri> emergencyContacts) {
116        final boolean changed = !mEmergencyContacts.equals(emergencyContacts);
117        if (changed || !mEmergencyContactsSet) {
118            mEmergencyContacts = emergencyContacts;
119            mEmergencyContactsSet = true;
120            persistString(serialize(emergencyContacts));
121            if (changed) {
122                notifyChanged();
123            }
124        }
125
126        while (getPreferenceCount() - emergencyContacts.size() > 0) {
127            removePreference(getPreference(0));
128        }
129
130        // Reload the preferences or add new ones if necessary
131        Iterator<Uri> it = emergencyContacts.iterator();
132        int i = 0;
133        while (it.hasNext()) {
134            if (i < getPreferenceCount()) {
135                ContactPreference contactPreference = (ContactPreference) getPreference(i);
136                contactPreference.setUri(it.next());
137            } else {
138                addContactPreference(it.next());
139            }
140            i++;
141        }
142    }
143
144    private void addContactPreference(Uri contactUri) {
145        final ContactPreference contactPreference = new ContactPreference(getContext(), contactUri);
146        onBindContactView(contactPreference);
147        addPreference(contactPreference);
148    }
149
150    /**
151     * Called when {@code contactPreference} has been added to this category. You may now set
152     * listeners.
153     */
154    protected void onBindContactView(final ContactPreference contactPreference) {
155        contactPreference.setRemoveContactPreferenceListener(this);
156        contactPreference
157                .setOnPreferenceClickListener(
158                        new Preference.OnPreferenceClickListener() {
159                            @Override
160                            public boolean onPreferenceClick(Preference preference) {
161                                contactPreference.displayContact();
162                                return true;
163                            }
164                        }
165                );
166    }
167
168    private List<Uri> getPersistedEmergencyContacts() {
169        return deserializeAndFilter(getKey(), getContext(), getPersistedString(""));
170    }
171
172    @Override
173    protected String getPersistedString(String defaultReturnValue) {
174        try {
175            return super.getPersistedString(defaultReturnValue);
176        } catch (ClassCastException e) {
177            // Protect against b/28194605: We used to store the contacts using a string set.
178            // If it is a string set, ignore its value. If it is not a string set it will throw
179            // a ClassCastException
180            getPersistedStringSet(Collections.<String>emptySet());
181            return defaultReturnValue;
182        }
183    }
184
185    /**
186     * Converts the string representing the emergency contacts to a list of Uris and only keeps
187     * those corresponding to still existing contacts. It persists the contacts if at least one
188     * contact was does not exist anymore.
189     */
190    public static List<Uri> deserializeAndFilter(String key, Context context,
191                                                 String emergencyContactString) {
192        String[] emergencyContactsArray =
193                emergencyContactString.split(QUOTE_CONTACT_SEPARATOR);
194        List<Uri> filteredEmergencyContacts = new ArrayList<Uri>(emergencyContactsArray.length);
195        for (String emergencyContact : emergencyContactsArray) {
196            Uri contactUri = Uri.parse(emergencyContact);
197            if (EmergencyContactManager.isValidEmergencyContact(context, contactUri)) {
198                filteredEmergencyContacts.add(contactUri);
199            }
200        }
201        // If not all contacts were added, then we need to overwrite the emergency contacts stored
202        // in shared preferences. This deals with emergency contacts being deleted from contacts:
203        // currently we have no way to being notified when this happens.
204        if (filteredEmergencyContacts.size() != emergencyContactsArray.length) {
205            String emergencyContactStrings = serialize(filteredEmergencyContacts);
206            SharedPreferences sharedPreferences =
207                    PreferenceManager.getDefaultSharedPreferences(context);
208            sharedPreferences.edit().putString(key, emergencyContactStrings).commit();
209        }
210        return filteredEmergencyContacts;
211    }
212
213    /** Converts the Uris to a string representation. */
214    public static String serialize(List<Uri> emergencyContacts) {
215        StringBuilder sb = new StringBuilder();
216        for (int i = 0; i < emergencyContacts.size(); i++) {
217            sb.append(emergencyContacts.get(i).toString());
218            sb.append(CONTACT_SEPARATOR);
219        }
220
221        if (sb.length() > 0) {
222            sb.setLength(sb.length() - 1);
223        }
224        return sb.toString();
225    }
226}
227