summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/searchfragment/directories/DirectoryContactsCursorLoader.java
blob: a94878577353fa2845c588aaa6c07e1f46af8afd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.dialer.searchfragment.directories;

import android.content.Context;
import android.content.CursorLoader;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import com.android.contacts.common.compat.DirectoryCompat;
import com.android.dialer.searchfragment.common.Projections;
import com.android.dialer.searchfragment.directories.DirectoriesCursorLoader.Directory;
import java.util.ArrayList;
import java.util.List;

/**
 * Cursor loader to load extended contacts on device.
 *
 * <p>This loader performs several database queries in serial and merges the resulting cursors
 * together into {@link DirectoryContactsCursor}. If there are no results, the loader will return a
 * null cursor.
 */
public final class DirectoryContactsCursorLoader extends CursorLoader {

  private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
      Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise");

  private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000";
  private static final String PHONE_NUMBER_NOT_NULL = Phone.NUMBER + " IS NOT NULL";
  private static final String MAX_RESULTS = "10";

  private final String query;
  private final List<Directory> directories;
  private final Cursor[] cursors;

  public DirectoryContactsCursorLoader(Context context, String query, List<Directory> directories) {
    super(
        context,
        null,
        Projections.DATA_PROJECTION,
        IGNORE_NUMBER_TOO_LONG_CLAUSE + " AND " + PHONE_NUMBER_NOT_NULL,
        null,
        Phone.SORT_KEY_PRIMARY);
    this.query = query;
    this.directories = new ArrayList<>(directories);
    cursors = new Cursor[directories.size()];
  }

  @Override
  public Cursor loadInBackground() {
    for (int i = 0; i < directories.size(); i++) {
      Directory directory = directories.get(i);

      // TODO(a bug): Consider moving DirectoryCompat out of "contacts/common" and share it
      // with PhoneLookups.
      if (!DirectoryCompat.isRemoteDirectoryId(directory.getId())
          && !DirectoryCompat.isEnterpriseDirectoryId(directory.getId())) {
        cursors[i] = null;
        continue;
      }

      // Filter out invisible directories.
      if (DirectoryCompat.isInvisibleDirectory(directory.getId())) {
        cursors[i] = null;
        continue;
      }

      Cursor cursor =
          getContext()
              .getContentResolver()
              .query(
                  getContentFilterUri(query, directory.getId()),
                  getProjection(),
                  getSelection(),
                  getSelectionArgs(),
                  getSortOrder());
      // Even though the cursor specifies "WHERE PHONE_NUMBER IS NOT NULL" the Blackberry Hub app's
      // directory extension doesn't appear to respect it, and sometimes returns a null phone
      // number. In this case just hide the row entirely. See a bug.
      cursors[i] = createMatrixCursorFilteringNullNumbers(cursor);
    }
    return DirectoryContactsCursor.newInstance(getContext(), cursors, directories);
  }

  private MatrixCursor createMatrixCursorFilteringNullNumbers(Cursor cursor) {
    if (cursor == null) {
      return null;
    }
    MatrixCursor matrixCursor = new MatrixCursor(cursor.getColumnNames());
    try {
      if (cursor.moveToFirst()) {
        do {
          String number = cursor.getString(Projections.PHONE_NUMBER);
          if (number == null) {
            continue;
          }
          matrixCursor.addRow(objectArrayFromCursor(cursor));
        } while (cursor.moveToNext());
      }
    } finally {
      cursor.close();
    }
    return matrixCursor;
  }

  @NonNull
  private static Object[] objectArrayFromCursor(@NonNull Cursor cursor) {
    Object[] values = new Object[cursor.getColumnCount()];
    for (int i = 0; i < cursor.getColumnCount(); i++) {
      int fieldType = cursor.getType(i);
      if (fieldType == Cursor.FIELD_TYPE_BLOB) {
        values[i] = cursor.getBlob(i);
      } else if (fieldType == Cursor.FIELD_TYPE_FLOAT) {
        values[i] = cursor.getDouble(i);
      } else if (fieldType == Cursor.FIELD_TYPE_INTEGER) {
        values[i] = cursor.getLong(i);
      } else if (fieldType == Cursor.FIELD_TYPE_STRING) {
        values[i] = cursor.getString(i);
      } else if (fieldType == Cursor.FIELD_TYPE_NULL) {
        values[i] = null;
      } else {
        throw new IllegalStateException("Unknown fieldType (" + fieldType + ") for column: " + i);
      }
    }
    return values;
  }

  @VisibleForTesting
  static Uri getContentFilterUri(String query, long directoryId) {
    Uri baseUri =
        VERSION.SDK_INT >= VERSION_CODES.N
            ? ENTERPRISE_CONTENT_FILTER_URI
            : Phone.CONTENT_FILTER_URI;

    return baseUri
        .buildUpon()
        .appendPath(query)
        .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
        .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true")
        .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, MAX_RESULTS)
        .build();
  }
}