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