<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>
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}