Merge "Implement new dismissal behaviour of contextual card"

This commit is contained in:
TreeHugger Robot
2020-02-20 12:05:36 +00:00
committed by Android (Google) Code Review
10 changed files with 193 additions and 105 deletions

View File

@@ -48,7 +48,7 @@ public class CardContentProvider extends ContentProvider {
public static final Uri DELETE_CARD_URI = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(CardContentProvider.CARD_AUTHORITY)
.appendPath(CardDatabaseHelper.CardColumns.CARD_DISMISSED)
.appendPath(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP)
.build();
private static final String TAG = "CardContentProvider";

View File

@@ -16,9 +16,7 @@
package com.android.settings.homepage.contextualcards;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
@@ -31,7 +29,7 @@ import androidx.annotation.VisibleForTesting;
public class CardDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "CardDatabaseHelper";
private static final String DATABASE_NAME = "homepage_cards.db";
private static final int DATABASE_VERSION = 6;
private static final int DATABASE_VERSION = 7;
public static final String CARD_TABLE = "cards";
@@ -72,31 +70,32 @@ public class CardDatabaseHelper extends SQLiteOpenHelper {
String APP_VERSION = "app_version";
/**
* Decide the card is dismissed or not.
* Timestamp of card being dismissed.
*/
String CARD_DISMISSED = "card_dismissed";
String DISMISSED_TIMESTAMP = "dismissed_timestamp";
}
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, " +
CardColumns.PACKAGE_NAME +
" TEXT NOT NULL, " +
CardColumns.APP_VERSION +
" INTEGER NOT NULL, " +
CardColumns.CARD_DISMISSED +
" INTEGER DEFAULT 0 " +
");";
"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, "
+ CardColumns.PACKAGE_NAME
+ " TEXT NOT NULL, "
+ CardColumns.APP_VERSION
+ " INTEGER NOT NULL, "
+ CardColumns.DISMISSED_TIMESTAMP
+ " INTEGER"
+ ");";
public CardDatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
@@ -125,32 +124,4 @@ public class CardDatabaseHelper extends SQLiteOpenHelper {
}
return sCardDatabaseHelper;
}
Cursor getContextualCards() {
final SQLiteDatabase db = getReadableDatabase();
final String selection = CardColumns.CARD_DISMISSED + "=0";
return db.query(CARD_TABLE, null /* columns */, selection,
null /* selectionArgs */, null /* groupBy */, null /* having */,
CardColumns.SCORE + " DESC" /* orderBy */);
}
/**
* Mark a specific ContextualCard with dismissal flag in the database to indicate that the
* card has been dismissed.
*
* @param context Context
* @param cardName The card name of the ContextualCard which is dismissed by user.
* @return The number of rows updated
*/
public int markContextualCardAsDismissed(Context context, String cardName) {
final SQLiteDatabase database = getWritableDatabase();
final ContentValues values = new ContentValues();
values.put(CardColumns.CARD_DISMISSED, 1);
final String selection = CardColumns.NAME + "=?";
final String[] selectionArgs = {cardName};
final int rowsUpdated = database.update(CARD_TABLE, values, selection, selectionArgs);
database.close();
context.getContentResolver().notifyChange(CardContentProvider.DELETE_CARD_URI, null);
return rowsUpdated;
}
}

View File

