Added the CardContentProvider

- Added the CardContentProvider
- Added the CardDatabaseHelper
- Added the CardContentProviderTest, CardDatabaseHelperTest
- Modified CardDatabaseHelper and added the locale and expire_time_ms
- Added the permission for CardContentProvider
- Modified CardDatabaseHelper and added the category and availability_uri
- Added the UriMatcher

Test: robotest
Bug: 111820446
Change-Id: Ie9df065133307f4eac2680637f67be1dcb8310a3
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ccdc2dc..f400cfc 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3148,6 +3148,12 @@
         <service android:name=".fuelgauge.batterytip.AnomalyDetectionJobService"
                  android:permission="android.permission.BIND_JOB_SERVICE" />
 
+        <provider
+            android:name=".homepage.CardContentProvider"
+            android:authorities="com.android.settings.homepage.CardContentProvider"
+            android:exported="true"
+            android:permission="android.permission.WRITE_SETTINGS_HOMEPAGE_DATA" />
+
         <!-- This is the longest AndroidManifest.xml ever. -->
     </application>
 </manifest>
diff --git a/src/com/android/settings/homepage/CardContentProvider.java b/src/com/android/settings/homepage/CardContentProvider.java
new file mode 100644
index 0000000..3081ae1
--- /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 0000000..b4dc221
--- /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 0000000..bf1527a
--- /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 0000000..b6ed358
--- /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();
+    }
+}