Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 195f61b2 authored by Sunny Shao's avatar Sunny Shao
Browse files

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
parent 89d35f94
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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>
+166 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+190 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+147 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+83 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}