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.speeddial; 18 19 import android.annotation.SuppressLint; 20 import android.database.Cursor; 21 import android.database.MatrixCursor; 22 import android.database.MergeCursor; 23 import android.support.annotation.IntDef; 24 import android.support.annotation.StringRes; 25 import com.android.dialer.common.Assert; 26 import java.lang.annotation.Retention; 27 import java.lang.annotation.RetentionPolicy; 28 import java.util.ArrayList; 29 import java.util.List; 30 31 /** Cursor for favorites contacts. */ 32 final class SpeedDialCursor extends MergeCursor { 33 34 /** 35 * Caps the speed dial list to contain at most 20 contacts, including favorites and suggestions. 36 * It is only a soft limit though, for the case that there are more than 20 favorite contacts. 37 */ 38 private static final int SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT = 20; 39 40 private static final String[] HEADER_CURSOR_PROJECTION = {"header"}; 41 private static final int HEADER_COLUMN_POSITION = 0; 42 private boolean hasFavorites; 43 44 @Retention(RetentionPolicy.SOURCE) 45 @IntDef({RowType.HEADER, RowType.STARRED, RowType.SUGGESTION}) 46 @interface RowType { 47 int HEADER = 0; 48 int STARRED = 1; 49 int SUGGESTION = 2; 50 } 51 52 public static SpeedDialCursor newInstance(Cursor strequentCursor) { 53 if (strequentCursor == null || strequentCursor.getCount() == 0) { 54 return null; 55 } 56 SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor)); 57 strequentCursor.close(); 58 return cursor; 59 } 60 61 private static Cursor[] buildCursors(Cursor strequentCursor) { 62 MatrixCursor starred = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); 63 MatrixCursor suggestions = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION); 64 65 strequentCursor.moveToPosition(-1); 66 while (strequentCursor.moveToNext()) { 67 if (strequentCursor.getPosition() != 0) { 68 long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID); 69 int position = strequentCursor.getPosition(); 70 boolean duplicate = false; 71 // Iterate backwards through the cursor to check that this isn't a duplicate contact 72 // TODO(calderwoodra): improve this algorithm (currently O(n^2)). 73 while (strequentCursor.moveToPrevious() && !duplicate) { 74 duplicate |= 75 strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID) == contactId; 76 } 77 strequentCursor.moveToPosition(position); 78 if (duplicate) { 79 continue; 80 } 81 } 82 83 if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { 84 StrequentContactsCursorLoader.addToCursor(starred, strequentCursor); 85 } else if (starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT) { 86 // Since all starred contacts come before each non-starred contact, it's safe to assume that 87 // this list will never exceed the soft limit unless there are more starred contacts than 88 // the limit permits. 89 StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor); 90 } 91 } 92 93 List<Cursor> cursorList = new ArrayList<>(); 94 if (starred.getCount() > 0) { 95 cursorList.add(createHeaderCursor(R.string.favorites_header)); 96 cursorList.add(starred); 97 } 98 if (suggestions.getCount() > 0) { 99 cursorList.add(createHeaderCursor(R.string.suggestions_header)); 100 cursorList.add(suggestions); 101 } 102 return cursorList.toArray(new Cursor[cursorList.size()]); 103 } 104 105 private static Cursor createHeaderCursor(@StringRes int header) { 106 MatrixCursor cursor = new MatrixCursor(HEADER_CURSOR_PROJECTION); 107 cursor.newRow().add(HEADER_CURSOR_PROJECTION[HEADER_COLUMN_POSITION], header); 108 return cursor; 109 } 110 111 @RowType 112 int getRowType(int position) { 113 moveToPosition(position); 114 if (getColumnCount() == 1) { 115 return RowType.HEADER; 116 } else if (getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { 117 return RowType.STARRED; 118 } else { 119 return RowType.SUGGESTION; 120 } 121 } 122 123 @SuppressLint("DefaultLocale") 124 @StringRes 125 int getHeader() { 126 if (getRowType(getPosition()) != RowType.HEADER) { 127 throw Assert.createIllegalStateFailException( 128 String.format("Current position (%d) is not a header.", getPosition())); 129 } 130 return getInt(HEADER_COLUMN_POSITION); 131 } 132 133 public boolean hasFavorites() { 134 return hasFavorites; 135 } 136 137 private SpeedDialCursor(Cursor[] cursors) { 138 super(cursors); 139 for (Cursor cursor : cursors) { 140 cursor.moveToFirst(); 141 if (cursor.getColumnCount() != 1 142 && cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) { 143 hasFavorites = true; 144 break; 145 } 146 } 147 } 148 } 149