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