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 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 // TODO: the phone number added to the ContactInteractions result should retain their 107 // original formatting since TalkBack is not reading the normalized number correctly 108 final String normalizedNumber = PhoneNumberUtils.normalizeNumber(phoneNumber); 109 // If the number contains only symbols, we can skip it 110 if (TextUtils.isEmpty(normalizedNumber)) { 111 return Collections.emptyList(); 112 } 113 final Uri uri = Uri.withAppendedPath(Calls.CONTENT_FILTER_URI, 114 Uri.encode(normalizedNumber)); 115 // Append the LIMIT clause onto the ORDER BY clause. This won't cause crashes as long 116 // as we don't also set the {@link android.provider.CallLog.Calls.LIMIT_PARAM_KEY} that 117 // becomes available in KK. 118 final String orderByAndLimit = Calls.DATE + " DESC LIMIT " + mMaxToRetrieve; 119 final Cursor cursor = getContext().getContentResolver().query(uri, null, null, null, 120 orderByAndLimit); 121 try { 122 if (cursor == null || cursor.getCount() < 1) { 123 return Collections.emptyList(); 124 } 125 cursor.moveToPosition(-1); 126 List<ContactInteraction> interactions = new ArrayList<>(); 127 while (cursor.moveToNext()) { 128 final ContentValues values = new ContentValues(); 129 DatabaseUtils.cursorRowToContentValues(cursor, values); 130 interactions.add(new CallLogInteraction(values)); 131 } 132 return interactions; 133 } finally { 134 if (cursor != null) { 135 cursor.close(); 136 } 137 } 138 } 139 140 @Override 141 protected void onStartLoading() { 142 super.onStartLoading(); 143 144 if (mData != null) { 145 deliverResult(mData); 146 } 147 148 if (takeContentChanged() || mData == null) { 149 forceLoad(); 150 } 151 } 152 153 @Override 154 protected void onStopLoading() { 155 // Attempt to cancel the current load task if possible. 156 cancelLoad(); 157 } 158 159 @Override 160 public void deliverResult(List<ContactInteraction> data) { 161 mData = data; 162 if (isStarted()) { 163 super.deliverResult(data); 164 } 165 } 166 167 @Override 168 protected void onReset() { 169 super.onReset(); 170 171 // Ensure the loader is stopped 172 onStopLoading(); 173 if (mData != null) { 174 mData.clear(); 175 } 176 } 177 }