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