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 17package com.android.server.telecom; 18 19import android.annotation.Nullable; 20import android.content.Context; 21import android.graphics.Bitmap; 22import android.graphics.drawable.Drawable; 23import android.net.Uri; 24import android.os.Handler; 25import android.os.Looper; 26import android.telecom.Log; 27import android.telecom.Logging.Runnable; 28import android.telecom.Logging.Session; 29import android.text.TextUtils; 30 31import com.android.internal.annotations.VisibleForTesting; 32import com.android.internal.telephony.CallerInfo; 33import com.android.internal.telephony.CallerInfoAsyncQuery; 34 35import java.io.InputStream; 36import java.util.HashMap; 37import java.util.LinkedList; 38import java.util.List; 39import java.util.Map; 40 41public 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 */ 48 void onCallerInfoQueryComplete(Uri handle, @Nullable 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 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 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 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 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