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