1 /* 2 * Copyright (C) 2011 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 17 package com.android.common.contacts; 18 19 import android.content.ContentResolver; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.os.Build; 25 import android.provider.ContactsContract; 26 import android.provider.ContactsContract.CommonDataKinds.Email; 27 import android.provider.ContactsContract.CommonDataKinds.Phone; 28 import android.provider.ContactsContract.Data; 29 import android.text.TextUtils; 30 import android.text.util.Rfc822Token; 31 import android.text.util.Rfc822Tokenizer; 32 import android.util.Log; 33 import android.widget.TextView; 34 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Collection; 38 import java.util.HashSet; 39 import java.util.Set; 40 41 /** 42 * Convenient class for updating usage statistics in ContactsProvider. 43 * 44 * Applications like Email, Sms, etc. can promote recipients for better sorting with this class 45 * 46 * @see ContactsContract.Contacts 47 */ 48 public class DataUsageStatUpdater { 49 private static final String TAG = DataUsageStatUpdater.class.getSimpleName(); 50 51 /** 52 * Copied from API in ICS (not available before it). You can use values here if you are sure 53 * it is supported by the device. 54 */ 55 public static final class DataUsageFeedback { 56 static final Uri FEEDBACK_URI = 57 Uri.withAppendedPath(Data.CONTENT_URI, "usagefeedback"); 58 59 static final String USAGE_TYPE = "type"; 60 public static final String USAGE_TYPE_CALL = "call"; 61 public static final String USAGE_TYPE_LONG_TEXT = "long_text"; 62 public static final String USAGE_TYPE_SHORT_TEXT = "short_text"; 63 } 64 65 private final ContentResolver mResolver; 66 67 public DataUsageStatUpdater(Context context) { 68 mResolver = context.getContentResolver(); 69 } 70 71 /** 72 * Updates usage statistics using comma-separated RFC822 address like 73 * "Joe <joe (at) example.com>, Due <due (at) example.com>". 74 * 75 * This will cause Disk access so should be called in a background thread. 76 * 77 * @return true when update request is correctly sent. False when the request fails, 78 * input has no valid entities. 79 */ 80 public boolean updateWithRfc822Address(Collection<CharSequence> texts){ 81 if (texts == null) { 82 return false; 83 } else { 84 final Set<String> addresses = new HashSet<String>(); 85 for (CharSequence text : texts) { 86 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(text.toString().trim()); 87 for (Rfc822Token token : tokens) { 88 addresses.add(token.getAddress()); 89 } 90 } 91 return updateWithAddress(addresses); 92 } 93 } 94 95 /** 96 * Update usage statistics information using a list of email addresses. 97 * 98 * This will cause Disk access so should be called in a background thread. 99 * 100 * @see #update(Collection, Collection, String) 101 * 102 * @return true when update request is correctly sent. False when the request fails, 103 * input has no valid entities. 104 */ 105 public boolean updateWithAddress(Collection<String> addresses) { 106 if (Log.isLoggable(TAG, Log.DEBUG)) { 107 Log.d(TAG, "updateWithAddress: " + Arrays.toString(addresses.toArray())); 108 } 109 if (addresses != null && !addresses.isEmpty()) { 110 final ArrayList<String> whereArgs = new ArrayList<String>(); 111 final StringBuilder whereBuilder = new StringBuilder(); 112 final String[] questionMarks = new String[addresses.size()]; 113 114 whereArgs.addAll(addresses); 115 Arrays.fill(questionMarks, "?"); 116 // Email.ADDRESS == Email.DATA1. Email.ADDRESS can be available from API Level 11. 117 whereBuilder.append(Email.DATA1 + " IN (") 118 .append(TextUtils.join(",", questionMarks)) 119 .append(")"); 120 final Cursor cursor = mResolver.query(Email.CONTENT_URI, 121 new String[] {Email.CONTACT_ID, Email._ID}, whereBuilder.toString(), 122 whereArgs.toArray(new String[0]), null); 123 124 if (cursor == null) { 125 Log.w(TAG, "Cursor for Email.CONTENT_URI became null."); 126 } else { 127 final Set<Long> contactIds = new HashSet<Long>(cursor.getCount()); 128 final Set<Long> dataIds = new HashSet<Long>(cursor.getCount()); 129 try { 130 cursor.move(-1); 131 while(cursor.moveToNext()) { 132 contactIds.add(cursor.getLong(0)); 133 dataIds.add(cursor.getLong(1)); 134 } 135 } finally { 136 cursor.close(); 137 } 138 return update(contactIds, dataIds, DataUsageFeedback.USAGE_TYPE_LONG_TEXT); 139 } 140 } 141 142 return false; 143 } 144 145 /** 146 * Update usage statistics information using a list of phone numbers. 147 * 148 * This will cause Disk access so should be called in a background thread. 149 * 150 * @see #update(Collection, Collection, String) 151 * 152 * @return true when update request is correctly sent. False when the request fails, 153 * input has no valid entities. 154 */ 155 public boolean updateWithPhoneNumber(Collection<String> numbers) { 156 if (Log.isLoggable(TAG, Log.DEBUG)) { 157 Log.d(TAG, "updateWithPhoneNumber: " + Arrays.toString(numbers.toArray())); 158 } 159 if (numbers != null && !numbers.isEmpty()) { 160 final ArrayList<String> whereArgs = new ArrayList<String>(); 161 final StringBuilder whereBuilder = new StringBuilder(); 162 final String[] questionMarks = new String[numbers.size()]; 163 164 whereArgs.addAll(numbers); 165 Arrays.fill(questionMarks, "?"); 166 // Phone.NUMBER == Phone.DATA1. NUMBER can be available from API Level 11. 167 whereBuilder.append(Phone.DATA1 + " IN (") 168 .append(TextUtils.join(",", questionMarks)) 169 .append(")"); 170 final Cursor cursor = mResolver.query(Phone.CONTENT_URI, 171 new String[] {Phone.CONTACT_ID, Phone._ID}, whereBuilder.toString(), 172 whereArgs.toArray(new String[0]), null); 173 174 if (cursor == null) { 175 Log.w(TAG, "Cursor for Phone.CONTENT_URI became null."); 176 } else { 177 final Set<Long> contactIds = new HashSet<Long>(cursor.getCount()); 178 final Set<Long> dataIds = new HashSet<Long>(cursor.getCount()); 179 try { 180 cursor.move(-1); 181 while(cursor.moveToNext()) { 182 contactIds.add(cursor.getLong(0)); 183 dataIds.add(cursor.getLong(1)); 184 } 185 } finally { 186 cursor.close(); 187 } 188 return update(contactIds, dataIds, DataUsageFeedback.USAGE_TYPE_SHORT_TEXT); 189 } 190 } 191 return false; 192 } 193 194 /** 195 * @return true when one or more of update requests are correctly sent. 196 * False when all the requests fail. 197 */ 198 private boolean update(Collection<Long> contactIds, Collection<Long> dataIds, String type) { 199 final long currentTimeMillis = System.currentTimeMillis(); 200 201 boolean successful = false; 202 203 // From ICS (SDK_INT 14) we can use per-contact-method structure. We'll check if the device 204 // supports it and call the API. 205 if (Build.VERSION.SDK_INT >= 14) { 206 if (dataIds.isEmpty()) { 207 if (Log.isLoggable(TAG, Log.DEBUG)) { 208 Log.d(TAG, "Given list for data IDs is null. Ignoring."); 209 } 210 } else { 211 final Uri uri = DataUsageFeedback.FEEDBACK_URI.buildUpon() 212 .appendPath(TextUtils.join(",", dataIds)) 213 .appendQueryParameter(DataUsageFeedback.USAGE_TYPE, type) 214 .build(); 215 if (mResolver.update(uri, new ContentValues(), null, null) > 0) { 216 successful = true; 217 } else { 218 if (Log.isLoggable(TAG, Log.DEBUG)) { 219 Log.d(TAG, "update toward data rows " + dataIds + " failed"); 220 } 221 } 222 } 223 } else { 224 // Use older API. 225 if (contactIds.isEmpty()) { 226 if (Log.isLoggable(TAG, Log.DEBUG)) { 227 Log.d(TAG, "Given list for contact IDs is null. Ignoring."); 228 } 229 } else { 230 final StringBuilder whereBuilder = new StringBuilder(); 231 final ArrayList<String> whereArgs = new ArrayList<String>(); 232 final String[] questionMarks = new String[contactIds.size()]; 233 for (long contactId : contactIds) { 234 whereArgs.add(String.valueOf(contactId)); 235 } 236 Arrays.fill(questionMarks, "?"); 237 whereBuilder.append(ContactsContract.Contacts._ID + " IN ("). 238 append(TextUtils.join(",", questionMarks)). 239 append(")"); 240 241 if (Log.isLoggable(TAG, Log.DEBUG)) { 242 Log.d(TAG, "contactId where: " + whereBuilder.toString()); 243 Log.d(TAG, "contactId selection: " + whereArgs); 244 } 245 246 final ContentValues values = new ContentValues(); 247 values.put(ContactsContract.Contacts.LAST_TIME_CONTACTED, currentTimeMillis); 248 if (mResolver.update(ContactsContract.Contacts.CONTENT_URI, values, 249 whereBuilder.toString(), whereArgs.toArray(new String[0])) > 0) { 250 successful = true; 251 } else { 252 if (Log.isLoggable(TAG, Log.DEBUG)) { 253 Log.d(TAG, "update toward raw contacts " + contactIds + " failed"); 254 } 255 } 256 } 257 } 258 259 return successful; 260 } 261 } 262