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 */ 16 package com.android.contacts.interactions; 17 18 import android.content.AsyncTaskLoader; 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.pm.PackageManager; 22 import android.database.Cursor; 23 import android.database.DatabaseUtils; 24 import android.net.Uri; 25 import android.provider.CallLog.Calls; 26 import android.telephony.PhoneNumberUtils; 27 import android.text.TextUtils; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.Comparator; 34 import java.util.List; 35 36 public class CallLogInteractionsLoader extends AsyncTaskLoader<List<ContactInteraction>> { 37 38 private final String[] mPhoneNumbers; 39 private final int mMaxToRetrieve; 40 private List<ContactInteraction> mData; 41 CallLogInteractionsLoader(Context context, String[] phoneNumbers, int maxToRetrieve)42 public CallLogInteractionsLoader(Context context, String[] phoneNumbers, 43 int maxToRetrieve) { 44 super(context); 45 mPhoneNumbers = phoneNumbers; 46 mMaxToRetrieve = maxToRetrieve; 47 } 48 49 @Override loadInBackground()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 pruneDuplicateCallLogInteractions( List<ContactInteraction> interactions, int maxToRetrieve)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 getCallLogInteractions(String phoneNumber)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 onStartLoading()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 onStopLoading()152 protected void onStopLoading() { 153 // Attempt to cancel the current load task if possible. 154 cancelLoad(); 155 } 156 157 @Override deliverResult(List<ContactInteraction> data)158 public void deliverResult(List<ContactInteraction> data) { 159 mData = data; 160 if (isStarted()) { 161 super.deliverResult(data); 162 } 163 } 164 165 @Override onReset()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 }