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     private CardDatabaseHelper mDBHelper;
69 
70     @Override
onCreate()71     public boolean onCreate() {
72         mDBHelper = CardDatabaseHelper.getInstance(getContext());
73         return true;
74     }
75 
76     @Override
insert(Uri uri, ContentValues values)77     public Uri insert(Uri uri, ContentValues values) {
78         final ContentValues[] cvs = {values};
79         bulkInsert(uri, cvs);
80         return uri;
81     }
82 
83     @Override
bulkInsert(Uri uri, ContentValues[] values)84     public int bulkInsert(Uri uri, ContentValues[] values) {
85         final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
86         int numInserted = 0;
87         final SQLiteDatabase database = mDBHelper.getWritableDatabase();
88         final boolean keepDismissalTimestampBeforeDeletion = getContext().getResources()
89                 .getBoolean(R.bool.config_keep_contextual_card_dismissal_timestamp);
90         final Map<String, Long> dismissedTimeMap = new ArrayMap<>();
91 
92         try {
93             maybeEnableStrictMode();
94 
95             final String table = getTableFromMatch(uri);
96             database.beginTransaction();
97 
98             if (keepDismissalTimestampBeforeDeletion) {
99                 // Query the existing db and get dismissal info.
100                 final String[] columns = new String[]{CardDatabaseHelper.CardColumns.NAME,
101                         CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP};
102                 final String selection =
103                         CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP + " IS NOT NULL";
104                 try (Cursor cursor = database.query(table, columns, selection,
105                         null/* selectionArgs */, null /* groupBy */,
106                         null /* having */, null /* orderBy */)) {
107                     // Save them to a Map
108                     for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
109                         final String cardName = cursor.getString(cursor.getColumnIndex(
110                                 CardDatabaseHelper.CardColumns.NAME));
111                         final long timestamp = cursor.getLong(cursor.getColumnIndex(
112                                 CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP));
113                         dismissedTimeMap.put(cardName, timestamp);
114                     }
115                 }
116             }
117 
118             // Here delete data first to avoid redundant insertion. According to cl/215350754
119             database.delete(table, null /* whereClause */, null /* whereArgs */);
120 
121             for (ContentValues value : values) {
122                 if (keepDismissalTimestampBeforeDeletion) {
123                     // Replace dismissedTimestamp in each value if there is an old one.
124                     final String cardName =
125                             value.get(CardDatabaseHelper.CardColumns.NAME).toString();
126                     if (dismissedTimeMap.containsKey(cardName)) {
127                         // Replace the value of dismissedTimestamp
128                         value.put(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP,
129                                 dismissedTimeMap.get(cardName));
130                         Log.d(TAG, "Replace dismissed time: " + cardName);
131                     }
132                 }
133 
134                 long ret = database.insert(table, null /* nullColumnHack */, value);
135                 if (ret != -1L) {
136                     numInserted++;
137                 } else {
138                     Log.e(TAG, "The row " + value.getAsString(CardDatabaseHelper.CardColumns.NAME)
139                             + " insertion failed! Please check your data.");
140                 }
141             }
142             database.setTransactionSuccessful();
143             getContext().getContentResolver().notifyChange(uri, null /* observer */);
144         } finally {
145             database.endTransaction();
146             StrictMode.setThreadPolicy(oldPolicy);
147         }
148         return numInserted;
149     }
150 
151     @Override
delete(Uri uri, String selection, String[] selectionArgs)152     public int delete(Uri uri, String selection, String[] selectionArgs) {
153         throw new UnsupportedOperationException("delete operation not supported currently.");
154     }
155 
156     @Override
getType(Uri uri)157     public String getType(Uri uri) {
158         throw new UnsupportedOperationException("getType operation not supported currently.");
159     }
160 
161     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)162     public Cursor query(Uri uri, String[] projection, String selection,
163             String[] selectionArgs, String sortOrder) {
164         final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
165         try {
166             maybeEnableStrictMode();
167 
168             final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
169             final String table = getTableFromMatch(uri);
170             queryBuilder.setTables(table);
171             final SQLiteDatabase database = mDBHelper.getReadableDatabase();
172             final Cursor cursor = queryBuilder.query(database,
173                     projection, selection, selectionArgs, null /* groupBy */, null /* having */,
174                     sortOrder);
175 
176             cursor.setNotificationUri(getContext().getContentResolver(), uri);
177             return cursor;
178         } finally {
179             StrictMode.setThreadPolicy(oldPolicy);
180         }
181     }
182 
183     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)184     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
185         throw new UnsupportedOperationException("update operation not supported currently.");
186     }
187 
188     @VisibleForTesting
maybeEnableStrictMode()189     void maybeEnableStrictMode() {
190         if (Build.IS_DEBUGGABLE && ThreadUtils.isMainThread()) {
191             enableStrictMode();
192         }
193     }
194 
195     @VisibleForTesting
enableStrictMode()196     void enableStrictMode() {
197         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().build());
198     }
199 
200     @VisibleForTesting
getTableFromMatch(Uri uri)201     String getTableFromMatch(Uri uri) {
202         final int match = URI_MATCHER.match(uri);
203         String table;
204         switch (match) {
205             case MATCH_CARDS:
206                 table = CardDatabaseHelper.CARD_TABLE;
207                 break;
208             default:
209                 throw new IllegalArgumentException("Unknown Uri format: " + uri);
210         }
211         return table;
212     }
213 }
214