1 /*
2  * Copyright (C) 2016 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.shortcuts;
18 
19 import android.Manifest;
20 import android.annotation.TargetApi;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ShortcutManager;
24 import android.database.Cursor;
25 import android.net.Uri;
26 import android.os.AsyncTask;
27 import android.os.Build;
28 import android.os.Build.VERSION_CODES;
29 import android.provider.ContactsContract.Contacts;
30 import android.provider.ContactsContract.PhoneLookup;
31 import android.support.annotation.MainThread;
32 import android.support.annotation.NonNull;
33 import android.support.annotation.Nullable;
34 import android.support.annotation.WorkerThread;
35 import android.support.v4.content.ContextCompat;
36 import android.text.TextUtils;
37 import com.android.dialer.common.Assert;
38 import com.android.dialer.common.LogUtil;
39 import com.android.dialer.common.concurrent.AsyncTaskExecutor;
40 import com.android.dialer.common.concurrent.AsyncTaskExecutors;
41 
42 /**
43  * Reports outgoing calls as shortcut usage.
44  *
45  * <p>Note that all outgoing calls are considered shortcut usage, no matter where they are initiated
46  * from (i.e. from anywhere in the dialer app, or even from other apps).
47  *
48  * <p>This allows launcher applications to provide users with shortcut suggestions, even if the user
49  * isn't already using shortcuts.
50  */
51 @TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N_MR1
52 public class ShortcutUsageReporter {
53 
54   private static final AsyncTaskExecutor EXECUTOR = AsyncTaskExecutors.createThreadPoolExecutor();
55 
56   /**
57    * Called when an outgoing call is added to the call list in order to report outgoing calls as
58    * shortcut usage. This should be called exactly once for each outgoing call.
59    *
60    * <p>Asynchronously queries the contacts database for the contact's lookup key which corresponds
61    * to the provided phone number, and uses that to report shortcut usage.
62    *
63    * @param context used to access ShortcutManager system service
64    * @param phoneNumber the phone number being called
65    */
66   @MainThread
onOutgoingCallAdded(@onNull Context context, @Nullable String phoneNumber)67   public static void onOutgoingCallAdded(@NonNull Context context, @Nullable String phoneNumber) {
68     Assert.isMainThread();
69     Assert.isNotNull(context);
70 
71     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || TextUtils.isEmpty(phoneNumber)) {
72       return;
73     }
74 
75     EXECUTOR.submit(Task.ID, new Task(context), phoneNumber);
76   }
77 
78   private static final class Task extends AsyncTask<String, Void, Void> {
79     private static final String ID = "ShortcutUsageReporter.Task";
80 
81     private final Context context;
82 
Task(Context context)83     public Task(Context context) {
84       this.context = context;
85     }
86 
87     /** @param phoneNumbers array with exactly one non-empty phone number */
88     @Override
89     @WorkerThread
doInBackground(@onNull String... phoneNumbers)90     protected Void doInBackground(@NonNull String... phoneNumbers) {
91       Assert.isWorkerThread();
92 
93       String lookupKey = queryForLookupKey(phoneNumbers[0]);
94       if (!TextUtils.isEmpty(lookupKey)) {
95         LogUtil.i("ShortcutUsageReporter.backgroundLogUsage", "%s", lookupKey);
96         ShortcutManager shortcutManager =
97             (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
98 
99         // Note: There may not currently exist a shortcut with the provided key, but it is logged
100         // anyway, so that launcher applications at least have the information should the shortcut
101         // be created in the future.
102         shortcutManager.reportShortcutUsed(lookupKey);
103       }
104       return null;
105     }
106 
107     @Nullable
108     @WorkerThread
queryForLookupKey(String phoneNumber)109     private String queryForLookupKey(String phoneNumber) {
110       Assert.isWorkerThread();
111 
112       if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
113           != PackageManager.PERMISSION_GRANTED) {
114         LogUtil.i("ShortcutUsageReporter.queryForLookupKey", "No contact permissions");
115         return null;
116       }
117 
118       Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
119       try (Cursor cursor =
120           context
121               .getContentResolver()
122               .query(uri, new String[] {Contacts.LOOKUP_KEY}, null, null, null)) {
123 
124         if (cursor == null || !cursor.moveToNext()) {
125           return null; // No contact for dialed number
126         }
127         // Arbitrarily use first result.
128         return cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
129       }
130     }
131   }
132 }
133