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 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.internal.telephony.CallerInfo;
33 import com.android.internal.telephony.CallerInfoAsyncQuery;
34 
35 import java.io.InputStream;
36 import java.util.HashMap;
37 import java.util.LinkedList;
38 import java.util.List;
39 import java.util.Map;
40 
41 public class CallerInfoLookupHelper {
42     public interface OnQueryCompleteListener {
43         /**
44          * Called when the query returns with the caller info
45          * @param info
46          * @return true if the value should be cached, false otherwise.
47          */
onCallerInfoQueryComplete(Uri handle, @Nullable CallerInfo info)48         void onCallerInfoQueryComplete(Uri handle, @Nullable CallerInfo info);
onContactPhotoQueryComplete(Uri handle, CallerInfo info)49         void onContactPhotoQueryComplete(Uri handle, CallerInfo info);
50     }
51 
52     private static class CallerInfoQueryInfo {
53         public CallerInfo callerInfo;
54         public List<OnQueryCompleteListener> listeners;
55         public boolean imageQueryPending = false;
56 
CallerInfoQueryInfo()57         public CallerInfoQueryInfo() {
58             listeners = new LinkedList<>();
59         }
60     }
61 
62     private final Map<Uri, CallerInfoQueryInfo> mQueryEntries = new HashMap<>();
63 
64     private final CallerInfoAsyncQueryFactory mCallerInfoAsyncQueryFactory;
65     private final ContactsAsyncHelper mContactsAsyncHelper;
66     private final Context mContext;
67     private final TelecomSystem.SyncRoot mLock;
68     private final Handler mHandler = new Handler(Looper.getMainLooper());
69 
CallerInfoLookupHelper(Context context, CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory, ContactsAsyncHelper contactsAsyncHelper, TelecomSystem.SyncRoot lock)70     public CallerInfoLookupHelper(Context context,
71             CallerInfoAsyncQueryFactory callerInfoAsyncQueryFactory,
72             ContactsAsyncHelper contactsAsyncHelper,
73             TelecomSystem.SyncRoot lock) {
74         mCallerInfoAsyncQueryFactory = callerInfoAsyncQueryFactory;
75         mContactsAsyncHelper = contactsAsyncHelper;
76         mContext = context;
77         mLock = lock;
78     }
79 
startLookup(final Uri handle, OnQueryCompleteListener listener)80     public void startLookup(final Uri handle, OnQueryCompleteListener listener) {
81         if (handle == null) {
82             listener.onCallerInfoQueryComplete(handle, null);
83             return;
84         }
85 
86         final String number = handle.getSchemeSpecificPart();
87         if (TextUtils.isEmpty(number)) {
88             listener.onCallerInfoQueryComplete(handle, null);
89             return;
90         }
91 
92         synchronized (mLock) {
93             if (mQueryEntries.containsKey(handle)) {
94                 CallerInfoQueryInfo info = mQueryEntries.get(handle);
95                 if (info.callerInfo != null) {
96                     Log.i(this, "Caller info already exists for handle %s; using cached value",
97                             Log.piiHandle(handle));
98                     listener.onCallerInfoQueryComplete(handle, info.callerInfo);
99                     if (!info.imageQueryPending && (info.callerInfo.cachedPhoto != null ||
100                             info.callerInfo.cachedPhotoIcon != null)) {
101                         listener.onContactPhotoQueryComplete(handle, info.callerInfo);
102                     } else if (info.imageQueryPending) {
103                         Log.i(this, "There is a pending photo query for handle %s. " +
104                                 "Adding to listeners for this query.", Log.piiHandle(handle));
105                         info.listeners.add(listener);
106                     }
107                 } else {
108                     Log.i(this, "There is a previously incomplete query for handle %s. Adding to " +
109                             "listeners for this query.", Log.piiHandle(handle));
110                     info.listeners.add(listener);
111                     return;
112                 }
113             } else {
114                 CallerInfoQueryInfo info = new CallerInfoQueryInfo();
115                 info.listeners.add(listener);
116                 mQueryEntries.put(handle, info);
117             }
118         }
119 
120         mHandler.post(new Runnable("CILH.sL", mLock) {
121             @Override
122             public void loggedRun() {
123                 Session continuedSession = Log.createSubsession();
124                 try {
125                     CallerInfoAsyncQuery query = mCallerInfoAsyncQueryFactory.startQuery(
126                             0, mContext, number,
127                             makeCallerInfoQueryListener(handle), continuedSession);
128                     if (query == null) {
129                         Log.w(this, "Lookup failed for %s.", Log.piiHandle(handle));
130                         Log.cancelSubsession(continuedSession);
131                     }
132                 } catch (Throwable t) {
133                     Log.cancelSubsession(continuedSession);
134                     throw t;
135                 }
136             }
137         }.prepare());
138     }
139 
makeCallerInfoQueryListener( final Uri handle)140     private CallerInfoAsyncQuery.OnQueryCompleteListener makeCallerInfoQueryListener(
141             final Uri handle) {
142         return (token, cookie, ci) -> {
143             synchronized (mLock) {
144                 Log.continueSession((Session) cookie, "CILH.oQC");
145                 try {
146                     if (mQueryEntries.containsKey(handle)) {
147                         Log.i(CallerInfoLookupHelper.this, "CI query for handle %s has completed;" +
148                                 " notifying all listeners.", Log.piiHandle(handle));
149                         CallerInfoQueryInfo info = mQueryEntries.get(handle);
150                         for (OnQueryCompleteListener l : info.listeners) {
151                             l.onCallerInfoQueryComplete(handle, ci);
152                         }
153                         if (ci.contactDisplayPhotoUri == null) {
154                             Log.i(CallerInfoLookupHelper.this, "There is no photo for this " +
155                                     "contact, skipping photo query");
156                             mQueryEntries.remove(handle);
157                         } else {
158                             info.callerInfo = ci;
159                             info.imageQueryPending = true;
160                             startPhotoLookup(handle, ci.contactDisplayPhotoUri);
161                         }
162                     } else {
163                         Log.i(CallerInfoLookupHelper.this, "CI query for handle %s has completed," +
164                                 " but there are no listeners left.", Log.piiHandle(handle));
165                     }
166                 } finally {
167                     Log.endSession();
168                 }
169             }
170         };
171     }
172 
173     private void startPhotoLookup(final Uri handle, final Uri contactPhotoUri) {
174         mHandler.post(new Runnable("CILH.sPL", mLock) {
175             @Override
176             public void loggedRun() {
177                 Session continuedSession = Log.createSubsession();
178                 try {
179                     mContactsAsyncHelper.startObtainPhotoAsync(
180                             0, mContext, contactPhotoUri,
181                             makeContactPhotoListener(handle), continuedSession);
182                 } catch (Throwable t) {
183                     Log.cancelSubsession(continuedSession);
184                     throw t;
185                 }
186             }
187         }.prepare());
188     }
189 
190     private ContactsAsyncHelper.OnImageLoadCompleteListener makeContactPhotoListener(
191             final Uri handle) {
192         return (token, photo, photoIcon, cookie) -> {
193             synchronized (mLock) {
194                 Log.continueSession((Session) cookie, "CLIH.oILC");
195                 try {
196                     if (mQueryEntries.containsKey(handle)) {
197                         CallerInfoQueryInfo info = mQueryEntries.get(handle);
198                         if (info.callerInfo == null) {
199                             Log.w(CallerInfoLookupHelper.this, "Photo query finished, but the " +
200                                     "CallerInfo object previously looked up was not cached.");
201                             mQueryEntries.remove(handle);
202                             return;
203                         }
204                         info.callerInfo.cachedPhoto = photo;
205                         info.callerInfo.cachedPhotoIcon = photoIcon;
206                         for (OnQueryCompleteListener l : info.listeners) {
207                             l.onContactPhotoQueryComplete(handle, info.callerInfo);
208                         }
209                         mQueryEntries.remove(handle);
210                     } else {
211                         Log.i(CallerInfoLookupHelper.this, "Photo query for handle %s has" +
212                                 " completed, but there are no listeners left.",
213                                 Log.piiHandle(handle));
214                     }
215                 } finally {
216                     Log.endSession();
217                 }
218             }
219         };
220     }
221 
222     @VisibleForTesting
223     public Map<Uri, CallerInfoQueryInfo> getCallerInfoEntries() {
224         return mQueryEntries;
225     }
226 
227     @VisibleForTesting
228     public Handler getHandler() {
229         return mHandler;
230     }
231 }
232