Home | History | Annotate | Download | only in group
      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 
     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