• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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 package com.android.car.dialer.telecom;
17 
18 import android.content.ContentResolver;
19 import android.content.Context;
20 import android.content.CursorLoader;
21 import android.content.Loader;
22 import android.database.Cursor;
23 import android.net.Uri;
24 import android.provider.BaseColumns;
25 import android.provider.CallLog;
26 import android.provider.ContactsContract;
27 import android.support.annotation.IntDef;
28 import android.text.TextUtils;
29 import android.util.Log;
30 
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.List;
34 
35 /**
36  * Manages loading different types of call logs.
37  * Currently supports:
38  * All calls
39  * Missed calls
40  * speed dial calls
41  */
42 public class PhoneLoader {
43     private static final String TAG = "Em.PhoneLoader";
44 
45     /** CALL_TYPE_ALL and _MISSED's values are assigned to be consistent with the Dialer **/
46     public final static int CALL_TYPE_ALL = -1;
47     public final static int CALL_TYPE_MISSED = CallLog.Calls.MISSED_TYPE;
48     /** Starred and frequent **/
49     public final static int CALL_TYPE_SPEED_DIAL = 2;
50 
51     @IntDef({
52             CallType.CALL_TYPE_ALL,
53             CallType.INCOMING_TYPE,
54             CallType.OUTGOING_TYPE,
55             CallType.MISSED_TYPE,
56     })
57     public @interface CallType {
58         int CALL_TYPE_ALL = -1;
59         int INCOMING_TYPE = CallLog.Calls.INCOMING_TYPE;
60         int OUTGOING_TYPE = CallLog.Calls.OUTGOING_TYPE;
61         int MISSED_TYPE = CallLog.Calls.MISSED_TYPE;
62     }
63 
64     private static final int NUM_LOGS_TO_DISPLAY = 100;
65     private static final String[] EMPTY_STRING_ARRAY = new String[0];
66 
67     public static final int INCOMING_TYPE = 1;
68     public static final int OUTGOING_TYPE = 2;
69     public static final int MISSED_TYPE = 3;
70     public static final int VOICEMAIL_TYPE = 4;
71 
72     private static HashMap<String, String> sNumberCache;
73 
74     /**
75      * Hybrid Factory for creating a Contact Loader that also immediately starts its execution.
76      * Note: NOT to be used wit LoaderManagers.
77      */
registerCallObserver(int type, Context context, Loader.OnLoadCompleteListener<Cursor> listener)78     public static CursorLoader registerCallObserver(int type,
79             Context context, Loader.OnLoadCompleteListener<Cursor> listener) {
80         if (Log.isLoggable(TAG, Log.DEBUG)) {
81             Log.d(TAG, "registerCallObserver: type: " + type + ", listener: " + listener);
82         }
83 
84         switch (type) {
85             case CALL_TYPE_ALL:
86             case CALL_TYPE_MISSED:
87                 return fetchCallLog(type, context, listener);
88             case CALL_TYPE_SPEED_DIAL:
89                 CursorLoader loader = newStrequentContactLoader(context);
90                 loader.registerListener(0, listener);
91                 loader.startLoading();
92                 return loader;
93             default:
94                 throw new UnsupportedOperationException("Unknown CALL_TYPE " + type + ".");
95         }
96     }
97 
98     /**
99      * Factory method for creating a Loader that will fetch strequent contacts from the phone.
100      */
newStrequentContactLoader(Context context)101     public static CursorLoader newStrequentContactLoader(Context context) {
102         Uri uri = ContactsContract.Contacts.CONTENT_STREQUENT_URI.buildUpon()
103                 .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
104                 .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").build();
105 
106         return new CursorLoader(context, uri, null, null, null, null);
107     }
108 
109     // TODO(mcrico): Separate into a factory method and move configuration to registerCallObserver
fetchCallLog(int callType, Context context, Loader.OnLoadCompleteListener<Cursor> listener)110     private static CursorLoader fetchCallLog(int callType,
111             Context context, Loader.OnLoadCompleteListener<Cursor> listener) {
112         if (Log.isLoggable(TAG, Log.DEBUG)) {
113             Log.d(TAG, "fetchCallLog");
114         }
115 
116         // We need to check for NULL explicitly otherwise entries with where READ is NULL
117         // may not match either the query or its negation.
118         // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
119         StringBuilder where = new StringBuilder();
120         List<String> selectionArgs = new ArrayList<String>();
121 
122         if (callType > CALL_TYPE_ALL) {
123             // add a filter for call type
124             where.append(String.format("(%s = ?)", CallLog.Calls.TYPE));
125             selectionArgs.add(Integer.toString(callType));
126         }
127         String selection = where.length() > 0 ? where.toString() : null;
128 
129         if (Log.isLoggable(TAG, Log.DEBUG)) {
130             Log.d(TAG, "accessingCallLog");
131         }
132 
133         Uri uri = CallLog.Calls.CONTENT_URI.buildUpon()
134                 .appendQueryParameter(CallLog.Calls.LIMIT_PARAM_KEY,
135                         Integer.toString(NUM_LOGS_TO_DISPLAY))
136                 .build();
137         CursorLoader loader = new CursorLoader(context, uri, null, selection,
138                 selectionArgs.toArray(EMPTY_STRING_ARRAY), CallLog.Calls.DEFAULT_SORT_ORDER);
139         loader.registerListener(0, listener);
140         loader.startLoading();
141         return loader;
142     }
143 
144     /**
145      * @return The column index of the contact id. It should be {@link BaseColumns#_ID}. However,
146      * if that fails use {@link android.provider.ContactsContract.RawContacts#CONTACT_ID}.
147      * If that also fails, we use the first column in the table.
148      */
getIdColumnIndex(Cursor cursor)149     public static int getIdColumnIndex(Cursor cursor) {
150         int ret = cursor.getColumnIndex(BaseColumns._ID);
151         if (ret == -1) {
152             if (Log.isLoggable(TAG, Log.INFO)) {
153                 Log.i(TAG, "Falling back to contact_id instead of _id");
154             }
155 
156             // Some versions of the ContactsProvider on LG don't have an _id column but instead
157             // use contact_id. If the lookup for _id fails, we fallback to contact_id.
158             ret = cursor.getColumnIndexOrThrow(ContactsContract.RawContacts.CONTACT_ID);
159         }
160         if (ret == -1) {
161             Log.e(TAG, "Neither _id or contact_id exist! Falling back to column 0. " +
162                     "There is no guarantee that this will work!");
163             ret = 0;
164         }
165         return ret;
166     }
167 
168     /**
169      * @return The column index of the number.
170      * Will return a valid column for call log or contacts queries.
171      */
getNumberColumnIndex(Cursor cursor)172     public static int getNumberColumnIndex(Cursor cursor) {
173         int numberColumn = cursor.getColumnIndex(CallLog.Calls.NUMBER);
174         if (numberColumn == -1) {
175             numberColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
176         }
177         return numberColumn;
178     }
179 
180 
181     /**
182      * @return The column index of the number type.
183      * Will return a valid column for call log or contacts queries.
184      */
getTypeColumnIndex(Cursor cursor)185     public static int getTypeColumnIndex(Cursor cursor) {
186         int typeColumn = cursor.getColumnIndex(CallLog.Calls.TYPE);
187         if (typeColumn == -1) {
188             typeColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE);
189         }
190         return typeColumn;
191     }
192 
193     /**
194      * @return The column index of the name.
195      * Will return a valid column for call log or contacts queries.
196      */
getNameColumnIndex(Cursor cursor)197     public static int getNameColumnIndex(Cursor cursor) {
198         int typeColumn = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME);
199         if (typeColumn == -1) {
200             typeColumn = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
201         }
202         return typeColumn;
203     }
204 
205     /**
206      * @return The phone number for the contact. Most phones will simply get the value in the
207      * column returned by {@link #getNumberColumnIndex(Cursor)}. However, some devices
208      * such as the Galaxy S6 return null for those columns. In those cases, we use the
209      * contact id (which we hopefully do have) to look up just the phone number for that
210      * specific contact.
211      */
getPhoneNumber(Cursor cursor, ContentResolver cr)212     public static String getPhoneNumber(Cursor cursor, ContentResolver cr) {
213         int columnIndex = getNumberColumnIndex(cursor);
214         String number = cursor.getString(columnIndex);
215         if (number == null) {
216             Log.w(TAG, "Phone number is null. Using fallback method.");
217             int idColumnIndex = getIdColumnIndex(cursor);
218             String idColumnName = cursor.getColumnName(idColumnIndex);
219             String contactId = cursor.getString(idColumnIndex);
220             getNumberFromContactId(cr, idColumnName, contactId);
221         }
222         return number;
223     }
224 
225     /**
226      * Return the phone number for the given contact id.
227      *
228      * @param columnName On some phones, we have to use non-standard columns for the primary key.
229      * @param id         The value in the columnName for the desired contact.
230      * @return The phone number for the given contact or empty string if there was an error.
231      */
getNumberFromContactId(ContentResolver cr, String columnName, String id)232     public static String getNumberFromContactId(ContentResolver cr, String columnName, String id) {
233         if (TextUtils.isEmpty(id)) {
234             Log.e(TAG, "You must specify a valid id to get a contact's phone number.");
235             return "";
236         }
237         if (sNumberCache == null) {
238             sNumberCache = new HashMap<>();
239         } else if (sNumberCache.containsKey(id)) {
240             return sNumberCache.get(id);
241         }
242 
243         Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
244         Cursor phoneNumberCursor = cr.query(uri,
245                 new String[]{ContactsContract.CommonDataKinds.Phone.NUMBER},
246                 columnName + " = ?", new String[]{id}, null);
247 
248         if (!phoneNumberCursor.moveToFirst()) {
249             Log.e(TAG, "Unable to move phone number cursor to the first item.");
250             return "";
251         }
252         String number = phoneNumberCursor.getString(0);
253         phoneNumberCursor.close();
254         return number;
255     }
256 }
257