1/*
2 * Copyright (C) 2013 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.dialer.calllog;
18
19import com.google.common.base.Strings;
20
21import android.Manifest;
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.Context;
25import android.database.Cursor;
26import android.net.Uri;
27import android.provider.CallLog.Calls;
28import android.provider.ContactsContract.PhoneLookup;
29import android.support.annotation.NonNull;
30import android.support.annotation.Nullable;
31import android.telephony.PhoneNumberUtils;
32import android.text.TextUtils;
33import android.util.Log;
34
35import com.android.contacts.common.GeoUtil;
36import com.android.contacts.common.util.PermissionsUtil;
37import com.android.dialer.R;
38import com.android.dialer.util.TelecomUtil;
39
40import java.util.ArrayList;
41import java.util.List;
42
43/**
44 * Helper class operating on call log notifications.
45 */
46public class CallLogNotificationsHelper {
47    private static final String TAG = "CallLogNotifHelper";
48    private static CallLogNotificationsHelper sInstance;
49
50    /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
51    public static CallLogNotificationsHelper getInstance(Context context) {
52        if (sInstance == null) {
53            ContentResolver contentResolver = context.getContentResolver();
54            String countryIso = GeoUtil.getCurrentCountryIso(context);
55            sInstance = new CallLogNotificationsHelper(context,
56                    createNewCallsQuery(context, contentResolver),
57                    createNameLookupQuery(context, contentResolver),
58                    new ContactInfoHelper(context, countryIso),
59                    countryIso);
60        }
61        return sInstance;
62    }
63
64    private final Context mContext;
65    private final NewCallsQuery mNewCallsQuery;
66    private final NameLookupQuery mNameLookupQuery;
67    private final ContactInfoHelper mContactInfoHelper;
68    private final String mCurrentCountryIso;
69
70    CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery,
71            NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper,
72            String countryIso) {
73        mContext = context;
74        mNewCallsQuery = newCallsQuery;
75        mNameLookupQuery = nameLookupQuery;
76        mContactInfoHelper = contactInfoHelper;
77        mCurrentCountryIso = countryIso;
78    }
79
80    /**
81     * Get all voicemails with the "new" flag set to 1.
82     *
83     * @return A list of NewCall objects where each object represents a new voicemail.
84     */
85    @Nullable
86    public List<NewCall> getNewVoicemails() {
87        return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE);
88    }
89
90    /**
91     * Get all missed calls with the "new" flag set to 1.
92     *
93     * @return A list of NewCall objects where each object represents a new missed call.
94     */
95    @Nullable
96    public List<NewCall> getNewMissedCalls() {
97        return mNewCallsQuery.query(Calls.MISSED_TYPE);
98    }
99
100    /**
101     * Given a number and number information (presentation and country ISO), get the best name
102     * for display. If the name is empty but we have a special presentation, display that.
103     * Otherwise attempt to look it up in the database or the cache.
104     * If that fails, fall back to displaying the number.
105     */
106    public String getName(@Nullable String number, int numberPresentation,
107                          @Nullable String countryIso) {
108        return getContactInfo(number, numberPresentation, countryIso).name;
109    }
110
111    /**
112     * Given a number and number information (presentation and country ISO), get
113     * {@link ContactInfo}. If the name is empty but we have a special presentation, display that.
114     * Otherwise attempt to look it up in the cache.
115     * If that fails, fall back to displaying the number.
116     */
117    public ContactInfo getContactInfo(@Nullable String number, int numberPresentation,
118                          @Nullable String countryIso) {
119        if (countryIso == null) {
120            countryIso = mCurrentCountryIso;
121        }
122
123        number = Strings.nullToEmpty(number);
124        ContactInfo contactInfo = new ContactInfo();
125        contactInfo.number = number;
126        contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso);
127        // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo.
128        contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
129
130        // 1. Special number representation.
131        contactInfo.name = PhoneNumberDisplayUtil.getDisplayName(
132                mContext,
133                number,
134                numberPresentation,
135                false).toString();
136        if (!TextUtils.isEmpty(contactInfo.name)) {
137            return contactInfo;
138        }
139
140        // 2. Look it up in the cache.
141        ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso);
142
143        if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) {
144            return cachedContactInfo;
145        }
146
147        if (!TextUtils.isEmpty(contactInfo.formattedNumber)) {
148            // 3. If we cannot lookup the contact, use the formatted number instead.
149            contactInfo.name = contactInfo.formattedNumber;
150        } else if (!TextUtils.isEmpty(number)) {
151            // 4. If number can't be formatted, use number.
152            contactInfo.name = number;
153        } else {
154            // 5. Otherwise, it's unknown number.
155            contactInfo.name = mContext.getResources().getString(R.string.unknown);
156        }
157        return contactInfo;
158    }
159
160    /** Removes the missed call notifications. */
161    public static void removeMissedCallNotifications(Context context) {
162        TelecomUtil.cancelMissedCallsNotification(context);
163    }
164
165    /** Update the voice mail notifications. */
166    public static void updateVoicemailNotifications(Context context) {
167        CallLogNotificationsService.updateVoicemailNotifications(context, null);
168    }
169
170    /** Information about a new voicemail. */
171    public static final class NewCall {
172        public final Uri callsUri;
173        public final Uri voicemailUri;
174        public final String number;
175        public final int numberPresentation;
176        public final String accountComponentName;
177        public final String accountId;
178        public final String transcription;
179        public final String countryIso;
180        public final long dateMs;
181
182        public NewCall(
183                Uri callsUri,
184                Uri voicemailUri,
185                String number,
186                int numberPresentation,
187                String accountComponentName,
188                String accountId,
189                String transcription,
190                String countryIso,
191                long dateMs) {
192            this.callsUri = callsUri;
193            this.voicemailUri = voicemailUri;
194            this.number = number;
195            this.numberPresentation = numberPresentation;
196            this.accountComponentName = accountComponentName;
197            this.accountId = accountId;
198            this.transcription = transcription;
199            this.countryIso = countryIso;
200            this.dateMs = dateMs;
201        }
202    }
203
204    /** Allows determining the new calls for which a notification should be generated. */
205    public interface NewCallsQuery {
206        /**
207         * Returns the new calls of a certain type for which a notification should be generated.
208         */
209        @Nullable
210        public List<NewCall> query(int type);
211    }
212
213    /** Create a new instance of {@link NewCallsQuery}. */
214    public static NewCallsQuery createNewCallsQuery(Context context,
215            ContentResolver contentResolver) {
216
217        return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
218    }
219
220    /**
221     * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
222     * notify about in the call log.
223     */
224    private static final class DefaultNewCallsQuery implements NewCallsQuery {
225        private static final String[] PROJECTION = {
226            Calls._ID,
227            Calls.NUMBER,
228            Calls.VOICEMAIL_URI,
229            Calls.NUMBER_PRESENTATION,
230            Calls.PHONE_ACCOUNT_COMPONENT_NAME,
231            Calls.PHONE_ACCOUNT_ID,
232            Calls.TRANSCRIPTION,
233            Calls.COUNTRY_ISO,
234            Calls.DATE
235        };
236        private static final int ID_COLUMN_INDEX = 0;
237        private static final int NUMBER_COLUMN_INDEX = 1;
238        private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
239        private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
240        private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
241        private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
242        private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
243        private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
244        private static final int DATE_COLUMN_INDEX = 8;
245
246        private final ContentResolver mContentResolver;
247        private final Context mContext;
248
249        private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
250            mContext = context;
251            mContentResolver = contentResolver;
252        }
253
254        @Override
255        @Nullable
256        public List<NewCall> query(int type) {
257            if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) {
258                Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
259                return null;
260            }
261            final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
262            final String[] selectionArgs = new String[]{ Integer.toString(type) };
263            try (Cursor cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL,
264                    PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER)) {
265                if (cursor == null) {
266                    return null;
267                }
268                List<NewCall> newCalls = new ArrayList<>();
269                while (cursor.moveToNext()) {
270                    newCalls.add(createNewCallsFromCursor(cursor));
271                }
272                return newCalls;
273            } catch (RuntimeException e) {
274                Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
275                return null;
276            }
277        }
278
279        /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
280        private NewCall createNewCallsFromCursor(Cursor cursor) {
281            String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
282            Uri callsUri = ContentUris.withAppendedId(
283                    Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
284            Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
285            return new NewCall(
286                    callsUri,
287                    voicemailUri,
288                    cursor.getString(NUMBER_COLUMN_INDEX),
289                    cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
290                    cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
291                    cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
292                    cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
293                    cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
294                    cursor.getLong(DATE_COLUMN_INDEX));
295        }
296    }
297
298    /** Allows determining the name associated with a given phone number. */
299    public interface NameLookupQuery {
300        /**
301         * Returns the name associated with the given number in the contacts database, or null if
302         * the number does not correspond to any of the contacts.
303         * <p>
304         * If there are multiple contacts with the same phone number, it will return the name of one
305         * of the matching contacts.
306         */
307        @Nullable
308        public String query(@Nullable String number);
309    }
310
311    /** Create a new instance of {@link NameLookupQuery}. */
312    public static NameLookupQuery createNameLookupQuery(Context context,
313            ContentResolver contentResolver) {
314        return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
315    }
316
317    /**
318     * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
319     * contacts database.
320     */
321    private static final class DefaultNameLookupQuery implements NameLookupQuery {
322        private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
323        private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
324
325        private final ContentResolver mContentResolver;
326        private final Context mContext;
327
328        private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
329            mContext = context;
330            mContentResolver = contentResolver;
331        }
332
333        @Override
334        @Nullable
335        public String query(@Nullable String number) {
336            if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CONTACTS)) {
337                Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
338                return null;
339            }
340            try (Cursor cursor =  mContentResolver.query(
341                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
342                    PROJECTION, null, null, null)) {
343                if (cursor == null || !cursor.moveToFirst()) {
344                    return null;
345                }
346                return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
347            } catch (RuntimeException e) {
348                Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
349                return null;
350            }
351        }
352    }
353}
354