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