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