Implement new dismissal behaviour of contextual card

Currently, if a contextual card gets dismissed, it will be gone forever.
After this change, all cards being dismissed can be shown again after a
certain amount of time(e.g one day). In order to calculate the amount of
time, CARD_DISMISSED column is replaced with DISMISSED_TIMESTAMP. Once a
card gets dismissed, a timestamp will be recorded for a corresponding
card.

In this change, some methods are moved from CardDatabaseHelper to
ContextualCardFeatureProvider. So OEMs could replace the providers with
their own ones to get cards and have different dismissal behaviours.

Bug: 143055685
Test: rototests
Change-Id: I00ace98991cabcbfcae4fc47a44e9448683d680c
This commit is contained in:
Yi-Ling Chuang
2020-02-18 09:16:10 +08:00
parent baf12eeb6d
commit deea015b65
10 changed files with 193 additions and 105 deletions

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.