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