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