1/*
2 * Copyright (C) 2014 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 */
16package com.android.contacts.interactions;
17
18import android.content.AsyncTaskLoader;
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.pm.PackageManager;
22import android.database.Cursor;
23import android.database.DatabaseUtils;
24import android.net.Uri;
25import android.provider.CallLog.Calls;
26import android.telephony.PhoneNumberUtils;
27import android.text.TextUtils;
28
29import com.google.common.annotations.VisibleForTesting;
30
31import com.android.contacts.common.util.PermissionsUtil;
32
33import java.util.ArrayList;
34import java.util.Collections;
35import java.util.Comparator;
36import java.util.List;
37
38public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> {
39
40    private final String[] mPhoneNumbers;
41    private final int mMaxToRetrieve;
42    private List<ContactInteraction> mData;
43
44    public CallLogInteractionsLoader(Context context, String[] phoneNumbers,
45            int maxToRetrieve) {
46        super(context);
47        mPhoneNumbers = phoneNumbers;
48        mMaxToRetrieve = maxToRetrieve;
49    }
50
51    @Override
52    public List<ContactInteraction> loadInBackground() {
53        if (!PermissionsUtil.hasPhonePermissions(getContext())
54                || !getContext().getPackageManager()
55                        .hasSystemFeature(PackageManager.FEATURE_TELEPHONY)
56                || mPhoneNumbers == null || mPhoneNumbers.length <= 0 || mMaxToRetrieve <= 0) {
57            return Collections.emptyList();
58        }
59
60        final List<ContactInteraction> interactions = new ArrayList<>();
61        for (String number : mPhoneNumbers) {
62            interactions.addAll(getCallLogInteractions(number));
63        }
64        // Sort the call log interactions by date for duplicate removal
65        Collections.sort(interactions, new Comparator<ContactInteraction>() {
66            @Override
67            public int compare(ContactInteraction i1, ContactInteraction i2) {
68                if (i2.getInteractionDate() - i1.getInteractionDate() > 0) {
69                    return 1;
70                } else if (i2.getInteractionDate() == i1.getInteractionDate()) {
71                    return 0;
72                } else {
73                    return -1;
74                }
75            }
76        });
77        // Duplicates only occur because of fuzzy matching. No need to dedupe a single number.
78        if (mPhoneNumbers.length == 1) {
79            return interactions;
80        }
81        return pruneDuplicateCallLogInteractions(interactions, mMaxToRetrieve);
82    }
83
84    /**
85     * Two different phone numbers can match the same call log entry (since phone number
86     * matching is inexact). Therefore, we need to remove duplicates. In a reasonable call log,
87     * every entry should have a distinct date. Therefore, we can assume duplicate entries are
88     * adjacent entries.
89     * @param interactions The interaction list potentially containing duplicates
90     * @return The list with duplicates removed
91     */
92    @VisibleForTesting
93    static List<ContactInteraction> pruneDuplicateCallLogInteractions(
94            List<ContactInteraction> interactions, int maxToRetrieve) {
95        final List<ContactInteraction> subsetInteractions = new ArrayList<>();
96        for (int i = 0; i < interactions.size(); i++) {
97            if (i >= 1 && interactions.get(i).getInteractionDate() ==
98                    interactions.get(i-1).getInteractionDate()) {
99                continue;
100            }
101            subsetInteractions.add(interactions.get(i));
102            if (subsetInteractions.size() >= maxToRetrieve) {
103                break;
104            }
105        }
106        return subsetInteractions;
107    }
108
109    private List<ContactInteraction> getCallLogInteractions(String phoneNumber) {
110        // TODO: the phone number added to the ContactInteractions result should retain their
111        // original formatting since TalkBack is not reading the normalized number correctly
112        final String normalizedNumber = PhoneNumberUtils.normalizeNumber(phoneNumber);
113        // If the number contains only symbols, we can skip it
114        if (TextUtils.isEmpty(normalizedNumber)) {
115            return Collections.emptyList();
116        }
117        final Uri uri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI,
118                Uri.encode(normalizedNumber));
119        // Append the LIMIT clause onto the ORDER BY clause. This won't cause crashes as long
120        // as we don't also set the {@link android.provider.CallLog.Calls.LIMIT_PARAM_KEY} that
121        // becomes available in KK.
122        final String orderByAndLimit = Calls.DATE + " DESC LIMIT " + mMaxToRetrieve;
123        final Cursor cursor = getContext().getContentResolver().query(uri, null, null, null,
124                orderByAndLimit);
125        try {
126            if (cursor == null || cursor.getCount() < 1) {
127                return Collections.emptyList();
128            }
129            cursor.moveToPosition(-1);
130            List<ContactInteraction> interactions = new ArrayList<>();
131            while (cursor.moveToNext()) {
132                final ContentValues values = new ContentValues();
133                DatabaseUtils.cursorRowToContentValues(cursor, values);
134                interactions.add(new CallLogInteraction(values));
135            }
136            return interactions;
137        } finally {
138            if (cursor != null) {
139                cursor.close();
140            }
141        }
142    }
143
144    @Override
145    protected void onStartLoading() {
146        super.onStartLoading();
147
148        if (mData != null) {
149            deliverResult(mData);
150        }
151
152        if (takeContentChanged() || mData == null) {
153            forceLoad();
154        }
155    }
156
157    @Override
158    protected void onStopLoading() {
159        // Attempt to cancel the current load task if possible.
160        cancelLoad();
161    }
162
163    @Override
164    public void deliverResult(List<ContactInteraction> data) {
165        mData = data;
166        if (isStarted()) {
167            super.deliverResult(data);
168        }
169    }
170
171    @Override
172    protected void onReset() {
173        super.onReset();
174
175        // Ensure the loader is stopped
176        onStopLoading();
177        if (mData != null) {
178            mData.clear();
179        }
180    }
181}