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