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
17package com.android.contacts.group;
18
19import android.app.Fragment;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.database.Cursor;
24import android.net.Uri;
25import android.os.Bundle;
26import android.provider.ContactsContract;
27import android.provider.ContactsContract.Contacts;
28import android.provider.ContactsContract.Groups;
29import android.text.TextUtils;
30
31import com.android.contacts.ContactsUtils;
32import com.android.contacts.GroupListLoader;
33import com.android.contacts.activities.ContactSelectionActivity;
34import com.android.contacts.list.ContactsSectionIndexer;
35import com.android.contacts.list.UiIntentActions;
36import com.android.contacts.model.account.GoogleAccountType;
37
38import java.util.ArrayList;
39import java.util.Arrays;
40import java.util.HashSet;
41import java.util.List;
42import java.util.Set;
43
44/**
45 * Group utility methods.
46 */
47public 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
69    private GroupUtil() {
70    }
71
72    /** Returns a {@link GroupListItem} read from the given cursor and 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
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 */
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) */
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. */
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
180    public static String convertArrayToString(long[] list) {
181        if (list == null || list.length == 0) return "";
182        return Arrays.toString(list).replace("[", "").replace("]", "");
183    }
184
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
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     */
211    public static boolean isEmptyFFCGroup(GroupListItem groupListItem) {
212        return groupListItem.isReadOnly()
213                && isSystemIdFFC(groupListItem.getSystemId())
214                && (groupListItem.getMemberCount() <= 0);
215    }
216
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     */
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     */
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     */
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     */
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
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
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
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
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
337        public String getTitle(Cursor cursor) {
338            return cursor.getString(title);
339        }
340
341        public long getId(Cursor cursor) {
342            return cursor.getLong(groupId);
343        }
344
345        public String getSystemId(Cursor cursor) {
346            return cursor.getString(systemId);
347        }
348
349        public int getSummaryCount(Cursor cursor) {
350            return cursor.getInt(summaryCount);
351        }
352
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