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