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 */ 44 void onCallerInfoQueryComplete(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 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 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 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", mLock) { 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 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", mLock) { 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