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 
17 package com.android.contacts.group;
18 
19 import android.app.Fragment;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.Bundle;
26 import android.provider.ContactsContract;
27 import android.provider.ContactsContract.Contacts;
28 import android.provider.ContactsContract.Groups;
29 import android.text.TextUtils;
30 
31 import com.android.contacts.ContactsUtils;
32 import com.android.contacts.GroupListLoader;
33 import com.android.contacts.activities.ContactSelectionActivity;
34 import com.android.contacts.list.ContactsSectionIndexer;
35 import com.android.contacts.list.UiIntentActions;
36 import com.android.contacts.model.account.GoogleAccountType;
37 
38 import java.util.ArrayList;
39 import java.util.Arrays;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Set;
43 
44 /**
45  * Group utility methods.
46  */
47 public final class GroupUtil {
48 
49     public final static String ALL_GROUPS_SELECTION =
50             Groups.ACCOUNT_TYPE + " NOT NULL AND " + Groups.ACCOUNT_NAME + " NOT NULL AND "
51                     + Groups.DELETED + "=0";
52 
53     public final static String DEFAULT_SELECTION = ALL_GROUPS_SELECTION + " AND "
54             + Groups.AUTO_ADD + "=0 AND " + Groups.FAVORITES + "=0";
55 
56     public static final String ACTION_ADD_TO_GROUP = "addToGroup";
57     public static final String ACTION_CREATE_GROUP = "createGroup";
58     public static final String ACTION_DELETE_GROUP = "deleteGroup";
59     public static final String ACTION_REMOVE_FROM_GROUP = "removeFromGroup";
60     public static final String ACTION_SWITCH_GROUP = "switchGroup";
61     public static final String ACTION_UPDATE_GROUP = "updateGroup";
62 
63     public static final int RESULT_SEND_TO_SELECTION = 100;
64 
65     // System IDs of FFC groups in Google accounts
66     private static final Set<String> FFC_GROUPS =
67             new HashSet(Arrays.asList("Friends", "Family", "Coworkers"));
68 
GroupUtil()69     private GroupUtil() {
70     }
71 
72     /** Returns a {@link GroupListItem} read from the given cursor and position. */
getGroupListItem(Cursor cursor, int position)73     public static GroupListItem getGroupListItem(Cursor cursor, int position) {
74         if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(position)) {
75             return null;
76         }
77         String accountName = cursor.getString(GroupListLoader.ACCOUNT_NAME);
78         String accountType = cursor.getString(GroupListLoader.ACCOUNT_TYPE);
79         String dataSet = cursor.getString(GroupListLoader.DATA_SET);
80         long groupId = cursor.getLong(GroupListLoader.GROUP_ID);
81         String title = cursor.getString(GroupListLoader.TITLE);
82         int memberCount = cursor.getInt(GroupListLoader.MEMBER_COUNT);
83         boolean isReadOnly = cursor.getInt(GroupListLoader.IS_READ_ONLY) == 1;
84         String systemId = cursor.getString(GroupListLoader.SYSTEM_ID);
85 
86         // Figure out if this is the first group for this account name / account type pair by
87         // checking the previous entry. This is to determine whether or not we need to display an
88         // account header in this item.
89         int previousIndex = position - 1;
90         boolean isFirstGroupInAccount = true;
91         if (previousIndex >= 0 && cursor.moveToPosition(previousIndex)) {
92             String previousGroupAccountName = cursor.getString(GroupListLoader.ACCOUNT_NAME);
93             String previousGroupAccountType = cursor.getString(GroupListLoader.ACCOUNT_TYPE);
94             String previousGroupDataSet = cursor.getString(GroupListLoader.DATA_SET);
95 
96             if (TextUtils.equals(accountName, previousGroupAccountName)
97                     && TextUtils.equals(accountType, previousGroupAccountType)
98                     && TextUtils.equals(dataSet, previousGroupDataSet)) {
99                 isFirstGroupInAccount = false;
100             }
101         }
102 
103         return new GroupListItem(accountName, accountType, dataSet, groupId, title,
104                 isFirstGroupInAccount, memberCount, isReadOnly, systemId);
105     }
106 
getSendToDataForIds(Context context, long[] ids, String scheme)107     public static List<String> getSendToDataForIds(Context context, long[] ids, String scheme) {
108         final List<String> items = new ArrayList<>();
109         final String sIds = GroupUtil.convertArrayToString(ids);
110         final String select = (ContactsUtils.SCHEME_MAILTO.equals(scheme)
111                 ? GroupMembersFragment.Query.EMAIL_SELECTION
112                 + " AND " + ContactsContract.CommonDataKinds.Email._ID + " IN (" + sIds + ")"
113                 : GroupMembersFragment.Query.PHONE_SELECTION
114                 + " AND " + ContactsContract.CommonDataKinds.Phone._ID + " IN (" + sIds + ")");
115         final ContentResolver contentResolver = context.getContentResolver();
116         final Cursor cursor = contentResolver.query(ContactsContract.Data.CONTENT_URI,
117                 ContactsUtils.SCHEME_MAILTO.equals(scheme)
118                         ? GroupMembersFragment.Query.EMAIL_PROJECTION
119                         : GroupMembersFragment.Query.PHONE_PROJECTION,
120                 select, null, null);
121 
122         if (cursor == null) {
123             return items;
124         }
125 
126         try {
127             cursor.moveToPosition(-1);
128             while (cursor.moveToNext()) {
129                 final String data = cursor.getString(GroupMembersFragment.Query.DATA1);
130 
131                 if (!TextUtils.isEmpty(data)) {
132                     items.add(data);
133                 }
134             }
135         } finally {
136             cursor.close();
137         }
138 
139         return items;
140     }
141 
142     /** Returns an Intent to send emails/phones to some activity/app */
startSendToSelectionActivity( Fragment fragment, String itemsList, String sendScheme, String title)143     public static void startSendToSelectionActivity(
144             Fragment fragment, String itemsList, String sendScheme, String title) {
145         final Intent intent = new Intent(Intent.ACTION_SENDTO,
146                 Uri.fromParts(sendScheme, itemsList, null));
147         fragment.startActivityForResult(
148                 Intent.createChooser(intent, title), RESULT_SEND_TO_SELECTION);
149     }
150 
151     /** Returns an Intent to pick emails/phones to send to selection (or group) */
createSendToSelectionPickerIntent(Context context, long[] ids, long[] defaultSelection, String sendScheme, String title)152     public static Intent createSendToSelectionPickerIntent(Context context, long[] ids,
153             long[] defaultSelection, String sendScheme, String title) {
154         final Intent intent = new Intent(context, ContactSelectionActivity.class);
155         intent.setAction(UiIntentActions.ACTION_SELECT_ITEMS);
156         intent.setType(ContactsUtils.SCHEME_MAILTO.equals(sendScheme)
157                 ? ContactsContract.CommonDataKinds.Email.CONTENT_TYPE
158                 : ContactsContract.CommonDataKinds.Phone.CONTENT_TYPE);
159         intent.putExtra(UiIntentActions.SELECTION_ITEM_LIST, ids);
160         intent.putExtra(UiIntentActions.SELECTION_DEFAULT_SELECTION, defaultSelection);
161         intent.putExtra(UiIntentActions.SELECTION_SEND_SCHEME, sendScheme);
162         intent.putExtra(UiIntentActions.SELECTION_SEND_TITLE, title);
163 
164         return intent;
165     }
166 
167     /** Returns an Intent to pick contacts to add to a group. */
createPickMemberIntent(Context context, GroupMetaData groupMetaData, ArrayList<String> memberContactIds)168     public static Intent createPickMemberIntent(Context context,
169             GroupMetaData groupMetaData, ArrayList<String> memberContactIds) {
170         final Intent intent = new Intent(context, ContactSelectionActivity.class);
171         intent.setAction(Intent.ACTION_PICK);
172         intent.setType(Groups.CONTENT_TYPE);
173         intent.putExtra(UiIntentActions.GROUP_ACCOUNT_NAME, groupMetaData.accountName);
174         intent.putExtra(UiIntentActions.GROUP_ACCOUNT_TYPE, groupMetaData.accountType);
175         intent.putExtra(UiIntentActions.GROUP_ACCOUNT_DATA_SET, groupMetaData.dataSet);
176         intent.putExtra(UiIntentActions.GROUP_CONTACT_IDS, memberContactIds);
177         return intent;
178     }
179 
convertArrayToString(long[] list)180     public static String convertArrayToString(long[] list) {
181         if (list == null || list.length == 0) return "";
182         return Arrays.toString(list).replace("[", "").replace("]", "");
183     }
184 
convertLongSetToLongArray(Set<Long> set)185     public static long[] convertLongSetToLongArray(Set<Long> set) {
186         final Long[] contactIds = set.toArray(new Long[set.size()]);
187         final long[] result = new long[contactIds.length];
188         for (int i = 0; i < contactIds.length; i++) {
189             result[i] = contactIds[i];
190         }
191         return result;
192     }
193 
convertStringSetToLongArray(Set<String> set)194     public static long[] convertStringSetToLongArray(Set<String> set) {
195         final String[] contactIds = set.toArray(new String[set.size()]);
196         final long[] result = new long[contactIds.length];
197         for (int i = 0; i < contactIds.length; i++) {
198             try {
199                 result[i] = Long.parseLong(contactIds[i]);
200             } catch (NumberFormatException e) {
201                 result[i] = -1;
202             }
203         }
204         return result;
205     }
206 
207     /**
208      * Returns true if it's an empty and read-only group and the system ID of
209      * the group is one of "Friends", "Family" and "Coworkers".
210      */
isEmptyFFCGroup(GroupListItem groupListItem)211     public static boolean isEmptyFFCGroup(GroupListItem groupListItem) {
212         return groupListItem.isReadOnly()
213                 && isSystemIdFFC(groupListItem.getSystemId())
214                 && (groupListItem.getMemberCount() <= 0);
215     }
216 
isSystemIdFFC(String systemId)217     private static boolean isSystemIdFFC(String systemId) {
218         return !TextUtils.isEmpty(systemId) && FFC_GROUPS.contains(systemId);
219     }
220 
221     /**
222      * Returns true the URI is a group URI.
223      */
isGroupUri(Uri uri)224     public static boolean isGroupUri(Uri uri) {
225         return  uri != null && uri.toString().startsWith(Groups.CONTENT_URI.toString());
226     }
227 
228     /**
229      * Sort groups alphabetically and in a localized way.
230      */
getGroupsSortOrder()231     public static String getGroupsSortOrder() {
232         return Groups.TITLE + " COLLATE LOCALIZED ASC";
233     }
234 
235     /**
236      * The sum of the last element in counts[] and the last element in positions[] is the total
237      * number of remaining elements in cursor. If count is more than what's in the indexer now,
238      * then we don't need to trim.
239      */
needTrimming(int count, int[] counts, int[] positions)240     public static boolean needTrimming(int count, int[] counts, int[] positions) {
241         // The sum of the last element in counts[] and the last element in positions[] is
242         // the total number of remaining elements in cursor. If mCount is more than
243         // what's in the indexer now, then we don't need to trim.
244         return positions.length > 0 && counts.length > 0
245                 && count <= (counts[counts.length - 1] + positions[positions.length - 1]);
246     }
247 
248     /**
249      * Update Bundle extras so as to update indexer.
250      */
updateBundle(Bundle bundle, ContactsSectionIndexer indexer, List<Integer> subscripts, String[] sections, int[] counts)251     public static void updateBundle(Bundle bundle, ContactsSectionIndexer indexer,
252             List<Integer> subscripts, String[] sections, int[] counts) {
253         for (int i : subscripts) {
254             final int filteredContact = indexer.getSectionForPosition(i);
255             if (filteredContact < counts.length && filteredContact >= 0) {
256                 counts[filteredContact]--;
257                 if (counts[filteredContact] == 0) {
258                     sections[filteredContact] = "";
259                 }
260             }
261         }
262         final String[] newSections = clearEmptyString(sections);
263         bundle.putStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, newSections);
264         final int[] newCounts = clearZeros(counts);
265         bundle.putIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, newCounts);
266     }
267 
clearEmptyString(String[] strings)268     private static String[] clearEmptyString(String[] strings) {
269         final List<String> list = new ArrayList<>();
270         for (String s : strings) {
271             if (!TextUtils.isEmpty(s)) {
272                 list.add(s);
273             }
274         }
275         return list.toArray(new String[list.size()]);
276     }
277 
clearZeros(int[] numbers)278     private static int[] clearZeros(int[] numbers) {
279         final List<Integer> list = new ArrayList<>();
280         for (int n : numbers) {
281             if (n > 0) {
282                 list.add(n);
283             }
284         }
285         final int[] array = new int[list.size()];
286         for(int i = 0; i < list.size(); i++) {
287             array[i] = list.get(i);
288         }
289         return array;
290     }
291 
292     /**
293      * Stores column ordering for the projection of a query of ContactsContract.Groups
294      */
295     public static final class GroupsProjection {
296         public final int groupId;
297         public final int title;
298         public final int summaryCount;
299         public final int systemId;
300         public final int accountName;
301         public final int accountType;
302         public final int dataSet;
303         public final int autoAdd;
304         public final int favorites;
305         public final int isReadOnly;
306         public final int deleted;
307 
GroupsProjection(Cursor cursor)308         public GroupsProjection(Cursor cursor) {
309             groupId = cursor.getColumnIndex(Groups._ID);
310             title = cursor.getColumnIndex(Groups.TITLE);
311             summaryCount = cursor.getColumnIndex(Groups.SUMMARY_COUNT);
312             systemId = cursor.getColumnIndex(Groups.SYSTEM_ID);
313             accountName = cursor.getColumnIndex(Groups.ACCOUNT_NAME);
314             accountType = cursor.getColumnIndex(Groups.ACCOUNT_TYPE);
315             dataSet = cursor.getColumnIndex(Groups.DATA_SET);
316             autoAdd = cursor.getColumnIndex(Groups.AUTO_ADD);
317             favorites = cursor.getColumnIndex(Groups.FAVORITES);
318             isReadOnly = cursor.getColumnIndex(Groups.GROUP_IS_READ_ONLY);
319             deleted = cursor.getColumnIndex(Groups.DELETED);
320         }
321 
GroupsProjection(String[] projection)322         public GroupsProjection(String[] projection) {
323             List<String> list = Arrays.asList(projection);
324             groupId = list.indexOf(Groups._ID);
325             title = list.indexOf(Groups.TITLE);
326             summaryCount = list.indexOf(Groups.SUMMARY_COUNT);
327             systemId = list.indexOf(Groups.SYSTEM_ID);
328             accountName = list.indexOf(Groups.ACCOUNT_NAME);
329             accountType = list.indexOf(Groups.ACCOUNT_TYPE);
330             dataSet = list.indexOf(Groups.DATA_SET);
331             autoAdd = list.indexOf(Groups.AUTO_ADD);
332             favorites = list.indexOf(Groups.FAVORITES);
333             isReadOnly = list.indexOf(Groups.GROUP_IS_READ_ONLY);
334             deleted = list.indexOf(Groups.DELETED);
335         }
336 
getTitle(Cursor cursor)337         public String getTitle(Cursor cursor) {
338             return cursor.getString(title);
339         }
340 
getId(Cursor cursor)341         public long getId(Cursor cursor) {
342             return cursor.getLong(groupId);
343         }
344 
getSystemId(Cursor cursor)345         public String getSystemId(Cursor cursor) {
346             return cursor.getString(systemId);
347         }
348 
getSummaryCount(Cursor cursor)349         public int getSummaryCount(Cursor cursor) {
350             return cursor.getInt(summaryCount);
351         }
352 
isEmptyFFCGroup(Cursor cursor)353         public boolean isEmptyFFCGroup(Cursor cursor) {
354             if (accountType == -1 || isReadOnly == -1 ||
355                     systemId == -1 || summaryCount == -1) {
356                 throw new IllegalArgumentException("Projection is missing required columns");
357             }
358             return GoogleAccountType.ACCOUNT_TYPE.equals(cursor.getString(accountType))
359                     && cursor.getInt(isReadOnly) != 0
360                     && isSystemIdFFC(cursor.getString(systemId))
361                     && cursor.getInt(summaryCount) <= 0;
362         }
363     }
364 }
365