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