1 /*
2  * Copyright (C) 2010 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 
17 package com.android.contacts.list;
18 
19 import android.accounts.Account;
20 import android.content.SharedPreferences;
21 import android.graphics.drawable.Drawable;
22 import android.net.Uri;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.provider.ContactsContract.RawContacts;
26 import android.text.TextUtils;
27 
28 import com.android.contacts.logging.ListEvent;
29 import com.android.contacts.model.account.AccountWithDataSet;
30 import com.android.contacts.model.account.GoogleAccountType;
31 
32 import java.util.ArrayList;
33 import java.util.List;
34 
35 /**
36  * Contact list filter parameters.
37  */
38 public final class ContactListFilter implements Comparable<ContactListFilter>, Parcelable {
39 
40     public static final int FILTER_TYPE_DEFAULT = -1;
41     public static final int FILTER_TYPE_ALL_ACCOUNTS = -2;
42     public static final int FILTER_TYPE_CUSTOM = -3;
43     public static final int FILTER_TYPE_STARRED = -4;
44     public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5;
45     public static final int FILTER_TYPE_SINGLE_CONTACT = -6;
46     public static final int FILTER_TYPE_GROUP_MEMBERS = -7;
47     public static final int FILTER_TYPE_DEVICE_CONTACTS = -8;
48 
49     public static final int FILTER_TYPE_ACCOUNT = 0;
50 
51     /**
52      * Obsolete filter which had been used in Honeycomb. This may be stored in
53      * {@link SharedPreferences}, but should be replaced with ALL filter when it is found.
54      *
55      * TODO: "group" filter and relevant variables are all obsolete. Remove them.
56      */
57     private static final int FILTER_TYPE_GROUP = 1;
58 
59     private static final String KEY_FILTER_TYPE = "filter.type";
60     private static final String KEY_ACCOUNT_NAME = "filter.accountName";
61     private static final String KEY_ACCOUNT_TYPE = "filter.accountType";
62     private static final String KEY_DATA_SET = "filter.dataSet";
63 
64     public final int filterType;
65     public final String accountType;
66     public final String accountName;
67     public final String dataSet;
68     public final Drawable icon;
69     private String mId;
70 
ContactListFilter(int filterType, String accountType, String accountName, String dataSet, Drawable icon)71     public ContactListFilter(int filterType, String accountType, String accountName, String dataSet,
72             Drawable icon) {
73         this.filterType = filterType;
74         this.accountType = accountType;
75         this.accountName = accountName;
76         this.dataSet = dataSet;
77         this.icon = icon;
78     }
79 
createFilterWithType(int filterType)80     public static ContactListFilter createFilterWithType(int filterType) {
81         return new ContactListFilter(filterType, null, null, null, null);
82     }
83 
createAccountFilter(String accountType, String accountName, String dataSet, Drawable icon)84     public static ContactListFilter createAccountFilter(String accountType, String accountName,
85             String dataSet, Drawable icon) {
86         return new ContactListFilter(ContactListFilter.FILTER_TYPE_ACCOUNT, accountType,
87                 accountName, dataSet, icon);
88     }
89 
createGroupMembersFilter(String accountType, String accountName, String dataSet)90     public static ContactListFilter createGroupMembersFilter(String accountType, String accountName,
91             String dataSet) {
92         return new ContactListFilter(ContactListFilter.FILTER_TYPE_GROUP_MEMBERS, accountType,
93                 accountName, dataSet, /* icon */ null);
94     }
95 
createDeviceContactsFilter(Drawable icon)96     public static ContactListFilter createDeviceContactsFilter(Drawable icon) {
97         return new ContactListFilter(ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS,
98                 /* accountType= */ null, /* accountName= */ null, /* dataSet= */ null, icon);
99     }
100 
createDeviceContactsFilter(Drawable icon, AccountWithDataSet account)101     public static ContactListFilter createDeviceContactsFilter(Drawable icon,
102             AccountWithDataSet account) {
103         return new ContactListFilter(ContactListFilter.FILTER_TYPE_DEVICE_CONTACTS,
104                 account.type, account.name, account.dataSet, icon);
105     }
106 
107     /**
108      * Whether the given {@link ContactListFilter} has a filter type that should be displayed as
109      * the default contacts list view.
110      */
isContactsFilterType()111     public boolean isContactsFilterType() {
112         return filterType == ContactListFilter.FILTER_TYPE_DEFAULT
113                 || filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
114                 || filterType == ContactListFilter.FILTER_TYPE_CUSTOM;
115     }
116 
117     /** Returns the {@link ListEvent.ListType} for the type of this filter. */
toListType()118     public int toListType() {
119         switch (filterType) {
120             case FILTER_TYPE_DEFAULT:
121                 // Fall through
122             case FILTER_TYPE_ALL_ACCOUNTS:
123                 return ListEvent.ListType.ALL_CONTACTS;
124             case FILTER_TYPE_CUSTOM:
125                 return ListEvent.ListType.CUSTOM;
126             case FILTER_TYPE_STARRED:
127                 return ListEvent.ListType.STARRED;
128             case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
129                 return ListEvent.ListType.PHONE_NUMBERS;
130             case FILTER_TYPE_SINGLE_CONTACT:
131                 return ListEvent.ListType.SINGLE_CONTACT;
132             case FILTER_TYPE_ACCOUNT:
133                 return ListEvent.ListType.ACCOUNT;
134             case FILTER_TYPE_GROUP_MEMBERS:
135                 return ListEvent.ListType.GROUP;
136             case FILTER_TYPE_DEVICE_CONTACTS:
137                 return ListEvent.ListType.DEVICE;
138         }
139         return ListEvent.ListType.UNKNOWN_LIST;
140     }
141 
142 
143     /**
144      * Returns true if this filter is based on data and may become invalid over time.
145      */
isValidationRequired()146     public boolean isValidationRequired() {
147         return filterType == FILTER_TYPE_ACCOUNT;
148     }
149 
150     @Override
toString()151     public String toString() {
152         switch (filterType) {
153             case FILTER_TYPE_DEFAULT:
154                 return "default";
155             case FILTER_TYPE_ALL_ACCOUNTS:
156                 return "all_accounts";
157             case FILTER_TYPE_CUSTOM:
158                 return "custom";
159             case FILTER_TYPE_STARRED:
160                 return "starred";
161             case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
162                 return "with_phones";
163             case FILTER_TYPE_SINGLE_CONTACT:
164                 return "single";
165             case FILTER_TYPE_ACCOUNT:
166                 return "account: " + accountType + (dataSet != null ? "/" + dataSet : "")
167                         + " " + accountName;
168             case FILTER_TYPE_GROUP_MEMBERS:
169                 return "group_members";
170             case FILTER_TYPE_DEVICE_CONTACTS:
171                 return "device_contacts";
172         }
173         return super.toString();
174     }
175 
176     @Override
compareTo(ContactListFilter another)177     public int compareTo(ContactListFilter another) {
178         int res = accountName.compareTo(another.accountName);
179         if (res != 0) {
180             return res;
181         }
182 
183         res = accountType.compareTo(another.accountType);
184         if (res != 0) {
185             return res;
186         }
187 
188         return filterType - another.filterType;
189     }
190 
191     @Override
hashCode()192     public int hashCode() {
193         int code = filterType;
194         if (accountType != null) {
195             code = code * 31 + accountType.hashCode();
196         }
197         if (accountName != null) {
198             code = code * 31 + accountName.hashCode();
199         }
200         if (dataSet != null) {
201             code = code * 31 + dataSet.hashCode();
202         }
203         return code;
204     }
205 
206     @Override
equals(Object other)207     public boolean equals(Object other) {
208         if (this == other) {
209             return true;
210         }
211 
212         if (!(other instanceof ContactListFilter)) {
213             return false;
214         }
215 
216         ContactListFilter otherFilter = (ContactListFilter) other;
217         if (filterType != otherFilter.filterType
218                 || !TextUtils.equals(accountName, otherFilter.accountName)
219                 || !TextUtils.equals(accountType, otherFilter.accountType)
220                 || !TextUtils.equals(dataSet, otherFilter.dataSet)) {
221             return false;
222         }
223 
224         return true;
225     }
226 
227     /**
228      * Store the given {@link ContactListFilter} to preferences. If the requested filter is
229      * of type {@link #FILTER_TYPE_SINGLE_CONTACT} then do not save it to preferences because
230      * it is a temporary state.
231      */
storeToPreferences(SharedPreferences prefs, ContactListFilter filter)232     public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) {
233         if (filter != null && filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
234             return;
235         }
236         prefs.edit()
237             .putInt(KEY_FILTER_TYPE, filter == null ? FILTER_TYPE_DEFAULT : filter.filterType)
238             .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName)
239             .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType)
240             .putString(KEY_DATA_SET, filter == null ? null : filter.dataSet)
241             .apply();
242     }
243 
244     /**
245      * Try to obtain ContactListFilter object saved in SharedPreference.
246      * If there's no info there, return ALL filter instead.
247      */
restoreDefaultPreferences(SharedPreferences prefs)248     public static ContactListFilter restoreDefaultPreferences(SharedPreferences prefs) {
249         ContactListFilter filter = restoreFromPreferences(prefs);
250         if (filter == null) {
251             filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
252         }
253         // "Group" filter is obsolete and thus is not exposed anymore. The "single contact mode"
254         // should also not be stored in preferences anymore since it is a temporary state.
255         if (filter.filterType == FILTER_TYPE_GROUP ||
256                 filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
257             filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
258         }
259         return filter;
260     }
261 
restoreFromPreferences(SharedPreferences prefs)262     private static ContactListFilter restoreFromPreferences(SharedPreferences prefs) {
263         int filterType = prefs.getInt(KEY_FILTER_TYPE, FILTER_TYPE_DEFAULT);
264         if (filterType == FILTER_TYPE_DEFAULT) {
265             return null;
266         }
267 
268         String accountName = prefs.getString(KEY_ACCOUNT_NAME, null);
269         String accountType = prefs.getString(KEY_ACCOUNT_TYPE, null);
270         String dataSet = prefs.getString(KEY_DATA_SET, null);
271         return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
272     }
273 
274 
275     @Override
writeToParcel(Parcel dest, int flags)276     public void writeToParcel(Parcel dest, int flags) {
277         dest.writeInt(filterType);
278         dest.writeString(accountName);
279         dest.writeString(accountType);
280         dest.writeString(dataSet);
281     }
282 
283     public static final Parcelable.Creator<ContactListFilter> CREATOR =
284             new Parcelable.Creator<ContactListFilter>() {
285         @Override
286         public ContactListFilter createFromParcel(Parcel source) {
287             int filterType = source.readInt();
288             String accountName = source.readString();
289             String accountType = source.readString();
290             String dataSet = source.readString();
291             return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
292         }
293 
294         @Override
295         public ContactListFilter[] newArray(int size) {
296             return new ContactListFilter[size];
297         }
298     };
299 
300     @Override
describeContents()301     public int describeContents() {
302         return 0;
303     }
304 
305     /**
306      * Returns a string that can be used as a stable persistent identifier for this filter.
307      */
getId()308     public String getId() {
309         if (mId == null) {
310             StringBuilder sb = new StringBuilder();
311             sb.append(filterType);
312             if (accountType != null) {
313                 sb.append('-').append(accountType);
314             }
315             if (dataSet != null) {
316                 sb.append('/').append(dataSet);
317             }
318             if (accountName != null) {
319                 sb.append('-').append(accountName.replace('-', '_'));
320             }
321             mId = sb.toString();
322         }
323         return mId;
324     }
325 
326     /**
327      * Adds the account query parameters to the given {@code uriBuilder}.
328      *
329      * @throws IllegalStateException if the filter type is not {@link #FILTER_TYPE_ACCOUNT} or
330      * {@link #FILTER_TYPE_GROUP_MEMBERS}.
331      */
addAccountQueryParameterToUrl(Uri.Builder uriBuilder)332     public Uri.Builder addAccountQueryParameterToUrl(Uri.Builder uriBuilder) {
333         if (filterType != FILTER_TYPE_ACCOUNT
334                 && filterType != FILTER_TYPE_GROUP_MEMBERS) {
335             throw new IllegalStateException(
336                     "filterType must be FILTER_TYPE_ACCOUNT or FILER_TYPE_GROUP_MEMBERS");
337         }
338         // null account names are not valid, see ContactsProvider2#appendAccountFromParameter
339         if (accountName != null) {
340             uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
341             uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
342         }
343         if (dataSet != null) {
344             uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet);
345         }
346         return uriBuilder;
347     }
348 
toAccountWithDataSet()349     public AccountWithDataSet toAccountWithDataSet() {
350         if (filterType == FILTER_TYPE_ACCOUNT || filterType == FILTER_TYPE_DEVICE_CONTACTS) {
351             return new AccountWithDataSet(accountName, accountType, dataSet);
352         } else {
353             throw new IllegalStateException("Cannot create Account from filter type " +
354                     filterTypeToString(filterType));
355         }
356     }
357 
toDebugString()358     public String toDebugString() {
359         final StringBuilder builder = new StringBuilder();
360         builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")");
361         if (filterType == FILTER_TYPE_ACCOUNT) {
362             builder.append(", accountType: " + accountType)
363                     .append(", accountName: " + accountName)
364                     .append(", dataSet: " + dataSet);
365         }
366         builder.append(", icon: " + icon + "]");
367         return builder.toString();
368     }
369 
filterTypeToString(int filterType)370     public static final String filterTypeToString(int filterType) {
371         switch (filterType) {
372             case FILTER_TYPE_DEFAULT:
373                 return "FILTER_TYPE_DEFAULT";
374             case FILTER_TYPE_ALL_ACCOUNTS:
375                 return "FILTER_TYPE_ALL_ACCOUNTS";
376             case FILTER_TYPE_CUSTOM:
377                 return "FILTER_TYPE_CUSTOM";
378             case FILTER_TYPE_STARRED:
379                 return "FILTER_TYPE_STARRED";
380             case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
381                 return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY";
382             case FILTER_TYPE_SINGLE_CONTACT:
383                 return "FILTER_TYPE_SINGLE_CONTACT";
384             case FILTER_TYPE_ACCOUNT:
385                 return "FILTER_TYPE_ACCOUNT";
386             case FILTER_TYPE_GROUP_MEMBERS:
387                 return "FILTER_TYPE_GROUP_MEMBERS";
388             case FILTER_TYPE_DEVICE_CONTACTS:
389                 return "FILTER_TYPE_DEVICE_CONTACTS";
390             default:
391                 return "(unknown)";
392         }
393     }
394 
isSyncable()395     public boolean isSyncable() {
396         return isGoogleAccountType() && filterType == FILTER_TYPE_ACCOUNT;
397     }
398 
399     /**
400      * Returns true if this ContactListFilter contains at least one Google account.
401      * (see {@link #isGoogleAccountType)
402      */
isSyncable(List<AccountWithDataSet> accounts)403     public boolean isSyncable(List<AccountWithDataSet> accounts) {
404         if (isSyncable()) {
405             return true;
406         }
407         // Since we don't know which group is selected until the actual contacts loading, we
408         // consider a custom filter syncable as long as there is a Google account on the device,
409         // and don't check if there is any group that belongs to a Google account is selected.
410         if (filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
411                 || filterType == ContactListFilter.FILTER_TYPE_CUSTOM
412                 || filterType == ContactListFilter.FILTER_TYPE_DEFAULT) {
413             if (accounts != null && accounts.size() > 0) {
414                 // If we're showing all contacts and there is any Google account on the device then
415                 // we're syncable.
416                 for (AccountWithDataSet account : accounts) {
417                     if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type)
418                             && account.dataSet == null) {
419                         return true;
420                     }
421                 }
422             }
423         }
424         return false;
425     }
426 
shouldShowSyncState()427     public boolean shouldShowSyncState() {
428         return (isGoogleAccountType() && filterType == ContactListFilter.FILTER_TYPE_ACCOUNT)
429                 || filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
430                 || filterType == ContactListFilter.FILTER_TYPE_CUSTOM
431                 || filterType == ContactListFilter.FILTER_TYPE_DEFAULT;
432     }
433 
434     /**
435      * Returns the Google accounts (see {@link #isGoogleAccountType) for this ContactListFilter.
436      */
getSyncableAccounts(List<AccountWithDataSet> accounts)437     public List<Account> getSyncableAccounts(List<AccountWithDataSet> accounts) {
438         final List<Account> syncableAccounts = new ArrayList<>();
439 
440         if (isGoogleAccountType() && filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
441             syncableAccounts.add(new Account(accountName, accountType));
442         } else if (filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS
443                 || filterType == ContactListFilter.FILTER_TYPE_CUSTOM
444                 || filterType == ContactListFilter.FILTER_TYPE_DEFAULT) {
445             if (accounts != null && accounts.size() > 0) {
446                 for (AccountWithDataSet account : accounts) {
447                     if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type)
448                             && account.dataSet == null) {
449                         syncableAccounts.add(new Account(account.name, account.type));
450                     }
451                 }
452             }
453         }
454         return syncableAccounts;
455     }
456 
457     /**
458      * Returns true if this ContactListFilter is Google account type. (i.e. where
459      * accountType = "com.google" and dataSet = null)
460      */
isGoogleAccountType()461     public boolean isGoogleAccountType() {
462         return GoogleAccountType.ACCOUNT_TYPE.equals(accountType) && dataSet == null;
463     }
464 }
465