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