1 /* 2 * Copyright (C) 2017 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.dialer.searchfragment.directories; 18 19 import android.content.Context; 20 import android.content.CursorLoader; 21 import android.database.Cursor; 22 import android.database.MatrixCursor; 23 import android.net.Uri; 24 import android.os.Build.VERSION; 25 import android.os.Build.VERSION_CODES; 26 import android.provider.ContactsContract; 27 import android.provider.ContactsContract.CommonDataKinds.Phone; 28 import android.support.annotation.NonNull; 29 import android.support.annotation.VisibleForTesting; 30 import com.android.dialer.common.cp2.DirectoryCompat; 31 import com.android.dialer.searchfragment.common.Projections; 32 import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory; 33 import java.util.ArrayList; 34 import java.util.List; 35 36 /** 37 * Cursor loader to load extended contacts on device. 38 * 39 * <p>This loader performs several database queries in serial and merges the resulting cursors 40 * together into {@link DirectoryContactsCursor}. If there are no results, the loader will return a 41 * null cursor. 42 */ 43 public final class DirectoryContactsCursorLoader extends CursorLoader { 44 45 private static final Uri ENTERPRISE_CONTENT_FILTER_URI = 46 Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise"); 47 48 private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000"; 49 private static final String PHONE_NUMBER_NOT_NULL = Phone.NUMBER + " IS NOT NULL"; 50 private static final String MAX_RESULTS = "10"; 51 52 private final String query; 53 private final List<Directory> directories; 54 private final Cursor[] cursors; 55 56 public DirectoryContactsCursorLoader(Context context, String query, List<Directory> directories) { 57 super( 58 context, 59 null, 60 Projections.DATA_PROJECTION, 61 IGNORE_NUMBER_TOO_LONG_CLAUSE + " AND " + PHONE_NUMBER_NOT_NULL, 62 null, 63 Phone.SORT_KEY_PRIMARY); 64 this.query = query; 65 this.directories = new ArrayList<>(directories); 66 cursors = new Cursor[directories.size()]; 67 } 68 69 @Override 70 public Cursor loadInBackground() { 71 for (int i = 0; i < directories.size(); i++) { 72 Directory directory = directories.get(i); 73 74 if (!DirectoryCompat.isRemoteDirectoryId(directory.getId()) 75 && !DirectoryCompat.isEnterpriseDirectoryId(directory.getId())) { 76 cursors[i] = null; 77 continue; 78 } 79 80 // Filter out invisible directories. 81 if (DirectoryCompat.isInvisibleDirectory(directory.getId())) { 82 cursors[i] = null; 83 continue; 84 } 85 86 Cursor cursor = 87 getContext() 88 .getContentResolver() 89 .query( 90 getContentFilterUri(query, directory.getId()), 91 getProjection(), 92 getSelection(), 93 getSelectionArgs(), 94 getSortOrder()); 95 // Even though the cursor specifies "WHERE PHONE_NUMBER IS NOT NULL" the Blackberry Hub app's 96 // directory extension doesn't appear to respect it, and sometimes returns a null phone 97 // number. In this case just hide the row entirely. See a bug. 98 cursors[i] = createMatrixCursorFilteringNullNumbers(cursor); 99 } 100 return DirectoryContactsCursor.newInstance(getContext(), cursors, directories); 101 } 102 103 private MatrixCursor createMatrixCursorFilteringNullNumbers(Cursor cursor) { 104 if (cursor == null) { 105 return null; 106 } 107 MatrixCursor matrixCursor = new MatrixCursor(cursor.getColumnNames()); 108 try { 109 if (cursor.moveToFirst()) { 110 do { 111 String number = cursor.getString(Projections.PHONE_NUMBER); 112 if (number == null) { 113 continue; 114 } 115 matrixCursor.addRow(objectArrayFromCursor(cursor)); 116 } while (cursor.moveToNext()); 117 } 118 } finally { 119 cursor.close(); 120 } 121 return matrixCursor; 122 } 123 124 @NonNull 125 private static Object[] objectArrayFromCursor(@NonNull Cursor cursor) { 126 Object[] values = new Object[cursor.getColumnCount()]; 127 for (int i = 0; i < cursor.getColumnCount(); i++) { 128 int fieldType = cursor.getType(i); 129 if (fieldType == Cursor.FIELD_TYPE_BLOB) { 130 values[i] = cursor.getBlob(i); 131 } else if (fieldType == Cursor.FIELD_TYPE_FLOAT) { 132 values[i] = cursor.getDouble(i); 133 } else if (fieldType == Cursor.FIELD_TYPE_INTEGER) { 134 values[i] = cursor.getLong(i); 135 } else if (fieldType == Cursor.FIELD_TYPE_STRING) { 136 values[i] = cursor.getString(i); 137 } else if (fieldType == Cursor.FIELD_TYPE_NULL) { 138 values[i] = null; 139 } else { 140 throw new IllegalStateException("Unknown fieldType (" + fieldType + ") for column: " + i); 141 } 142 } 143 return values; 144 } 145 146 @VisibleForTesting 147 static Uri getContentFilterUri(String query, long directoryId) { 148 Uri baseUri = 149 VERSION.SDK_INT >= VERSION_CODES.N 150 ? ENTERPRISE_CONTENT_FILTER_URI 151 : Phone.CONTENT_FILTER_URI; 152 153 return baseUri 154 .buildUpon() 155 .appendPath(query) 156 .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) 157 .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true") 158 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, MAX_RESULTS) 159 .build(); 160 } 161 } 162