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.dialer.calllog;
18
19import android.content.AsyncQueryHandler;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.database.Cursor;
23import android.database.MatrixCursor;
24import android.database.MergeCursor;
25import android.database.sqlite.SQLiteDatabaseCorruptException;
26import android.database.sqlite.SQLiteDiskIOException;
27import android.database.sqlite.SQLiteException;
28import android.database.sqlite.SQLiteFullException;
29import android.net.Uri;
30import android.os.Handler;
31import android.os.Looper;
32import android.os.Message;
33import android.provider.CallLog.Calls;
34import android.provider.VoicemailContract.Status;
35import android.util.Log;
36
37import com.android.common.io.MoreCloseables;
38import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
39import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
40import com.google.common.collect.Lists;
41
42import java.lang.ref.WeakReference;
43import java.util.List;
44
45/** Handles asynchronous queries to the call log. */
46public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
47    private static final String[] EMPTY_STRING_ARRAY = new String[0];
48
49    private static final String TAG = "CallLogQueryHandler";
50    private static final int NUM_LOGS_TO_DISPLAY = 1000;
51
52    /** The token for the query to fetch the old entries from the call log. */
53    private static final int QUERY_CALLLOG_TOKEN = 54;
54    /** The token for the query to mark all missed calls as old after seeing the call log. */
55    private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
56    /** The token for the query to mark all new voicemails as old. */
57    private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 56;
58    /** The token for the query to mark all missed calls as read after seeing the call log. */
59    private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 57;
60    /** The token for the query to fetch voicemail status messages. */
61    private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
62
63    private final int mLogLimit;
64
65    /**
66     * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
67     * type.
68     */
69    public static final int CALL_TYPE_ALL = -1;
70
71    private final WeakReference<Listener> mListener;
72
73    /**
74     * Simple handler that wraps background calls to catch
75     * {@link SQLiteException}, such as when the disk is full.
76     */
77    protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
78        public CatchingWorkerHandler(Looper looper) {
79            super(looper);
80        }
81
82        @Override
83        public void handleMessage(Message msg) {
84            try {
85                // Perform same query while catching any exceptions
86                super.handleMessage(msg);
87            } catch (SQLiteDiskIOException e) {
88                Log.w(TAG, "Exception on background worker thread", e);
89            } catch (SQLiteFullException e) {
90                Log.w(TAG, "Exception on background worker thread", e);
91            } catch (SQLiteDatabaseCorruptException e) {
92                Log.w(TAG, "Exception on background worker thread", e);
93            } catch (IllegalArgumentException e) {
94                Log.w(TAG, "ContactsProvider not present on device", e);
95            }
96        }
97    }
98
99    @Override
100    protected Handler createHandler(Looper looper) {
101        // Provide our special handler that catches exceptions
102        return new CatchingWorkerHandler(looper);
103    }
104
105    public CallLogQueryHandler(ContentResolver contentResolver, Listener listener) {
106        this(contentResolver, listener, -1);
107    }
108
109    public CallLogQueryHandler(ContentResolver contentResolver, Listener listener, int limit) {
110        super(contentResolver);
111        mListener = new WeakReference<Listener>(listener);
112        mLogLimit = limit;
113    }
114
115    /**
116     * Fetches the list of calls from the call log for a given type.
117     * This call ignores the new or old state.
118     * <p>
119     * It will asynchronously update the content of the list view when the fetch completes.
120     */
121    public void fetchCalls(int callType, long newerThan) {
122        cancelFetch();
123        fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan);
124    }
125
126    public void fetchCalls(int callType) {
127        fetchCalls(callType, 0);
128    }
129
130    public void fetchVoicemailStatus() {
131        startQuery(QUERY_VOICEMAIL_STATUS_TOKEN, null, Status.CONTENT_URI,
132                VoicemailStatusHelperImpl.PROJECTION, null, null, null);
133    }
134
135    /** Fetches the list of calls in the call log. */
136    private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
137        // We need to check for NULL explicitly otherwise entries with where READ is NULL
138        // may not match either the query or its negation.
139        // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
140        StringBuilder where = new StringBuilder();
141        List<String> selectionArgs = Lists.newArrayList();
142
143        if (newOnly) {
144            where.append(Calls.NEW);
145            where.append(" = 1");
146        }
147
148        if (callType > CALL_TYPE_ALL) {
149            if (where.length() > 0) {
150                where.append(" AND ");
151            }
152            // Add a clause to fetch only items of type voicemail.
153            where.append(String.format("(%s = ?)", Calls.TYPE));
154            // Add a clause to fetch only items newer than the requested date
155            selectionArgs.add(Integer.toString(callType));
156        }
157
158        if (newerThan > 0) {
159            if (where.length() > 0) {
160                where.append(" AND ");
161            }
162            where.append(String.format("(%s > ?)", Calls.DATE));
163            selectionArgs.add(Long.toString(newerThan));
164        }
165
166        final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
167        final String selection = where.length() > 0 ? where.toString() : null;
168        Uri uri = Calls.CONTENT_URI_WITH_VOICEMAIL.buildUpon()
169                .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
170                .build();
171        startQuery(token, null, uri,
172                CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
173                Calls.DEFAULT_SORT_ORDER);
174    }
175
176    /** Cancel any pending fetch request. */
177    private void cancelFetch() {
178        cancelOperation(QUERY_CALLLOG_TOKEN);
179    }
180
181    /** Updates all new calls to mark them as old. */
182    public void markNewCallsAsOld() {
183        // Mark all "new" calls as not new anymore.
184        StringBuilder where = new StringBuilder();
185        where.append(Calls.NEW);
186        where.append(" = 1");
187
188        ContentValues values = new ContentValues(1);
189        values.put(Calls.NEW, "0");
190
191        startUpdate(UPDATE_MARK_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
192                values, where.toString(), null);
193    }
194
195    /** Updates all new voicemails to mark them as old. */
196    public void markNewVoicemailsAsOld() {
197        // Mark all "new" voicemails as not new anymore.
198        StringBuilder where = new StringBuilder();
199        where.append(Calls.NEW);
200        where.append(" = 1 AND ");
201        where.append(Calls.TYPE);
202        where.append(" = ?");
203
204        ContentValues values = new ContentValues(1);
205        values.put(Calls.NEW, "0");
206
207        startUpdate(UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
208                values, where.toString(), new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) });
209    }
210
211    /** Updates all missed calls to mark them as read. */
212    public void markMissedCallsAsRead() {
213        // Mark all "new" calls as not new anymore.
214        StringBuilder where = new StringBuilder();
215        where.append(Calls.IS_READ).append(" = 0");
216        where.append(" AND ");
217        where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
218
219        ContentValues values = new ContentValues(1);
220        values.put(Calls.IS_READ, "1");
221
222        startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
223                where.toString(), null);
224    }
225
226    @Override
227    protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) {
228        if (cursor == null) {
229            return;
230        }
231        try {
232            if (token == QUERY_CALLLOG_TOKEN) {
233                if (updateAdapterData(cursor)) {
234                    cursor = null;
235                }
236            } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
237                updateVoicemailStatus(cursor);
238            } else {
239                Log.w(TAG, "Unknown query completed: ignoring: " + token);
240            }
241        } finally {
242            if (cursor != null) {
243                cursor.close();
244            }
245        }
246    }
247
248    /**
249     * Updates the adapter in the call log fragment to show the new cursor data.
250     * Returns true if the listener took ownership of the cursor.
251     */
252    private boolean updateAdapterData(Cursor cursor) {
253        final Listener listener = mListener.get();
254        if (listener != null) {
255            return listener.onCallsFetched(cursor);
256        }
257        return false;
258
259    }
260
261    private void updateVoicemailStatus(Cursor statusCursor) {
262        final Listener listener = mListener.get();
263        if (listener != null) {
264            listener.onVoicemailStatusFetched(statusCursor);
265        }
266    }
267
268    /** Listener to completion of various queries. */
269    public interface Listener {
270        /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
271        void onVoicemailStatusFetched(Cursor statusCursor);
272
273        /**
274         * Called when {@link CallLogQueryHandler#fetchCalls(int)}complete.
275         * Returns true if takes ownership of cursor.
276         */
277        boolean onCallsFetched(Cursor combinedCursor);
278    }
279}
280