1/*
2 * Copyright (C) 2015 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.database;
18
19import android.content.AsyncQueryHandler;
20import android.content.ContentResolver;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.database.Cursor;
24import android.database.DatabaseUtils;
25import android.database.sqlite.SQLiteDatabaseCorruptException;
26import android.net.Uri;
27import android.support.annotation.Nullable;
28import android.telephony.PhoneNumberUtils;
29import android.text.TextUtils;
30
31import com.android.dialer.compat.FilteredNumberCompat;
32import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
33import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
34import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
35
36public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler {
37    private static final int NO_TOKEN = 0;
38
39    public FilteredNumberAsyncQueryHandler(ContentResolver cr) {
40        super(cr);
41    }
42
43    /**
44     * Methods for FilteredNumberAsyncQueryHandler result returns.
45     */
46    private static abstract class Listener {
47        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
48        }
49        protected void onInsertComplete(int token, Object cookie, Uri uri) {
50        }
51        protected void onUpdateComplete(int token, Object cookie, int result) {
52        }
53        protected void onDeleteComplete(int token, Object cookie, int result) {
54        }
55    }
56
57    public interface OnCheckBlockedListener {
58        /**
59         * Invoked after querying if a number is blocked.
60         * @param id The ID of the row if blocked, null otherwise.
61         */
62        void onCheckComplete(Integer id);
63    }
64
65    public interface OnBlockNumberListener {
66        /**
67         * Invoked after inserting a blocked number.
68         * @param uri The uri of the newly created row.
69         */
70        void onBlockComplete(Uri uri);
71    }
72
73    public interface OnUnblockNumberListener {
74        /**
75         * Invoked after removing a blocked number
76         * @param rows The number of rows affected (expected value 1).
77         * @param values The deleted data (used for restoration).
78         */
79        void onUnblockComplete(int rows, ContentValues values);
80    }
81
82    public interface OnHasBlockedNumbersListener {
83        /**
84         * @param hasBlockedNumbers {@code true} if any blocked numbers are stored.
85         *     {@code false} otherwise.
86         */
87        void onHasBlockedNumbers(boolean hasBlockedNumbers);
88    }
89
90    @Override
91    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
92        if (cookie != null) {
93            ((Listener) cookie).onQueryComplete(token, cookie, cursor);
94        }
95    }
96
97    @Override
98    protected void onInsertComplete(int token, Object cookie, Uri uri) {
99        if (cookie != null) {
100            ((Listener) cookie).onInsertComplete(token, cookie, uri);
101        }
102    }
103
104    @Override
105    protected void onUpdateComplete(int token, Object cookie, int result) {
106        if (cookie != null) {
107            ((Listener) cookie).onUpdateComplete(token, cookie, result);
108        }
109    }
110
111    @Override
112    protected void onDeleteComplete(int token, Object cookie, int result) {
113        if (cookie != null) {
114            ((Listener) cookie).onDeleteComplete(token, cookie, result);
115        }
116    }
117
118    public final void incrementFilteredCount(Integer id) {
119        // No concept of counts with new filtering
120        if (FilteredNumberCompat.useNewFiltering()) {
121            return;
122        }
123        startUpdate(NO_TOKEN, null,
124                ContentUris.withAppendedId(FilteredNumber.CONTENT_URI_INCREMENT_FILTERED_COUNT, id),
125                null, null, null);
126    }
127
128    public void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) {
129        startQuery(NO_TOKEN,
130                new Listener() {
131                    @Override
132                    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
133                        listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0);
134                    }
135                },
136                FilteredNumberCompat.getContentUri(null),
137                new String[]{ FilteredNumberCompat.getIdColumnName() },
138                FilteredNumberCompat.useNewFiltering() ? null : FilteredNumberColumns.TYPE
139                        + "=" + FilteredNumberTypes.BLOCKED_NUMBER,
140                null,
141                null);
142    }
143
144    /**
145     * Check if this number has been blocked.
146     *
147     * @return {@code false} if the number was invalid and couldn't be checked,
148     *     {@code true} otherwise,
149     */
150    public boolean isBlockedNumber(
151            final OnCheckBlockedListener listener, String number, String countryIso) {
152        final String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
153        if (TextUtils.isEmpty(e164Number)) {
154            return false;
155        }
156
157        startQuery(NO_TOKEN,
158                new Listener() {
159                    @Override
160                    protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
161                        /*
162                         * In the frameworking blocking, numbers can be blocked in both e164 format
163                         * and not, resulting in multiple rows being returned for this query. For
164                         * example, both '16502530000' and '6502530000' can exist at the same time
165                         * and will be returned by this query.
166                         */
167                        if (cursor == null || cursor.getCount() == 0) {
168                            listener.onCheckComplete(null);
169                            return;
170                        }
171                        cursor.moveToFirst();
172                        // New filtering doesn't have a concept of type
173                        if (!FilteredNumberCompat.useNewFiltering()
174                                && cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE))
175                                != FilteredNumberTypes.BLOCKED_NUMBER) {
176                            listener.onCheckComplete(null);
177                            return;
178                        }
179                        listener.onCheckComplete(
180                                cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)));
181                    }
182                },
183                FilteredNumberCompat.getContentUri(null),
184                FilteredNumberCompat.filter(new String[]{FilteredNumberCompat.getIdColumnName(),
185                        FilteredNumberCompat.getTypeColumnName()}),
186                FilteredNumberCompat.getE164NumberColumnName() + " = ?",
187                new String[]{e164Number},
188                null);
189
190        return true;
191    }
192
193    public void blockNumber(
194            final OnBlockNumberListener listener, String number, @Nullable String countryIso) {
195        blockNumber(listener, null, number, countryIso);
196    }
197
198    /**
199     * Add a number manually blocked by the user.
200     */
201    public void blockNumber(
202            final OnBlockNumberListener listener,
203            @Nullable String normalizedNumber,
204            String number,
205            @Nullable String countryIso) {
206        blockNumber(listener, FilteredNumberCompat.newBlockNumberContentValues(number,
207                normalizedNumber, countryIso));
208    }
209
210    /**
211     * Block a number with specified ContentValues. Can be manually added or a restored row
212     * from performing the 'undo' action after unblocking.
213     */
214    public void blockNumber(final OnBlockNumberListener listener, ContentValues values) {
215        startInsert(NO_TOKEN,
216                new Listener() {
217                    @Override
218                    public void onInsertComplete(int token, Object cookie, Uri uri) {
219                        if (listener != null ) {
220                            listener.onBlockComplete(uri);
221                        }
222                    }
223                }, FilteredNumberCompat.getContentUri(null), values);
224    }
225
226    /**
227     * Unblocks the number with the given id.
228     *
229     * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
230     * unblocked.
231     * @param id The id of the number to unblock.
232     */
233    public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) {
234        if (id == null) {
235            throw new IllegalArgumentException("Null id passed into unblock");
236        }
237        unblock(listener, FilteredNumberCompat.getContentUri(id));
238    }
239
240    /**
241     * Removes row from database.
242     * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
243     * unblocked.
244     * @param uri The uri of row to remove, from
245     * {@link FilteredNumberAsyncQueryHandler#blockNumber}.
246     */
247    public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) {
248        startQuery(NO_TOKEN, new Listener() {
249            @Override
250            public void onQueryComplete(int token, Object cookie, Cursor cursor) {
251                int rowsReturned = cursor == null ? 0 : cursor.getCount();
252                if (rowsReturned != 1) {
253                    throw new SQLiteDatabaseCorruptException
254                            ("Returned " + rowsReturned + " rows for uri "
255                                    + uri + "where 1 expected.");
256                }
257                cursor.moveToFirst();
258                final ContentValues values = new ContentValues();
259                DatabaseUtils.cursorRowToContentValues(cursor, values);
260                values.remove(FilteredNumberCompat.getIdColumnName());
261
262                startDelete(NO_TOKEN, new Listener() {
263                    @Override
264                    public void onDeleteComplete(int token, Object cookie, int result) {
265                        if (listener != null) {
266                            listener.onUnblockComplete(result, values);
267                        }
268                    }
269                }, uri, null, null);
270            }
271        }, uri, null, null, null, null);
272    }
273}
274