summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/searchfragment/remote/RemoteContactsCursorLoader.java
blob: 5f92c490210e9f62b9619bebb5c4b567df09f78f (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
/*
 * 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.remote;

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 android.text.TextUtils;
import com.android.dialer.searchfragment.common.Projections;
import com.android.dialer.searchfragment.remote.RemoteDirectoriesCursorLoader.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 RemoteContactsCursor}. If there are no results, the loader will return a
 * null cursor.
 */
public final class RemoteContactsCursorLoader 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 RemoteContactsCursorLoader(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);
      // Since the on device contacts could be queried as remote directories and we already query
      // them in SearchContactsCursorLoader, avoid querying them again.
      // TODO(calderwoodra): It's a happy coincidence that on device contacts don't have directory
      // names set, leaving this todo to investigate a better way to isolate them from other remote
      // directories.
      if (TextUtils.isEmpty(directory.getDisplayName())) {
        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 RemoteContactsCursor.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, int 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();
  }
}