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.list; 18 19 import android.database.MatrixCursor; 20 import android.support.annotation.IntDef; 21 import android.support.annotation.Nullable; 22 import android.support.annotation.VisibleForTesting; 23 import com.android.dialer.common.Assert; 24 import com.android.dialer.searchfragment.common.SearchCursor; 25 import java.lang.annotation.Retention; 26 import java.lang.annotation.RetentionPolicy; 27 import java.util.ArrayList; 28 import java.util.List; 29 30 /** 31 * Manages all of the cursors needed for {@link SearchAdapter}. 32 * 33 * <p>This class accepts four data sources: 34 * 35 * <ul> 36 * <li>A contacts cursor {@link #setContactsCursor(SearchCursor)} 37 * <li>A google search results cursor {@link #setNearbyPlacesCursor(SearchCursor)} 38 * <li>A work directory cursor {@link #setCorpDirectoryCursor(SearchCursor)} 39 * <li>A list of action to be performed on a number {@link #setSearchActions(List)} 40 * </ul> 41 * 42 * <p>The key purpose of this class is to compose three aforementioned cursors together to function 43 * as one cursor. The key methods needed to utilize this class as a cursor are: 44 * 45 * <ul> 46 * <li>{@link #getCursor(int)} 47 * <li>{@link #getCount()} 48 * <li>{@link #getRowType(int)} 49 * </ul> 50 */ 51 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) 52 public final class SearchCursorManager { 53 54 /** IntDef for the different types of rows that can be shown when searching. */ 55 @Retention(RetentionPolicy.SOURCE) 56 @IntDef({ 57 SearchCursorManager.RowType.INVALID, 58 SearchCursorManager.RowType.CONTACT_HEADER, 59 SearchCursorManager.RowType.CONTACT_ROW, 60 SearchCursorManager.RowType.NEARBY_PLACES_HEADER, 61 SearchCursorManager.RowType.NEARBY_PLACES_ROW, 62 SearchCursorManager.RowType.DIRECTORY_HEADER, 63 SearchCursorManager.RowType.DIRECTORY_ROW, 64 SearchCursorManager.RowType.SEARCH_ACTION, 65 SearchCursorManager.RowType.LOCATION_REQUEST 66 }) 67 @interface RowType { 68 int INVALID = 0; 69 // TODO(calderwoodra) add suggestions header and list 70 /** Header to mark the start of contact rows. */ 71 int CONTACT_HEADER = 1; 72 /** A row containing contact information for contacts stored locally on device. */ 73 int CONTACT_ROW = 2; 74 /** Header to mark the end of contact rows and start of nearby places rows. */ 75 int NEARBY_PLACES_HEADER = 3; 76 /** A row containing nearby places information/search results. */ 77 int NEARBY_PLACES_ROW = 4; 78 /** Header to mark the end of the previous row set and start of directory rows. */ 79 int DIRECTORY_HEADER = 5; 80 /** A row containing contact information for contacts stored externally in corp directories. */ 81 int DIRECTORY_ROW = 6; 82 /** A row containing a search action */ 83 int SEARCH_ACTION = 7; 84 /** A row which requests location permission */ 85 int LOCATION_REQUEST = 8; 86 } 87 88 private static final LocationPermissionCursor LOCATION_PERMISSION_CURSOR = 89 new LocationPermissionCursor(new String[0]); 90 91 private SearchCursor contactsCursor = null; 92 private SearchCursor nearbyPlacesCursor = null; 93 private SearchCursor corpDirectoryCursor = null; 94 private List<Integer> searchActions = new ArrayList<>(); 95 96 private boolean showLocationPermissionRequest; 97 98 /** Returns true if the cursor changed. */ setContactsCursor(@ullable SearchCursor cursor)99 boolean setContactsCursor(@Nullable SearchCursor cursor) { 100 if (cursor == contactsCursor) { 101 return false; 102 } 103 104 if (cursor != null) { 105 contactsCursor = cursor; 106 } else { 107 contactsCursor = null; 108 } 109 return true; 110 } 111 112 /** Returns true if the cursor changed. */ setNearbyPlacesCursor(@ullable SearchCursor cursor)113 boolean setNearbyPlacesCursor(@Nullable SearchCursor cursor) { 114 if (cursor == nearbyPlacesCursor) { 115 return false; 116 } 117 118 if (cursor != null) { 119 nearbyPlacesCursor = cursor; 120 } else { 121 nearbyPlacesCursor = null; 122 } 123 return true; 124 } 125 126 /** Returns true if the value changed. */ showLocationPermissionRequest(boolean enabled)127 boolean showLocationPermissionRequest(boolean enabled) { 128 if (showLocationPermissionRequest == enabled) { 129 return false; 130 } 131 showLocationPermissionRequest = enabled; 132 return true; 133 } 134 135 /** Returns true if a cursor changed. */ setCorpDirectoryCursor(@ullable SearchCursor cursor)136 boolean setCorpDirectoryCursor(@Nullable SearchCursor cursor) { 137 if (cursor == corpDirectoryCursor) { 138 return false; 139 } 140 141 if (cursor != null) { 142 corpDirectoryCursor = cursor; 143 } else { 144 corpDirectoryCursor = null; 145 } 146 return true; 147 } 148 setQuery(String query)149 boolean setQuery(String query) { 150 boolean updated = false; 151 if (contactsCursor != null) { 152 updated = contactsCursor.updateQuery(query); 153 } 154 155 if (nearbyPlacesCursor != null) { 156 updated |= nearbyPlacesCursor.updateQuery(query); 157 } 158 159 if (corpDirectoryCursor != null) { 160 updated |= corpDirectoryCursor.updateQuery(query); 161 } 162 return updated; 163 } 164 165 /** Sets search actions, returning true if different from existing actions. */ setSearchActions(List<Integer> searchActions)166 boolean setSearchActions(List<Integer> searchActions) { 167 if (!this.searchActions.equals(searchActions)) { 168 this.searchActions = searchActions; 169 return true; 170 } 171 return false; 172 } 173 174 /** Returns {@link SearchActionViewHolder.Action}. */ getSearchAction(int position)175 int getSearchAction(int position) { 176 return searchActions.get(position - getCount() + searchActions.size()); 177 } 178 179 /** Returns the sum of counts of all cursors, including headers. */ getCount()180 int getCount() { 181 int count = 0; 182 if (contactsCursor != null) { 183 count += contactsCursor.getCount(); 184 } 185 186 if (showLocationPermissionRequest) { 187 count++; 188 } else if (nearbyPlacesCursor != null) { 189 count += nearbyPlacesCursor.getCount(); 190 } 191 192 if (corpDirectoryCursor != null) { 193 count += corpDirectoryCursor.getCount(); 194 } 195 196 return count + searchActions.size(); 197 } 198 199 @RowType getRowType(int position)200 int getRowType(int position) { 201 int cursorCount = getCount(); 202 if (position >= cursorCount) { 203 throw Assert.createIllegalStateFailException( 204 String.format("Invalid position: %d, cursor count: %d", position, cursorCount)); 205 } else if (position >= cursorCount - searchActions.size()) { 206 return RowType.SEARCH_ACTION; 207 } 208 209 SearchCursor cursor = getCursor(position); 210 if (cursor == contactsCursor) { 211 return cursor.isHeader() ? RowType.CONTACT_HEADER : RowType.CONTACT_ROW; 212 } 213 214 if (cursor == LOCATION_PERMISSION_CURSOR) { 215 return RowType.LOCATION_REQUEST; 216 } 217 218 if (cursor == nearbyPlacesCursor) { 219 return cursor.isHeader() ? RowType.NEARBY_PLACES_HEADER : RowType.NEARBY_PLACES_ROW; 220 } 221 222 if (cursor == corpDirectoryCursor) { 223 return cursor.isHeader() ? RowType.DIRECTORY_HEADER : RowType.DIRECTORY_ROW; 224 } 225 throw Assert.createIllegalStateFailException("No valid row type."); 226 } 227 228 /** 229 * Gets cursor corresponding to position in coalesced list of search cursors. 230 * 231 * @param position in coalesced list of search cursors 232 * @return Cursor moved to position specific to passed in position. 233 */ getCursor(int position)234 SearchCursor getCursor(int position) { 235 if (showLocationPermissionRequest) { 236 if (position == 0) { 237 return LOCATION_PERMISSION_CURSOR; 238 } 239 position--; 240 } 241 242 if (contactsCursor != null) { 243 int count = contactsCursor.getCount(); 244 245 if (position - count < 0) { 246 contactsCursor.moveToPosition(position); 247 return contactsCursor; 248 } 249 position -= count; 250 } 251 252 if (!showLocationPermissionRequest && nearbyPlacesCursor != null) { 253 int count = nearbyPlacesCursor.getCount(); 254 255 if (position - count < 0) { 256 nearbyPlacesCursor.moveToPosition(position); 257 return nearbyPlacesCursor; 258 } 259 position -= count; 260 } 261 262 if (corpDirectoryCursor != null) { 263 int count = corpDirectoryCursor.getCount(); 264 265 if (position - count < 0) { 266 corpDirectoryCursor.moveToPosition(position); 267 return corpDirectoryCursor; 268 } 269 position -= count; 270 } 271 272 throw Assert.createIllegalStateFailException("No valid cursor."); 273 } 274 275 /** removes all cursors. */ clear()276 void clear() { 277 contactsCursor = null; 278 nearbyPlacesCursor = null; 279 corpDirectoryCursor = null; 280 } 281 282 /** 283 * No-op implementation of {@link android.database.Cursor} and {@link SearchCursor} for 284 * representing location permission request row elements. 285 */ 286 private static class LocationPermissionCursor extends MatrixCursor implements SearchCursor { 287 LocationPermissionCursor(String[] columnNames)288 LocationPermissionCursor(String[] columnNames) { 289 super(columnNames); 290 } 291 292 @Override isHeader()293 public boolean isHeader() { 294 return false; 295 } 296 297 @Override updateQuery(String query)298 public boolean updateQuery(String query) { 299 return false; 300 } 301 302 @Override getDirectoryId()303 public long getDirectoryId() { 304 return 0; 305 } 306 } 307 } 308