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