1 /*
2  * Copyright (C) 2018 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.settings.homepage.contextualcards;
18 
19 import android.content.ContentProvider;
20 import android.content.ContentResolver;
21 import android.content.ContentValues;
22 import android.content.UriMatcher;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteDatabase;
25 import android.database.sqlite.SQLiteQueryBuilder;
26 import android.net.Uri;
27 import android.os.Build;
28 import android.os.StrictMode;
29 import android.util.ArrayMap;
30 import android.util.Log;
31 
32 import androidx.annotation.VisibleForTesting;
33 
34 import com.android.settings.R;
35 import com.android.settingslib.utils.ThreadUtils;
36 
37 import java.util.Map;
38 
39 /**
40  * Provider stores and manages user interaction feedback for homepage contextual cards.
41  */
42 public class CardContentProvider extends ContentProvider {
43 
44     public static final String CARD_AUTHORITY = "com.android.settings.homepage.CardContentProvider";
45 
46     public static final Uri REFRESH_CARD_URI = new Uri.Builder()
47             .scheme(ContentResolver.SCHEME_CONTENT)
48             .authority(CardContentProvider.CARD_AUTHORITY)
49             .appendPath(CardDatabaseHelper.CARD_TABLE)
50             .build();
51 
52     public static final Uri DELETE_CARD_URI = new Uri.Builder()
53             .scheme(ContentResolver.SCHEME_CONTENT)
54             .authority(CardContentProvider.CARD_AUTHORITY)
55             .appendPath(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP)
56             .build();
57 
58     private static final String TAG = "CardContentProvider";
59     /** URI matcher for ContentProvider queries. */
60     private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
61     /** URI matcher type for cards table */
62     private static final int MATCH_CARDS = 100;
63 
64     static {
URI_MATCHER.addURI(CARD_AUTHORITY, CardDatabaseHelper.CARD_TABLE, MATCH_CARDS)65         URI_MATCHER.addURI(CARD_AUTHORITY, CardDatabaseHelper.CARD_TABLE, MATCH_CARDS);
66     }
67 
68     @Override
onCreate()69     public boolean onCreate() {
70         return true;
71     }
72 
73     @Override
insert(Uri uri, ContentValues values)74     public Uri insert(Uri uri, ContentValues values) {
75         final ContentValues[] cvs = {values};
76         bulkInsert(uri, cvs);
77         return uri;
78     }
79 
80     @Override
bulkInsert(Uri uri, ContentValues[] values)81     public int bulkInsert(Uri uri, ContentValues[] values) {
82         final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
83         int numInserted = 0;
84         final CardDatabaseHelper DBHelper = CardDatabaseHelper.getInstance(getContext());
85         final SQLiteDatabase database = DBHelper.getWritableDatabase();
86         final boolean keepDismissalTimestampBeforeDeletion = getContext().getResources()
87                 .getBoolean(R.bool.config_keep_contextual_card_dismissal_timestamp);
88         final Map<String, Long> dismissedTimeMap = new ArrayMap<>();
89 
90         try {
91             maybeEnableStrictMode();
92 
93             final String table = getTableFromMatch(uri);
94             database.beginTransaction();
95 
96             if (keepDismissalTimestampBeforeDeletion) {
97                 // Query the existing db and get dismissal info.
98                 final String[] columns = new String[]{CardDatabaseHelper.CardColumns.NAME,
99                         CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP};
100                 final String selection =
101                         CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP + " IS NOT NULL";
102                 try (Cursor cursor = database.query(table, columns, selection,
103                         null/* selectionArgs */, null /* groupBy */,
104                         null /* having */, null /* orderBy */)) {
105                     // Save them to a Map
106                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
107                         final String cardName = cursor.getString(cursor.getColumnIndex(
108                                 CardDatabaseHelper.CardColumns.NAME));
109                         final long timestamp = cursor.getLong(cursor.getColumnIndex(
110                                 CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP));
111                         dismissedTimeMap.put(cardName, timestamp);
112                     }
113                 }
114             }
115 
116             // Here delete data first to avoid redundant insertion. According to cl/215350754
117             database.delete(table, null /* whereClause */, null /* whereArgs */);
118 
119             for (ContentValues value : values) {
120                 if (keepDismissalTimestampBeforeDeletion) {
121                     // Replace dismissedTimestamp in each value if there is an old one.
122                     final String cardName =
123                             value.get(CardDatabaseHelper.CardColumns.NAME).toString();
124                     if (dismissedTimeMap.containsKey(cardName)) {
125                         // Replace the value of dismissedTimestamp
126                         value.put(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP,
127                                 dismissedTimeMap.get(cardName));
128                         Log.d(TAG, "Replace dismissed time: " + cardName);
129                     }
130                 }
131 
132                 long ret = database.insert(table, null /* nullColumnHack */, value);
133                 if (ret != -1L) {
134                     numInserted++;
135                 } else {
136                     Log.e(TAG, "The row " + value.getAsString(CardDatabaseHelper.CardColumns.NAME)
137                             + " insertion failed! Please check your data.");
138                 }
139             }
140             database.setTransactionSuccessful();
141             getContext().getContentResolver().notifyChange(uri, null /* observer */);
142         } finally {
143             database.endTransaction();
144             StrictMode.setThreadPolicy(oldPolicy);
145         }
146         return numInserted;
147     }
148 
149     @Override
delete(Uri uri, String selection, String[] selectionArgs)150     public int delete(Uri uri, String selection, String[] selectionArgs) {
151         throw new UnsupportedOperationException("delete operation not supported currently.");
152     }
153 
154     @Override
getType(Uri uri)155     public String getType(Uri uri) {
156         throw new UnsupportedOperationException("getType operation not supported currently.");
157     }
158 
159     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)160     public Cursor query(Uri uri, String[] projection, String selection,
161             String[] selectionArgs, String sortOrder) {
162         final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
163         try {
164             maybeEnableStrictMode();
165 
166             final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
167             final String table = getTableFromMatch(uri);
168             queryBuilder.setTables(table);
169             final CardDatabaseHelper DBHelper = CardDatabaseHelper.getInstance(getContext());
170             final SQLiteDatabase database = DBHelper.getReadableDatabase();
171             final Cursor cursor = queryBuilder.query(database,
172                     projection, selection, selectionArgs, null /* groupBy */, null /* having */,
173                     sortOrder);
174 
175             cursor.setNotificationUri(getContext().getContentResolver(), uri);
176             return cursor;
177         } finally {
178             StrictMode.setThreadPolicy(oldPolicy);
179         }
180     }
181 
182     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)183     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
184         throw new UnsupportedOperationException("update operation not supported currently.");
185     }
186 
187     @VisibleForTesting
maybeEnableStrictMode()188     void maybeEnableStrictMode() {
189         if (Build.IS_DEBUGGABLE && ThreadUtils.isMainThread()) {
190             enableStrictMode();
191         }
192     }
193 
194     @VisibleForTesting
enableStrictMode()195     void enableStrictMode() {
196         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().build());
197     }
198 
199     @VisibleForTesting
getTableFromMatch(Uri uri)200     String getTableFromMatch(Uri uri) {
201         final int match = URI_MATCHER.match(uri);
202         String table;
203         switch (match) {
204             case MATCH_CARDS:
205                 table = CardDatabaseHelper.CARD_TABLE;
206                 break;
207             default:
208                 throw new IllegalArgumentException("Unknown Uri format: " + uri);
209         }
210         return table;
211     }
212 }
213