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
17package com.android.common.contacts;
18
19import android.content.ContentResolver;
20import android.content.ContentValues;
21import android.content.Context;
22import android.database.Cursor;
23import android.net.Uri;
24import android.os.Build;
25import android.provider.ContactsContract;
26import android.provider.ContactsContract.CommonDataKinds.Email;
27import android.provider.ContactsContract.CommonDataKinds.Phone;
28import android.provider.ContactsContract.Data;
29import android.text.TextUtils;
30import android.text.util.Rfc822Token;
31import android.text.util.Rfc822Tokenizer;
32import android.util.Log;
33import android.widget.TextView;
34
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.Collection;
38import java.util.HashSet;
39import 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 */
48public 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@example.com>, Due <due@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