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 
17 package com.android.dialer.calllog;
18 
19 import com.google.common.annotations.VisibleForTesting;
20 
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.provider.CallLog;
29 import android.provider.VoicemailContract.Voicemails;
30 import android.telecom.PhoneAccountHandle;
31 import android.telephony.PhoneNumberUtils;
32 import android.text.TextUtils;
33 import android.util.Log;
34 
35 import com.android.contacts.common.GeoUtil;
36 import com.android.contacts.common.compat.CompatUtils;
37 import com.android.contacts.common.util.PermissionsUtil;
38 import com.android.dialer.PhoneCallDetails;
39 import com.android.dialer.compat.CallsSdkCompat;
40 import com.android.dialer.database.VoicemailArchiveContract;
41 import com.android.dialer.util.AsyncTaskExecutor;
42 import com.android.dialer.util.AsyncTaskExecutors;
43 import com.android.dialer.util.PhoneNumberUtil;
44 import com.android.dialer.util.TelecomUtil;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.Locale;
49 
50 public class CallLogAsyncTaskUtil {
51     private static String TAG = CallLogAsyncTaskUtil.class.getSimpleName();
52 
53    /** The enumeration of {@link AsyncTask} objects used in this class. */
54     public enum Tasks {
55         DELETE_VOICEMAIL,
56         DELETE_CALL,
57         DELETE_BLOCKED_CALL,
58         MARK_VOICEMAIL_READ,
59         MARK_CALL_READ,
60         GET_CALL_DETAILS,
61         UPDATE_DURATION
62     }
63 
64     private static final class CallDetailQuery {
65 
66         private static final String[] CALL_LOG_PROJECTION_INTERNAL = new String[] {
67             CallLog.Calls.DATE,
68             CallLog.Calls.DURATION,
69             CallLog.Calls.NUMBER,
70             CallLog.Calls.TYPE,
71             CallLog.Calls.COUNTRY_ISO,
72             CallLog.Calls.GEOCODED_LOCATION,
73             CallLog.Calls.NUMBER_PRESENTATION,
74             CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME,
75             CallLog.Calls.PHONE_ACCOUNT_ID,
76             CallLog.Calls.FEATURES,
77             CallLog.Calls.DATA_USAGE,
78             CallLog.Calls.TRANSCRIPTION
79         };
80         public static final String[] CALL_LOG_PROJECTION;
81 
82         static final int DATE_COLUMN_INDEX = 0;
83         static final int DURATION_COLUMN_INDEX = 1;
84         static final int NUMBER_COLUMN_INDEX = 2;
85         static final int CALL_TYPE_COLUMN_INDEX = 3;
86         static final int COUNTRY_ISO_COLUMN_INDEX = 4;
87         static final int GEOCODED_LOCATION_COLUMN_INDEX = 5;
88         static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6;
89         static final int ACCOUNT_COMPONENT_NAME = 7;
90         static final int ACCOUNT_ID = 8;
91         static final int FEATURES = 9;
92         static final int DATA_USAGE = 10;
93         static final int TRANSCRIPTION_COLUMN_INDEX = 11;
94         static final int POST_DIAL_DIGITS = 12;
95         static final int VIA_NUMBER = 13;
96 
97         static {
98             ArrayList<String> projectionList = new ArrayList<>();
Arrays.asList(CALL_LOG_PROJECTION_INTERNAL)99             projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL));
100             if (CompatUtils.isNCompatible()) {
101                 projectionList.add(CallsSdkCompat.POST_DIAL_DIGITS);
102                 projectionList.add(CallsSdkCompat.VIA_NUMBER);
103             }
projectionList.trimToSize()104             projectionList.trimToSize();
105             CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]);
106         }
107     }
108 
109     private static class CallLogDeleteBlockedCallQuery {
110         static final String[] PROJECTION = new String[] {
111             CallLog.Calls._ID,
112             CallLog.Calls.DATE
113         };
114 
115         static final int ID_COLUMN_INDEX = 0;
116         static final int DATE_COLUMN_INDEX = 1;
117     }
118 
119     public interface CallLogAsyncTaskListener {
onDeleteCall()120         void onDeleteCall();
onDeleteVoicemail()121         void onDeleteVoicemail();
onGetCallDetails(PhoneCallDetails[] details)122         void onGetCallDetails(PhoneCallDetails[] details);
123     }
124 
125     public interface OnCallLogQueryFinishedListener {
onQueryFinished(boolean hasEntry)126         void onQueryFinished(boolean hasEntry);
127     }
128 
129     // Try to identify if a call log entry corresponds to a number which was blocked. We match by
130     // by comparing its creation time to the time it was added in the InCallUi and seeing if they
131     // fall within a certain threshold.
132     private static final int MATCH_BLOCKED_CALL_THRESHOLD_MS = 3000;
133 
134     private static AsyncTaskExecutor sAsyncTaskExecutor;
135 
initTaskExecutor()136     private static void initTaskExecutor() {
137         sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
138     }
139 
getCallDetails( final Context context, final Uri[] callUris, final CallLogAsyncTaskListener callLogAsyncTaskListener)140     public static void getCallDetails(
141             final Context context,
142             final Uri[] callUris,
143             final CallLogAsyncTaskListener callLogAsyncTaskListener) {
144         if (sAsyncTaskExecutor == null) {
145             initTaskExecutor();
146         }
147 
148         sAsyncTaskExecutor.submit(Tasks.GET_CALL_DETAILS,
149                 new AsyncTask<Void, Void, PhoneCallDetails[]>() {
150                     @Override
151                     public PhoneCallDetails[] doInBackground(Void... params) {
152                         // TODO: All calls correspond to the same person, so make a single lookup.
153                         final int numCalls = callUris.length;
154                         PhoneCallDetails[] details = new PhoneCallDetails[numCalls];
155                         try {
156                             for (int index = 0; index < numCalls; ++index) {
157                                 details[index] =
158                                         getPhoneCallDetailsForUri(context, callUris[index]);
159                             }
160                             return details;
161                         } catch (IllegalArgumentException e) {
162                             // Something went wrong reading in our primary data.
163                             Log.w(TAG, "Invalid URI starting call details", e);
164                             return null;
165                         }
166                     }
167 
168                     @Override
169                     public void onPostExecute(PhoneCallDetails[] phoneCallDetails) {
170                         if (callLogAsyncTaskListener != null) {
171                             callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails);
172                         }
173                     }
174                 });
175     }
176 
177     /**
178      * Return the phone call details for a given call log URI.
179      */
getPhoneCallDetailsForUri(Context context, Uri callUri)180     private static PhoneCallDetails getPhoneCallDetailsForUri(Context context, Uri callUri) {
181         Cursor cursor = context.getContentResolver().query(
182                 callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null);
183 
184         try {
185             if (cursor == null || !cursor.moveToFirst()) {
186                 throw new IllegalArgumentException("Cannot find content: " + callUri);
187             }
188 
189             // Read call log.
190             final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX);
191             final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX);
192             final String postDialDigits = CompatUtils.isNCompatible()
193                     ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS) : "";
194             final String viaNumber = CompatUtils.isNCompatible() ?
195                     cursor.getString(CallDetailQuery.VIA_NUMBER) : "";
196             final int numberPresentation =
197                     cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX);
198 
199             final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
200                     cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME),
201                     cursor.getString(CallDetailQuery.ACCOUNT_ID));
202 
203             // If this is not a regular number, there is no point in looking it up in the contacts.
204             ContactInfoHelper contactInfoHelper =
205                     new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context));
206             boolean isVoicemail = PhoneNumberUtil.isVoicemailNumber(context, accountHandle, number);
207             boolean shouldLookupNumber =
208                     PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemail;
209             ContactInfo info = ContactInfo.EMPTY;
210 
211             if (shouldLookupNumber) {
212                 ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso);
213                 info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY;
214             }
215 
216             PhoneCallDetails details = new PhoneCallDetails(
217                     context, number, numberPresentation, info.formattedNumber,
218                     postDialDigits, isVoicemail);
219 
220             details.viaNumber = viaNumber;
221             details.accountHandle = accountHandle;
222             details.contactUri = info.lookupUri;
223             details.namePrimary = info.name;
224             details.nameAlternative = info.nameAlternative;
225             details.numberType = info.type;
226             details.numberLabel = info.label;
227             details.photoUri = info.photoUri;
228             details.sourceType = info.sourceType;
229             details.objectId = info.objectId;
230 
231             details.callTypes = new int[] {
232                 cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX)
233             };
234             details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX);
235             details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX);
236             details.features = cursor.getInt(CallDetailQuery.FEATURES);
237             details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX);
238             details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX);
239 
240             details.countryIso = !TextUtils.isEmpty(countryIso) ? countryIso
241                     : GeoUtil.getCurrentCountryIso(context);
242 
243             if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) {
244                 details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE);
245             }
246 
247             return details;
248         } finally {
249             if (cursor != null) {
250                 cursor.close();
251             }
252         }
253     }
254 
255 
256     /**
257      * Delete specified calls from the call log.
258      *
259      * @param context The context.
260      * @param callIds String of the callIds to delete from the call log, delimited by commas (",").
261      * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted.
262      */
deleteCalls( final Context context, final String callIds, final CallLogAsyncTaskListener callLogAsyncTaskListener)263     public static void deleteCalls(
264             final Context context,
265             final String callIds,
266             final CallLogAsyncTaskListener callLogAsyncTaskListener) {
267         if (sAsyncTaskExecutor == null) {
268             initTaskExecutor();
269         }
270 
271         sAsyncTaskExecutor.submit(Tasks.DELETE_CALL, new AsyncTask<Void, Void, Void>() {
272             @Override
273             public Void doInBackground(Void... params) {
274                 context.getContentResolver().delete(
275                         TelecomUtil.getCallLogUri(context),
276                         CallLog.Calls._ID + " IN (" + callIds + ")", null);
277                 return null;
278             }
279 
280             @Override
281             public void onPostExecute(Void result) {
282                 if (callLogAsyncTaskListener != null) {
283                     callLogAsyncTaskListener.onDeleteCall();
284                 }
285             }
286         });
287     }
288 
289     /**
290      * Deletes the last call made by the number within a threshold of the call time added in the
291      * call log, assuming it is a blocked call for which no entry should be shown.
292      *
293      * @param context The context.
294      * @param number Number of blocked call, for which to delete the call log entry.
295      * @param timeAddedMs The time the number was added to InCall, in milliseconds.
296      * @param listener The listener to invoke after looking up for a call log entry matching the
297      *     number and time added.
298      */
deleteBlockedCall( final Context context, final String number, final long timeAddedMs, final OnCallLogQueryFinishedListener listener)299     public static void deleteBlockedCall(
300             final Context context,
301             final String number,
302             final long timeAddedMs,
303             final OnCallLogQueryFinishedListener listener) {
304         if (sAsyncTaskExecutor == null) {
305             initTaskExecutor();
306         }
307 
308         sAsyncTaskExecutor.submit(Tasks.DELETE_BLOCKED_CALL, new AsyncTask<Void, Void, Long>() {
309             @Override
310             public Long doInBackground(Void... params) {
311                 // First, lookup the call log entry of the most recent call with this number.
312                 Cursor cursor = context.getContentResolver().query(
313                         TelecomUtil.getCallLogUri(context),
314                         CallLogDeleteBlockedCallQuery.PROJECTION,
315                         CallLog.Calls.NUMBER + "= ?",
316                         new String[] { number },
317                         CallLog.Calls.DATE + " DESC LIMIT 1");
318 
319                 // If match is found, delete this call log entry and return the call log entry id.
320                 if (cursor.moveToFirst()) {
321                     long creationTime =
322                             cursor.getLong(CallLogDeleteBlockedCallQuery.DATE_COLUMN_INDEX);
323                     if (timeAddedMs > creationTime
324                             && timeAddedMs - creationTime < MATCH_BLOCKED_CALL_THRESHOLD_MS) {
325                         long callLogEntryId =
326                                 cursor.getLong(CallLogDeleteBlockedCallQuery.ID_COLUMN_INDEX);
327                         context.getContentResolver().delete(
328                                 TelecomUtil.getCallLogUri(context),
329                                 CallLog.Calls._ID + " IN (" + callLogEntryId + ")",
330                                 null);
331                         return callLogEntryId;
332                     }
333                 }
334                 return (long) -1;
335             }
336 
337             @Override
338             public void onPostExecute(Long callLogEntryId) {
339                 if (listener != null) {
340                     listener.onQueryFinished(callLogEntryId >= 0);
341                 }
342             }
343         });
344     }
345 
346 
markVoicemailAsRead(final Context context, final Uri voicemailUri)347     public static void markVoicemailAsRead(final Context context, final Uri voicemailUri) {
348         if (sAsyncTaskExecutor == null) {
349             initTaskExecutor();
350         }
351 
352         sAsyncTaskExecutor.submit(Tasks.MARK_VOICEMAIL_READ, new AsyncTask<Void, Void, Void>() {
353             @Override
354             public Void doInBackground(Void... params) {
355                 ContentValues values = new ContentValues();
356                 values.put(Voicemails.IS_READ, true);
357                 context.getContentResolver().update(
358                         voicemailUri, values, Voicemails.IS_READ + " = 0", null);
359 
360                 Intent intent = new Intent(context, CallLogNotificationsService.class);
361                 intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
362                 context.startService(intent);
363                 return null;
364             }
365         });
366     }
367 
deleteVoicemail( final Context context, final Uri voicemailUri, final CallLogAsyncTaskListener callLogAsyncTaskListener)368     public static void deleteVoicemail(
369             final Context context,
370             final Uri voicemailUri,
371             final CallLogAsyncTaskListener callLogAsyncTaskListener) {
372         if (sAsyncTaskExecutor == null) {
373             initTaskExecutor();
374         }
375 
376         sAsyncTaskExecutor.submit(Tasks.DELETE_VOICEMAIL, new AsyncTask<Void, Void, Void>() {
377             @Override
378             public Void doInBackground(Void... params) {
379                 context.getContentResolver().delete(voicemailUri, null, null);
380                 return null;
381             }
382 
383             @Override
384             public void onPostExecute(Void result) {
385                 if (callLogAsyncTaskListener != null) {
386                     callLogAsyncTaskListener.onDeleteVoicemail();
387                 }
388             }
389         });
390     }
391 
markCallAsRead(final Context context, final long[] callIds)392     public static void markCallAsRead(final Context context, final long[] callIds) {
393         if (!PermissionsUtil.hasPhonePermissions(context)) {
394             return;
395         }
396         if (sAsyncTaskExecutor == null) {
397             initTaskExecutor();
398         }
399 
400         sAsyncTaskExecutor.submit(Tasks.MARK_CALL_READ, new AsyncTask<Void, Void, Void>() {
401             @Override
402             public Void doInBackground(Void... params) {
403 
404                 StringBuilder where = new StringBuilder();
405                 where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE);
406                 where.append(" AND ");
407 
408                 Long[] callIdLongs = new Long[callIds.length];
409                 for (int i = 0; i < callIds.length; i++) {
410                     callIdLongs[i] = callIds[i];
411                 }
412                 where.append(CallLog.Calls._ID).append(
413                         " IN (" + TextUtils.join(",", callIdLongs) + ")");
414 
415                 ContentValues values = new ContentValues(1);
416                 values.put(CallLog.Calls.IS_READ, "1");
417                 context.getContentResolver().update(
418                         CallLog.Calls.CONTENT_URI, values, where.toString(), null);
419                 return null;
420             }
421         });
422     }
423 
424     /**
425      * Updates the duration of a voicemail call log entry if the duration given is greater than 0,
426      * and if if the duration currently in the database is less than or equal to 0 (non-existent).
427      */
updateVoicemailDuration( final Context context, final Uri voicemailUri, final long duration)428     public static void updateVoicemailDuration(
429             final Context context,
430             final Uri voicemailUri,
431             final long duration) {
432         if (duration <= 0 || !PermissionsUtil.hasPhonePermissions(context)) {
433             return;
434         }
435         if (sAsyncTaskExecutor == null) {
436             initTaskExecutor();
437         }
438 
439         sAsyncTaskExecutor.submit(Tasks.UPDATE_DURATION, new AsyncTask<Void, Void, Void>() {
440             @Override
441             public Void doInBackground(Void... params) {
442                 ContentResolver contentResolver = context.getContentResolver();
443                 Cursor cursor = contentResolver.query(
444                         voicemailUri,
445                         new String[] { VoicemailArchiveContract.VoicemailArchive.DURATION },
446                         null, null, null);
447                 if (cursor != null && cursor.moveToFirst() && cursor.getInt(
448                         cursor.getColumnIndex(
449                                 VoicemailArchiveContract.VoicemailArchive.DURATION)) <= 0) {
450                     ContentValues values = new ContentValues(1);
451                     values.put(CallLog.Calls.DURATION, duration);
452                     context.getContentResolver().update(voicemailUri, values, null, null);
453                 }
454                 return null;
455             }
456         });
457     }
458 
459     @VisibleForTesting
resetForTest()460     public static void resetForTest() {
461         sAsyncTaskExecutor = null;
462     }
463 }
464