@@ -16,10 +16,25 @@
package com.android.settings.homepage.contextualcards;
import android.content.Context;
import android.database.Cursor;
import androidx.slice.Slice;
/** Feature provider for the contextual card feature. */
public interface ContextualCardFeatureProvider {
/** Get contextual cards from the card provider */
Cursor getContextualCards();
/**
* Mark a specific {@link ContextualCard} as dismissed with dismissal signal in the database
* to indicate that the card has been dismissed.
*
* @param context Context
* @param cardName The card name of the ContextualCard which is dismissed by user.
* @return The number of rows updated
*/
int markCardAsDismissed(Context context, String cardName);
/** Log package when user clicks contextual notification channel card. */
void logNotificationPackage(Slice slice);

View File

@@ -18,10 +18,19 @@ package com.android.settings.homepage.contextualcards;
import static android.content.Context.MODE_PRIVATE;
import static com.android.settings.homepage.contextualcards.CardDatabaseHelper.CARD_TABLE;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.slice.Slice;
import androidx.slice.SliceMetadata;
import androidx.slice.core.SliceAction;
@@ -30,16 +39,46 @@ import com.android.settings.SettingsActivity;
import com.android.settings.applications.AppInfoBase;
import com.android.settings.homepage.contextualcards.slices.ContextualNotificationChannelSlice;
import com.android.settings.slices.CustomSliceRegistry;
import com.android.settingslib.utils.ThreadUtils;
import java.util.Set;
public class ContextualCardFeatureProviderImpl implements ContextualCardFeatureProvider {
private static final String TAG = "ContextualCardFeatureProvider";
private final Context mContext;
public ContextualCardFeatureProviderImpl(Context context) {
mContext = context;
}
@Override
public Cursor getContextualCards() {
final SQLiteDatabase db = CardDatabaseHelper.getInstance(mContext).getReadableDatabase();
//TODO(b/149542061): Make the dismissal duration configurable.
final long threshold = System.currentTimeMillis() - DateUtils.DAY_IN_MILLIS;
final String selection = CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP + " < ? OR "
+ CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP + " IS NULL";
final String[] selectionArgs = {String.valueOf(threshold)};
final Cursor cursor = db.query(CARD_TABLE, null /* columns */, selection,
selectionArgs /* selectionArgs */, null /* groupBy */, null /* having */,
CardDatabaseHelper.CardColumns.SCORE + " DESC" /* orderBy */);
ThreadUtils.postOnBackgroundThread(() -> resetDismissedTime(threshold));
return cursor;
}
@Override
public int markCardAsDismissed(Context context, String cardName) {
final SQLiteDatabase db = CardDatabaseHelper.getInstance(mContext).getWritableDatabase();
final ContentValues values = new ContentValues();
values.put(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP, System.currentTimeMillis());
final String selection = CardDatabaseHelper.CardColumns.NAME + "=?";
final String[] selectionArgs = {cardName};
final int rowsUpdated = db.update(CARD_TABLE, values, selection, selectionArgs);
context.getContentResolver().notifyChange(CardContentProvider.DELETE_CARD_URI, null);
return rowsUpdated;
}
@Override
public void logNotificationPackage(Slice slice) {
if (slice == null || !slice.getUri().equals(
@@ -62,4 +101,20 @@ public class ContextualCardFeatureProviderImpl implements ContextualCardFeatureP
prefs.edit().putStringSet(ContextualNotificationChannelSlice.PREF_KEY_INTERACTED_PACKAGES,
newInteractedPackages).apply();
}
@VisibleForTesting
int resetDismissedTime(long threshold) {
final SQLiteDatabase database =
CardDatabaseHelper.getInstance(mContext).getWritableDatabase();
final ContentValues values = new ContentValues();
values.putNull(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP);
final String selection = CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP + " < ? AND "
+ CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP + " IS NOT NULL";
final String[] selectionArgs = {String.valueOf(threshold)};
final int rowsUpdated = database.update(CARD_TABLE, values, selection, selectionArgs);
if (Build.IS_DEBUGGABLE) {
Log.d(TAG, "Reset " + rowsUpdated + " records of dismissed time.");
}
return rowsUpdated;
}
}

View File

@@ -178,7 +178,9 @@ public class ContextualCardLoader extends AsyncLoaderCompat<List<ContextualCard>
@VisibleForTesting
Cursor getContextualCardsFromProvider() {
return CardDatabaseHelper.getInstance(mContext).getContextualCards();
final ContextualCardFeatureProvider cardFeatureProvider =
FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider(mContext);
return cardFeatureProvider.getContextualCards();
}
@VisibleForTesting

View File

@@ -25,9 +25,9 @@ import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import com.android.settings.R;
import com.android.settings.homepage.contextualcards.CardDatabaseHelper;
import com.android.settings.homepage.contextualcards.ContextualCard;
import com.android.settings.homepage.contextualcards.ContextualCardController;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
import com.android.settings.homepage.contextualcards.ContextualCardFeedbackDialog;
import com.android.settings.homepage.contextualcards.ContextualCardUpdateListener;
import com.android.settings.homepage.contextualcards.logging.ContextualCardLogUtils;
@@ -68,8 +68,9 @@ public class SliceContextualCardController implements ContextualCardController {
@Override
public void onDismissed(ContextualCard card) {
ThreadUtils.postOnBackgroundThread(() -> {
final CardDatabaseHelper dbHelper = CardDatabaseHelper.getInstance(mContext);
dbHelper.markContextualCardAsDismissed(mContext, card.getName());
final ContextualCardFeatureProvider cardFeatureProvider =
FeatureFactory.getFactory(mContext).getContextualCardFeatureProvider(mContext);
cardFeatureProvider.markCardAsDismissed(mContext, card.getName());
});
showFeedbackDialog(card);

View File

@@ -18,13 +18,10 @@ package com.android.settings.homepage.contextualcards;
import static com.google.common.truth.Truth.assertThat;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.android.settings.intelligence.ContextualCardProto;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -32,9 +29,6 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class CardDatabaseHelperTest {
@@ -46,7 +40,7 @@ public class CardDatabaseHelperTest {
public void setUp() {
mContext = RuntimeEnvironment.application;
mCardDatabaseHelper = CardDatabaseHelper.getInstance(mContext);
mDatabase = mCardDatabaseHelper.getWritableDatabase();
mDatabase = mCardDatabaseHelper.getReadableDatabase();
}
@After
@@ -69,44 +63,10 @@ public class CardDatabaseHelperTest {
CardDatabaseHelper.CardColumns.CATEGORY,
CardDatabaseHelper.CardColumns.PACKAGE_NAME,
CardDatabaseHelper.CardColumns.APP_VERSION,
CardDatabaseHelper.CardColumns.CARD_DISMISSED,
CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP,
};
assertThat(columnNames).isEqualTo(expectedNames);
cursor.close();
}
@Test
public void getContextualCards_shouldSortByScore() {
insertFakeCard(mDatabase, "card1", 1, "uri1");
insertFakeCard(mDatabase, "card2", 0, "uri2");
insertFakeCard(mDatabase, "card3", 10, "uri3");
// Should sort as 3,1,2
try (final Cursor cursor = CardDatabaseHelper.getInstance(mContext).getContextualCards()) {
assertThat(cursor.getCount()).isEqualTo(3);
final List<ContextualCard> cards = new ArrayList<>();
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
cards.add(new ContextualCard(cursor));
}
assertThat(cards.get(0).getName()).isEqualTo("card3");
assertThat(cards.get(1).getName()).isEqualTo("card1");
assertThat(cards.get(2).getName()).isEqualTo("card2");
}
}
private static void insertFakeCard(SQLiteDatabase db, String name, double score, String uri) {
final ContentValues value = new ContentValues();
value.put(CardDatabaseHelper.CardColumns.NAME, name);
value.put(CardDatabaseHelper.CardColumns.SCORE, score);
value.put(CardDatabaseHelper.CardColumns.SLICE_URI, uri);
value.put(CardDatabaseHelper.CardColumns.TYPE, ContextualCard.CardType.SLICE);
value.put(CardDatabaseHelper.CardColumns.CATEGORY,
ContextualCardProto.ContextualCard.Category.DEFAULT.getNumber());
value.put(CardDatabaseHelper.CardColumns.PACKAGE_NAME,
RuntimeEnvironment.application.getPackageName());
value.put(CardDatabaseHelper.CardColumns.APP_VERSION, 1);
db.insert(CardDatabaseHelper.CARD_TABLE, null, value);
}
}

View File

@@ -28,10 +28,14 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Bundle;
import android.util.ArraySet;
@@ -46,6 +50,7 @@ import androidx.slice.widget.SliceLiveData;
import com.android.settings.R;
import com.android.settings.SettingsActivity;
import com.android.settings.applications.AppInfoBase;
import com.android.settings.intelligence.ContextualCardProto;
import org.junit.After;
import org.junit.Before;
@@ -54,6 +59,8 @@ import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@RunWith(RobolectricTestRunner.class)
@@ -62,6 +69,8 @@ public class ContextualCardFeatureProviderImplTest {
private Context mContext;
private ContextualCardFeatureProviderImpl mImpl;
private SharedPreferences mSharedPreferences;
private CardDatabaseHelper mCardDatabaseHelper;
private SQLiteDatabase mDatabase;
@Before
public void setUp() {
@@ -70,13 +79,55 @@ public class ContextualCardFeatureProviderImplTest {
mSharedPreferences = mContext.getSharedPreferences(PREFS, MODE_PRIVATE);
// Set-up specs for SliceMetadata.
SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
mCardDatabaseHelper = CardDatabaseHelper.getInstance(mContext);
mDatabase = mCardDatabaseHelper.getWritableDatabase();
}
@After
public void tearDown() {
CardDatabaseHelper.getInstance(mContext).close();
CardDatabaseHelper.sCardDatabaseHelper = null;
removeInteractedPackageFromSharedPreference();
}
@Test
public void getContextualCards_shouldSortByScore() {
insertFakeCard(mDatabase, "card1", 1, "uri1", 1000L);
insertFakeCard(mDatabase, "card2", 0, "uri2", 1000L);
insertFakeCard(mDatabase, "card3", 10, "uri3", 1000L);
// Should sort as 3,1,2
try (Cursor cursor = mImpl.getContextualCards()) {
assertThat(cursor.getCount()).isEqualTo(3);
final List<ContextualCard> cards = new ArrayList<>();
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
cards.add(new ContextualCard(cursor));
}
assertThat(cards.get(0).getName()).isEqualTo("card3");
assertThat(cards.get(1).getName()).isEqualTo("card1");
assertThat(cards.get(2).getName()).isEqualTo("card2");
}
}
@Test
public void resetDismissedTime_durationExpired_shouldResetToNull() {
insertFakeCard(mDatabase, "card1", 1, "uri1", 100L);
final long threshold = 1000L;
final int rowsUpdated = mImpl.resetDismissedTime(threshold);
assertThat(rowsUpdated).isEqualTo(1);
}
@Test
public void resetDismissedTime_durationNotExpired_shouldNotUpdate() {
insertFakeCard(mDatabase, "card1", 1, "uri1", 1111L);
final long threshold = 1000L;
final int rowsUpdated = mImpl.resetDismissedTime(threshold);
assertThat(rowsUpdated).isEqualTo(0);
}
@Test
public void logNotificationPackage_isContextualNotificationChannel_shouldLogPackage() {
final String packageName = "com.android.test.app";
@@ -101,6 +152,28 @@ public class ContextualCardFeatureProviderImplTest {
assertThat(interactedPackages.contains(packageName)).isFalse();
}
private static void insertFakeCard(
SQLiteDatabase db, String name, double score, String uri, @Nullable Long time) {
final ContentValues value = new ContentValues();
value.put(CardDatabaseHelper.CardColumns.NAME, name);
value.put(CardDatabaseHelper.CardColumns.SCORE, score);
value.put(CardDatabaseHelper.CardColumns.SLICE_URI, uri);
value.put(CardDatabaseHelper.CardColumns.TYPE, ContextualCard.CardType.SLICE);
value.put(CardDatabaseHelper.CardColumns.CATEGORY,
ContextualCardProto.ContextualCard.Category.DEFAULT.getNumber());
value.put(CardDatabaseHelper.CardColumns.PACKAGE_NAME,
RuntimeEnvironment.application.getPackageName());
value.put(CardDatabaseHelper.CardColumns.APP_VERSION, 1);
if (time == null) {
value.putNull(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP);
} else {
value.put(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP, time);
}
db.insert(CardDatabaseHelper.CARD_TABLE, null, value);
}
private Slice buildSlice(Uri sliceUri, String packageName) {
final Bundle args = new Bundle();
args.putString(AppInfoBase.ARG_PACKAGE_NAME, packageName);

View File

@@ -38,10 +38,13 @@ import com.android.settings.R;
import com.android.settings.homepage.contextualcards.CardContentProvider;
import com.android.settings.homepage.contextualcards.CardDatabaseHelper;
import com.android.settings.homepage.contextualcards.ContextualCard;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProvider;
import com.android.settings.homepage.contextualcards.ContextualCardFeatureProviderImpl;
import com.android.settings.homepage.contextualcards.ContextualCardFeedbackDialog;
import com.android.settings.homepage.contextualcards.ContextualCardsFragment;
import com.android.settings.testutils.FakeFeatureFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -78,10 +81,18 @@ public class SliceContextualCardControllerTest {
mResolver = mContext.getContentResolver();
mController = spy(new SliceContextualCardController(mContext));
mFeatureFactory = FakeFeatureFactory.setupForTest();
final ContextualCardFeatureProvider cardFeatureProvider =
new ContextualCardFeatureProviderImpl(mContext);
mFeatureFactory.mContextualCardFeatureProvider = cardFeatureProvider;
}
@After
public void tearDown() {
CardDatabaseHelper.getInstance(mContext).close();
}
@Test
public void onDismissed_cardShouldBeMarkedAsDismissed() {
public void onDismissed_cardShouldBeMarkedAsDismissedWithTimestamp() {
final Uri providerUri = CardContentProvider.REFRESH_CARD_URI;
mResolver.insert(providerUri, generateOneRow());
doNothing().when(mController).showFeedbackDialog(any(ContextualCard.class));
@@ -89,15 +100,15 @@ public class SliceContextualCardControllerTest {
final ContextualCard card = getTestSliceCard();
mController.onDismissed(card);
final String[] columns = {CardDatabaseHelper.CardColumns.CARD_DISMISSED};
final String[] columns = {CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP};
final String selection = CardDatabaseHelper.CardColumns.NAME + "=?";
final String[] selectionArgs = {TEST_CARD_NAME};
final Cursor cr = mResolver.query(providerUri, columns, selection, selectionArgs, null);
cr.moveToFirst();
final int qryDismissed = cr.getInt(0);
final long dismissedTimestamp = cr.getLong(0);
cr.close();
assertThat(qryDismissed).isEqualTo(1);
assertThat(dismissedTimestamp).isNotEqualTo(0L);
verify(mFeatureFactory.metricsFeatureProvider).action(any(),
eq(SettingsEnums.ACTION_CONTEXTUAL_CARD_DISMISS), any(String.class));
}
@@ -172,7 +183,7 @@ public class SliceContextualCardControllerTest {
values.put(CardDatabaseHelper.CardColumns.CATEGORY, 2);
values.put(CardDatabaseHelper.CardColumns.PACKAGE_NAME, "com.android.settings");
values.put(CardDatabaseHelper.CardColumns.APP_VERSION, 10001);
values.put(CardDatabaseHelper.CardColumns.CARD_DISMISSED, 0);
values.put(CardDatabaseHelper.CardColumns.DISMISSED_TIMESTAMP, 0L);
return values;
}

View File

@@ -66,7 +66,6 @@ public class FakeFeatureFactory extends FeatureFactory {
public final UserFeatureProvider userFeatureProvider;
public final AssistGestureFeatureProvider assistGestureFeatureProvider;
public final AccountFeatureProvider mAccountFeatureProvider;
public final ContextualCardFeatureProvider mContextualCardFeatureProvider;
public final BluetoothFeatureProvider mBluetoothFeatureProvider;
public final AwareFeatureProvider mAwareFeatureProvider;
public final FaceFeatureProvider mFaceFeatureProvider;
@@ -74,6 +73,7 @@ public class FakeFeatureFactory extends FeatureFactory {
public PanelFeatureProvider panelFeatureProvider;
public SlicesFeatureProvider slicesFeatureProvider;
public SearchFeatureProvider searchFeatureProvider;
public ContextualCardFeatureProvider mContextualCardFeatureProvider;
/**
* Call this in {@code @Before} method of the test class to use fake factory.