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