diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ccdc2dc0838..f400cfcdfc6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -3148,6 +3148,12 @@ + + diff --git a/src/com/android/settings/homepage/CardContentProvider.java b/src/com/android/settings/homepage/CardContentProvider.java new file mode 100644 index 00000000000..3081ae1fa81 --- /dev/null +++ b/src/com/android/settings/homepage/CardContentProvider.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.homepage; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.os.Build; +import android.os.StrictMode; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +/** + * Provider stores and manages user interaction feedback for homepage contextual cards. + */ +public class CardContentProvider extends ContentProvider { + + private static final String TAG = "CardContentProvider"; + + public static final String CARD_AUTHORITY = "com.android.settings.homepage.CardContentProvider"; + + /** URI matcher for ContentProvider queries. */ + private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + /** URI matcher type for cards table */ + private static final int MATCH_CARDS = 100; + /** URI matcher type for card log table */ + private static final int MATCH_CARD_LOG = 200; + + static { + sUriMatcher.addURI(CARD_AUTHORITY, CardDatabaseHelper.CARD_TABLE, MATCH_CARDS); + } + + private CardDatabaseHelper mDBHelper; + + @Override + public boolean onCreate() { + mDBHelper = CardDatabaseHelper.getInstance(getContext()); + return true; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + if (Build.IS_DEBUGGABLE) { + enableStrictMode(true); + } + + final SQLiteDatabase database = mDBHelper.getWritableDatabase(); + final String table = getTableFromMatch(uri); + final long ret = database.insert(table, null, values); + if (ret != -1) { + getContext().getContentResolver().notifyChange(uri, null); + } else { + Log.e(TAG, "The CardContentProvider insertion failed! Plase check SQLiteDatabase's " + + "message."); + } + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + return uri; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + if (Build.IS_DEBUGGABLE) { + enableStrictMode(true); + } + + final SQLiteDatabase database = mDBHelper.getWritableDatabase(); + final String table = getTableFromMatch(uri); + final int rowsDeleted = database.delete(table, selection, selectionArgs); + getContext().getContentResolver().notifyChange(uri, null); + return rowsDeleted; + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + } + + @Override + public String getType(Uri uri) { + throw new UnsupportedOperationException("getType operation not supported currently."); + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + if (Build.IS_DEBUGGABLE) { + enableStrictMode(true); + } + + final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + final String table = getTableFromMatch(uri); + queryBuilder.setTables(table); + final SQLiteDatabase database = mDBHelper.getReadableDatabase(); + final Cursor cursor = queryBuilder.query(database, + projection, selection, selectionArgs, null, null, sortOrder); + + cursor.setNotificationUri(getContext().getContentResolver(), uri); + return cursor; + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + if (Build.IS_DEBUGGABLE) { + enableStrictMode(true); + } + + final SQLiteDatabase database = mDBHelper.getWritableDatabase(); + final String table = getTableFromMatch(uri); + final int rowsUpdated = database.update(table, values, selection, selectionArgs); + getContext().getContentResolver().notifyChange(uri, null); + return rowsUpdated; + } finally { + StrictMode.setThreadPolicy(oldPolicy); + } + } + + private void enableStrictMode(boolean enabled) { + StrictMode.setThreadPolicy(enabled + ? new StrictMode.ThreadPolicy.Builder().detectAll().build() + : StrictMode.ThreadPolicy.LAX); + } + + @VisibleForTesting + String getTableFromMatch(Uri uri) { + final int match = sUriMatcher.match(uri); + String table; + switch (match) { + case MATCH_CARDS: + table = CardDatabaseHelper.CARD_TABLE; + break; + default: + throw new IllegalArgumentException("Unknown Uri format: " + uri); + } + return table; + } +} diff --git a/src/com/android/settings/homepage/CardDatabaseHelper.java b/src/com/android/settings/homepage/CardDatabaseHelper.java new file mode 100644 index 00000000000..b4dc221d39b --- /dev/null +++ b/src/com/android/settings/homepage/CardDatabaseHelper.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.homepage; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import androidx.annotation.VisibleForTesting; + +/** + * Defines the schema for the Homepage Cards database. + */ +public class CardDatabaseHelper extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "homepage_cards.db"; + private static final int DATABASE_VERSION = 1; + + public static final String CARD_TABLE = "cards"; + + public interface CardColumns { + /** + * Primary key. Name of the card. + */ + String NAME = "name"; + + /** + * Type of the card. + */ + String TYPE = "type"; + + /** + * Score of the card. Higher numbers have higher priorities. + */ + String SCORE = "score"; + + /** + * URI of the slice card. + */ + String SLICE_URI = "slice_uri"; + + /** + * Category of the card. The value is between 0 to 3. + */ + String CATEGORY = "category"; + + /** + * URI decides the card can be shown. + */ + String AVAILABILITY_URI = "availability_uri"; + + /** + * Keep the card last display's locale. + */ + String LOCALIZED_TO_LOCALE = "localized_to_locale"; + + /** + * Package name for all card candidates. + */ + String PACKAGE_NAME = "package_name"; + + /** + * Application version of the package. + */ + String APP_VERSION = "app_version"; + + /** + * Title resource name of the package. + */ + String TITLE_RES_NAME = "title_res_name"; + + /** + * Title of the package to be shown. + */ + String TITLE_TEXT = "title_text"; + + /** + * Summary resource name of the package. + */ + String SUMMARY_RES_NAME = "summary_res_name"; + + /** + * Summary of the package to be shown. + */ + String SUMMARY_TEXT = "summary_text"; + + /** + * Icon resource name of the package. + */ + String ICON_RES_NAME = "icon_res_name"; + + /** + * Icon resource id of the package. + */ + String ICON_RES_ID = "icon_res_id"; + + /** + * PendingIntent for for custom view card candidate. Do action when user press card. + */ + String CARD_ACTION = "card_action"; + + /** + * Expire time of the card. The unit of the value is mini-second. + */ + String EXPIRE_TIME_MS = "expire_time_ms"; + } + + private static final String CREATE_CARD_TABLE = + "CREATE TABLE " + CARD_TABLE + + "(" + + CardColumns.NAME + + " TEXT NOT NULL PRIMARY KEY, " + + CardColumns.TYPE + + " INTEGER NOT NULL, " + + CardColumns.SCORE + + " DOUBLE NOT NULL, " + + CardColumns.SLICE_URI + + " TEXT, " + + CardColumns.CATEGORY + + " INTEGER DEFAULT 0 CHECK (" + + CardColumns.CATEGORY + + " >= 0 AND " + + CardColumns.CATEGORY + + " <= 3), " + + CardColumns.AVAILABILITY_URI + + " TEXT, " + + CardColumns.LOCALIZED_TO_LOCALE + + " TEXT, " + + CardColumns.PACKAGE_NAME + + " TEXT NOT NULL, " + + CardColumns.APP_VERSION + + " TEXT NOT NULL, " + + CardColumns.TITLE_RES_NAME + + " TEXT, " + + CardColumns.TITLE_TEXT + + " TEXT, " + + CardColumns.SUMMARY_RES_NAME + + " TEXT, " + + CardColumns.SUMMARY_TEXT + + " TEXT, " + + CardColumns.ICON_RES_NAME + + " TEXT, " + + CardColumns.ICON_RES_ID + + " INTEGER DEFAULT 0, " + + CardColumns.CARD_ACTION + + " TEXT, " + + CardColumns.EXPIRE_TIME_MS + + " INTEGER " + + ");"; + + public CardDatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_CARD_TABLE); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + CARD_TABLE); + onCreate(db); + } + } + + @VisibleForTesting + static CardDatabaseHelper sCardDatabaseHelper; + + public static synchronized CardDatabaseHelper getInstance(Context context) { + if (sCardDatabaseHelper == null) { + sCardDatabaseHelper = new CardDatabaseHelper(context.getApplicationContext()); + } + return sCardDatabaseHelper; + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/CardContentProviderTest.java b/tests/robotests/src/com/android/settings/homepage/CardContentProviderTest.java new file mode 100644 index 00000000000..bf1527a4a42 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/CardContentProviderTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.homepage; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; + +@RunWith(SettingsRobolectricTestRunner.class) +public class CardContentProviderTest { + + private Context mContext; + private CardContentProvider mProvider; + private Uri mUri; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mProvider = Robolectric.setupContentProvider(CardContentProvider.class); + mUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(CardContentProvider.CARD_AUTHORITY) + .path(CardDatabaseHelper.CARD_TABLE) + .build(); + } + + @After + public void cleanUp() { + CardDatabaseHelper.getInstance(mContext).close(); + CardDatabaseHelper.sCardDatabaseHelper = null; + } + + @Test + public void cardData_insert() { + final int cnt_before_instert = getRowCount(); + mContext.getContentResolver().insert(mUri, insertOneRow()); + final int cnt_after_instert = getRowCount(); + + assertThat(cnt_after_instert - cnt_before_instert).isEqualTo(1); + } + + @Test + public void cardData_query() { + mContext.getContentResolver().insert(mUri, insertOneRow()); + final int count = getRowCount(); + + assertThat(count).isGreaterThan(0); + } + + @Test + public void cardData_delete() { + final ContentResolver contentResolver = mContext.getContentResolver(); + contentResolver.insert(mUri, insertOneRow()); + final int del_count = contentResolver.delete(mUri, null, null); + + assertThat(del_count).isGreaterThan(0); + } + + @Test + public void cardData_update() { + final ContentResolver contentResolver = mContext.getContentResolver(); + contentResolver.insert(mUri, insertOneRow()); + + final double updatingScore= 0.87; + final ContentValues values = new ContentValues(); + values.put(CardDatabaseHelper.CardColumns.SCORE, updatingScore); + final String strWhere = CardDatabaseHelper.CardColumns.NAME + "=?"; + final String[] selectionArgs = {"auto_rotate"}; + final int update_count = contentResolver.update(mUri, values, strWhere, selectionArgs); + + assertThat(update_count).isGreaterThan(0); + + final String[] columns = {CardDatabaseHelper.CardColumns.SCORE}; + final Cursor cr = contentResolver.query(mUri, columns, strWhere, selectionArgs, null); + cr.moveToFirst(); + final double qryScore = cr.getDouble(0); + + cr.close(); + assertThat(qryScore).isEqualTo(updatingScore); + } + + @Test(expected = UnsupportedOperationException.class) + public void getType_shouldCrash() { + mProvider.getType(null); + } + + @Test(expected = IllegalArgumentException.class) + public void invalid_Uri_shouldCrash() { + final Uri invalid_Uri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(CardContentProvider.CARD_AUTHORITY) + .path("Invalid_table") + .build(); + + mProvider.getTableFromMatch(invalid_Uri); + } + + private ContentValues insertOneRow() { + final ContentValues values = new ContentValues(); + values.put(CardDatabaseHelper.CardColumns.NAME, "auto_rotate"); + values.put(CardDatabaseHelper.CardColumns.TYPE, 0); + values.put(CardDatabaseHelper.CardColumns.SCORE, 0.9); + values.put(CardDatabaseHelper.CardColumns.SLICE_URI, + "content://com.android.settings.slices/action/auto_rotate"); + values.put(CardDatabaseHelper.CardColumns.CATEGORY, 2); + values.put(CardDatabaseHelper.CardColumns.PACKAGE_NAME, "com.android.settings"); + values.put(CardDatabaseHelper.CardColumns.APP_VERSION, "1.0.0"); + + return values; + } + + private int getRowCount() { + final ContentResolver contentResolver = mContext.getContentResolver(); + final Cursor cr = contentResolver.query(mUri, null, null, null); + final int count = cr.getCount(); + cr.close(); + return count; + } +} diff --git a/tests/robotests/src/com/android/settings/homepage/CardDatabaseHelperTest.java b/tests/robotests/src/com/android/settings/homepage/CardDatabaseHelperTest.java new file mode 100644 index 00000000000..b6ed358a5d5 --- /dev/null +++ b/tests/robotests/src/com/android/settings/homepage/CardDatabaseHelperTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.homepage; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; + +@RunWith(SettingsRobolectricTestRunner.class) +public class CardDatabaseHelperTest { + + private Context mContext; + private CardDatabaseHelper mCardDatabaseHelper; + private SQLiteDatabase mDatabase; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mCardDatabaseHelper = CardDatabaseHelper.getInstance(mContext); + mDatabase = mCardDatabaseHelper.getWritableDatabase(); + } + + @After + public void cleanUp() { + CardDatabaseHelper.getInstance(mContext).close(); + CardDatabaseHelper.sCardDatabaseHelper = null; + } + + @Test + public void testDatabaseSchema() { + final Cursor cursor = mDatabase.rawQuery("SELECT * FROM " + CardDatabaseHelper.CARD_TABLE, + null); + final String[] columnNames = cursor.getColumnNames(); + + final String[] expectedNames = { + CardDatabaseHelper.CardColumns.NAME, + CardDatabaseHelper.CardColumns.TYPE, + CardDatabaseHelper.CardColumns.SCORE, + CardDatabaseHelper.CardColumns.SLICE_URI, + CardDatabaseHelper.CardColumns.CATEGORY, + CardDatabaseHelper.CardColumns.AVAILABILITY_URI, + CardDatabaseHelper.CardColumns.LOCALIZED_TO_LOCALE, + CardDatabaseHelper.CardColumns.PACKAGE_NAME, + CardDatabaseHelper.CardColumns.APP_VERSION, + CardDatabaseHelper.CardColumns.TITLE_RES_NAME, + CardDatabaseHelper.CardColumns.TITLE_TEXT, + CardDatabaseHelper.CardColumns.SUMMARY_RES_NAME, + CardDatabaseHelper.CardColumns.SUMMARY_TEXT, + CardDatabaseHelper.CardColumns.ICON_RES_NAME, + CardDatabaseHelper.CardColumns.ICON_RES_ID, + CardDatabaseHelper.CardColumns.CARD_ACTION, + CardDatabaseHelper.CardColumns.EXPIRE_TIME_MS, + }; + + assertThat(columnNames).isEqualTo(expectedNames); + cursor.close(); + } +}