• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.server.telecom;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.graphics.Bitmap;
22 import android.graphics.drawable.Drawable;
23 import android.net.Uri;
24 import android.os.Handler;
25 import android.os.Looper;
26 import android.telecom.Log;
27 import android.telecom.Logging.Runnable;
28 import android.telecom.Logging.Session;
29 import android.text.TextUtils;
30 import android.util.Pair;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.internal.telephony.CallerInfo;
34 import com.android.internal.telephony.CallerInfoAsyncQuery;
35 
36 import java.io.InputStream;
37 import java.util.HashMap;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.concurrent.CompletableFuture;
42 
43 public class CallerInfoLookupHelper {
44     public interface OnQueryCompleteListener {
45         /**
46          * Called when the query returns with the caller info
47          * @param info
48          * @return true if the value should be cached, false otherwise.
49          */
onCallerInfoQueryComplete(Uri handle, @Nullable CallerInfo info)50         void onCallerInfoQueryComplete(Uri handle, @Nullable CallerInfo info);
onContactPhotoQueryComplete(Uri handle, CallerInfo info)51         void onContactPhotoQueryComplete(Uri handle, CallerInfo info);
52     }
53 
54     private static class CallerInfoQueryInfo {
55         public CallerInfo callerInfo;
56         public List<OnQueryCompleteListener> listeners;
57         public boolean imageQueryPending = false;
58 
CallerInfoQueryInfo()59         public CallerInfoQueryInfo() {
60             listeners = new LinkedList<>();
61         }
62     }
63 
64     private final Map<Uri, CallerInfoQueryInfo> mQueryEntries = new HashMap<>();
65 
66     private final CallerInfoAsyncQueryFactory mCallerInfoAsyncQueryFactory;
67     private final ContactsAsyncHelper mContactsAsyncHelper;
68     private final Context mContext;
69     private final TelecomSystem.SyncRoot mLock;
70     private final Handler mHandler = new Handler(Looper.getMainLooper());
71 
CallerInfoLookupHelper(Context context, CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory, ContactsAsyncHelper contactsAsyncHelper, TelecomSystem.SyncRoot lock)72     public CallerInfoLookupHelper(Context context,
73             CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
74             ContactsAsyncHelper contactsAsyncHelper,
75             TelecomSystem.SyncRoot lock) {
76         mCallerInfoAsyncQueryFactory = callerInfoAsyncQueryFactory;
77         mContactsAsyncHelper = contactsAsyncHelper;
78         mContext = context;
79         mLock = lock;
80     }
81 
82     /**
83      * Generates a CompletableFuture which performs a contacts lookup asynchronously.  The future
84      * returns a {@link Pair} containing the original handle which is being looked up and any
85      * {@link CallerInfo} which was found.
86      * @param handle
87      * @return {@link CompletableFuture} to perform the contacts lookup.
88      */
startLookup(final Uri handle)89     public CompletableFuture<Pair<Uri, CallerInfo>> startLookup(final Uri handle) {
90         // Create the returned future and
91         final CompletableFuture<Pair<Uri, CallerInfo>> callerInfoFuture = new CompletableFuture<>();
92 
93         final String number = handle.getSchemeSpecificPart();
94         if (TextUtils.isEmpty(number)) {
95             // Nothing to do here, just finish.
96             Log.d(CallerInfoLookupHelper.this, "onCallerInfoQueryComplete - no number; end early");
97             callerInfoFuture.complete(new Pair<>(handle, null));
98             return callerInfoFuture;
99         }
100 
101         // Setup a query complete listener which will get the results of the contacts lookup.
102         OnQueryCompleteListener listener = new OnQueryCompleteListener() {
103             @Override
104             public void onCallerInfoQueryComplete(Uri handle, CallerInfo info) {
105                 Log.d(CallerInfoLookupHelper.this, "onCallerInfoQueryComplete - found info for %s",
106                         Log.piiHandle(handle));
107                 // Got something, so complete the future.
108                 callerInfoFuture.complete(new Pair<>(handle, info));
109             }
110 
111             @Override
112             public void onContactPhotoQueryComplete(Uri handle, CallerInfo info) {
113                 // No-op for now; not something this future cares about.
114             }
115         };
116 
117         // Start async lookup.
118         startLookup(handle, listener);
119 
120         return callerInfoFuture;
121     }
122 
startLookup(final Uri handle, OnQueryCompleteListener listener)123     public void startLookup(final Uri handle, OnQueryCompleteListener listener) {
124         if (handle == null) {
125             listener.onCallerInfoQueryComplete(handle, null);
126             return;
127         }
128 
129         final String number = handle.getSchemeSpecificPart();
130         if (TextUtils.isEmpty(number)) {
131             listener.onCallerInfoQueryComplete(handle, null);
132             return;
133         }
134 
135         synchronized (mLock) {
136             if (mQueryEntries.containsKey(handle)) {
137                 CallerInfoQueryInfo info = mQueryEntries.get(handle);
138                 if (info.callerInfo != null) {
139                     Log.i(this, "Caller info already exists for handle %s; using cached value",
140                             Log.piiHandle(handle));
141                     listener.onCallerInfoQueryComplete(handle, info.callerInfo);
142                     if (!info.imageQueryPending && (info.callerInfo.cachedPhoto != null ||
143                             info.callerInfo.cachedPhotoIcon != null)) {
144                         listener.onContactPhotoQueryComplete(handle, info.callerInfo);
145                     } else if (info.imageQueryPending) {
146                         Log.i(this, "There is a pending photo query for handle %s. " +
147                                 "Adding to listeners for this query.", Log.piiHandle(handle));
148                         info.listeners.add(listener);
149                     }
150                 } else {
151                     Log.i(this, "There is a previously incomplete query for handle %s. Adding to " +
152                             "listeners for this query.", Log.piiHandle(handle));
153                     info.listeners.add(listener);
154                 }
155                 // Since we have a pending query for this handle already, don't re-query it.
156                 return;
157             } else {
158                 CallerInfoQueryInfo info = new CallerInfoQueryInfo();
159                 info.listeners.add(listener);
160                 mQueryEntries.put(handle, info);
161             }
162         }
163 
164         mHandler.post(new Runnable("CILH.sL", null) {
165             @Override
166             public void loggedRun() {
167                 Session continuedSession = Log.createSubsession();
168                 try {
169                     CallerInfoAsyncQuery query = mCallerInfoAsyncQueryFactory.startQuery(
170                             0, mContext, number,
171                             makeCallerInfoQueryListener(handle), continuedSession);
172                     if (query == null) {
173                         Log.w(this, "Lookup failed for %s.", Log.piiHandle(handle));
174                         Log.cancelSubsession(continuedSession);
175                     }
176                 } catch (Throwable t) {
177                     Log.cancelSubsession(continuedSession);
178                     throw t;
179                 }
180             }
181         }.prepare());
182     }
183 
makeCallerInfoQueryListener( final Uri handle)184     private CallerInfoAsyncQuery.OnQueryCompleteListener makeCallerInfoQueryListener(
185             final Uri handle) {
186         return (token, cookie, ci) -> {
187             synchronized (mLock) {
188                 Log.continueSession((Session) cookie, "CILH.oQC");
189                 try {
190                     if (mQueryEntries.containsKey(handle)) {
191                         Log.i(CallerInfoLookupHelper.this, "CI query for handle %s has completed;" +
192                                 " notifying all listeners.", Log.piiHandle(handle));
193                         CallerInfoQueryInfo info = mQueryEntries.get(handle);
194                         for (OnQueryCompleteListener l : info.listeners) {
195                             l.onCallerInfoQueryComplete(handle, ci);
196                         }
197                         if (ci.contactDisplayPhotoUri == null) {
198                             Log.i(CallerInfoLookupHelper.this, "There is no photo for this " +
199                                     "contact, skipping photo query");
200                             mQueryEntries.remove(handle);
201                         } else {
202                             info.callerInfo = ci;
203                             info.imageQueryPending = true;
204                             startPhotoLookup(handle, ci.contactDisplayPhotoUri);
205                         }
206                     } else {
207                         Log.i(CallerInfoLookupHelper.this, "CI query for handle %s has completed," +
208                                 " but there are no listeners left.", Log.piiHandle(handle));
209                     }
210                 } finally {
211                     Log.endSession();
212                 }
213             }
214         };
215     }
216 
217     private void startPhotoLookup(final Uri handle, final Uri contactPhotoUri) {
218         mHandler.post(new Runnable("CILH.sPL", null) {
219             @Override
220             public void loggedRun() {
221                 Session continuedSession = Log.createSubsession();
222                 try {
223                     mContactsAsyncHelper.startObtainPhotoAsync(
224                             0, mContext, contactPhotoUri,
225                             makeContactPhotoListener(handle), continuedSession);
226                 } catch (Throwable t) {
227                     Log.cancelSubsession(continuedSession);
228                     throw t;
229                 }
230             }
231         }.prepare());
232     }
233 
234     private ContactsAsyncHelper.OnImageLoadCompleteListener makeContactPhotoListener(
235             final Uri handle) {
236         return (token, photo, photoIcon, cookie) -> {
237             synchronized (mLock) {
238                 Log.continueSession((Session) cookie, "CLIH.oILC");
239                 try {
240                     if (mQueryEntries.containsKey(handle)) {
241                         CallerInfoQueryInfo info = mQueryEntries.get(handle);
242                         if (info.callerInfo == null) {
243                             Log.w(CallerInfoLookupHelper.this, "Photo query finished, but the " +
244                                     "CallerInfo object previously looked up was not cached.");
245                             mQueryEntries.remove(handle);
246                             return;
247                         }
248                         info.callerInfo.cachedPhoto = photo;
249                         info.callerInfo.cachedPhotoIcon = photoIcon;
250                         for (OnQueryCompleteListener l : info.listeners) {
251                             l.onContactPhotoQueryComplete(handle, info.callerInfo);
252                         }
253                         mQueryEntries.remove(handle);
254                     } else {
255                         Log.i(CallerInfoLookupHelper.this, "Photo query for handle %s has" +
256                                 " completed, but there are no listeners left.",
257                                 Log.piiHandle(handle));
258                     }
259                 } finally {
260                     Log.endSession();
261                 }
262             }
263         };
264     }
265 
266     @VisibleForTesting
267     public Map<Uri, CallerInfoQueryInfo> getCallerInfoEntries() {
268         return mQueryEntries;
269     }
270 
271     @VisibleForTesting
272     public Handler getHandler() {
273         return mHandler;
274     }
275 }
276