blob: 54df8b7c7d1a3f7d3f661c498f56fc98473e213f [file] [log] [blame]
/*
* Copyright (C) 2013 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.cts.verifier.notifications;
import static android.app.Notification.VISIBILITY_PRIVATE;
import static android.app.NotificationManager.IMPORTANCE_LOW;
import static android.app.NotificationManager.IMPORTANCE_MAX;
import static android.app.NotificationManager.IMPORTANCE_NONE;
import static android.provider.Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS;
import static android.provider.Settings.EXTRA_APP_PACKAGE;
import static android.provider.Settings.EXTRA_CHANNEL_ID;
import static com.android.cts.verifier.notifications.MockListener.JSON_FLAGS;
import static com.android.cts.verifier.notifications.MockListener.JSON_ICON;
import static com.android.cts.verifier.notifications.MockListener.JSON_ID;
import static com.android.cts.verifier.notifications.MockListener.JSON_LAST_AUDIBLY_ALERTED;
import static com.android.cts.verifier.notifications.MockListener.JSON_PACKAGE;
import static com.android.cts.verifier.notifications.MockListener.JSON_REASON;
import static com.android.cts.verifier.notifications.MockListener.JSON_STATS;
import static com.android.cts.verifier.notifications.MockListener.JSON_TAG;
import static com.android.cts.verifier.notifications.MockListener.JSON_WHEN;
import static com.android.cts.verifier.notifications.MockListener.REASON_LISTENER_CANCEL;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.PendingIntent;
import android.app.Person;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.SystemClock;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.RemoteViews;
import androidx.core.app.NotificationCompat;
import com.android.cts.verifier.R;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public class NotificationListenerVerifierActivity extends InteractiveVerifierActivity
implements Runnable {
static final String TAG = "NoListenerVerifier";
private static final String NOTIFICATION_CHANNEL_ID = TAG;
private static final String NOISY_NOTIFICATION_CHANNEL_ID = TAG + "noisy";
protected static final String PREFS = "listener_prefs";
final int NUM_NOTIFICATIONS_SENT = 3; // # notifications sent by sendNotifications()
private String mTag1;
private String mTag2;
private String mTag3;
private String mTag4;
private int mIcon1;
private int mIcon2;
private int mIcon3;
private int mIcon4;
private int mId1;
private int mId2;
private int mId3;
private int mId4;
private long mWhen1;
private long mWhen2;
private long mWhen3;
private long mWhen4;
private int mFlag1;
private int mFlag2;
private int mFlag3;
@Override
protected int getTitleResource() {
return R.string.nls_test;
}
@Override
protected int getInstructionsResource() {
return R.string.nls_info;
}
// Test Setup
@Override
protected List<InteractiveTestCase> createTestItems() {
boolean isAutomotive = getPackageManager().hasSystemFeature(
PackageManager.FEATURE_AUTOMOTIVE);
List<InteractiveTestCase> tests = new ArrayList<>(17);
tests.add(new IsEnabledTest());
tests.add(new ServiceStartedTest());
tests.add(new NotificationReceivedTest());
/*
// TODO (b/200701618): re-enable tests if conditions in 3.8.3.1 change to MUST
if (!isAutomotive) {
tests.add(new SendUserToChangeFilter());
tests.add(new AskIfFilterChanged());
tests.add(new NotificationTypeFilterTest());
tests.add(new ResetChangeFilter());
}*/
tests.add(new LongMessageTest());
tests.add(new DataIntactTest());
tests.add(new AudiblyAlertedTest());
tests.add(new DismissOneTest());
tests.add(new DismissOneWithReasonTest());
tests.add(new DismissOneWithStatsTest());
tests.add(new DismissAllTest());
tests.add(new SnoozeNotificationForTimeTest());
tests.add(new SnoozeNotificationForTimeCancelTest());
tests.add(new GetSnoozedNotificationTest());
tests.add(new EnableHintsTest());
tests.add(new ReceiveAppBlockNoticeTest());
tests.add(new ReceiveAppUnblockNoticeTest());
if (!isAutomotive) {
tests.add(new ReceiveChannelBlockNoticeTest());
tests.add(new ReceiveGroupBlockNoticeTest());
}
tests.add(new RequestUnbindTest());
tests.add(new RequestBindTest());
tests.add(new MessageBundleTest());
tests.add(new ConversationOrderingTest());
tests.add(new HunDisplayTest());
tests.add(new EnableHintsTest());
tests.add(new IsDisabledTest());
tests.add(new ServiceStoppedTest());
tests.add(new NotificationNotReceivedTest());
return tests;
}
private void createChannels() {
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_ID, IMPORTANCE_LOW);
NotificationChannel noisyChannel = new NotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID,
NOISY_NOTIFICATION_CHANNEL_ID, IMPORTANCE_MAX);
noisyChannel.setVibrationPattern(new long[]{100, 0, 100});
mNm.createNotificationChannel(channel);
mNm.createNotificationChannel(noisyChannel);
}
private void deleteChannels() {
mNm.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID);
mNm.deleteNotificationChannel(NOISY_NOTIFICATION_CHANNEL_ID);
}
@SuppressLint("NewApi")
private void sendNotifications() {
mTag1 = UUID.randomUUID().toString();
Log.d(TAG, "Sending #1: " + mTag1);
mTag2 = UUID.randomUUID().toString();
Log.d(TAG, "Sending #2: " + mTag2);
mTag3 = UUID.randomUUID().toString();
Log.d(TAG, "Sending #3: " + mTag3);
mWhen1 = System.currentTimeMillis() + 1;
mWhen2 = System.currentTimeMillis() + 2;
mWhen3 = System.currentTimeMillis() + 3;
mIcon1 = R.drawable.ic_stat_alice;
mIcon2 = R.drawable.ic_stat_bob;
mIcon3 = R.drawable.ic_stat_charlie;
mId1 = NOTIFICATION_ID + 1;
mId2 = NOTIFICATION_ID + 2;
mId3 = NOTIFICATION_ID + 3;
mPackageString = "com.android.cts.verifier";
Notification n1 = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle("ClearTest 1")
.setContentText(mTag1)
.setSmallIcon(mIcon1)
.setWhen(mWhen1)
.setDeleteIntent(makeIntent(1, mTag1))
.setOnlyAlertOnce(true)
.build();
mNm.notify(mTag1, mId1, n1);
mFlag1 = Notification.FLAG_ONLY_ALERT_ONCE;
Notification n2 = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle("ClearTest 2")
.setContentText(mTag2)
.setSmallIcon(mIcon2)
.setWhen(mWhen2)
.setDeleteIntent(makeIntent(2, mTag2))
.setAutoCancel(true)
.build();
mNm.notify(mTag2, mId2, n2);
mFlag2 = Notification.FLAG_AUTO_CANCEL;
Notification n3 = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle("ClearTest 3")
.setContentText(mTag3)
.setSmallIcon(mIcon3)
.setWhen(mWhen3)
.setDeleteIntent(makeIntent(3, mTag3))
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.build();
mNm.notify(mTag3, mId3, n3);
mFlag3 = Notification.FLAG_ONLY_ALERT_ONCE | Notification.FLAG_AUTO_CANCEL;
}
private void sendNoisyNotification() {
mTag4 = UUID.randomUUID().toString();
Log.d(TAG, "Sending noisy notif: " + mTag4);
mWhen4 = System.currentTimeMillis() + 4;
mIcon4 = R.drawable.ic_stat_charlie;
mId4 = NOTIFICATION_ID + 4;
mPackageString = "com.android.cts.verifier";
Notification n1 = new Notification.Builder(mContext, NOISY_NOTIFICATION_CHANNEL_ID)
.setContentTitle("NoisyTest 1")
.setContentText(mTag4)
.setSmallIcon(mIcon4)
.setWhen(mWhen4)
.setDeleteIntent(makeIntent(4, mTag4))
.setCategory(Notification.CATEGORY_REMINDER)
.build();
mNm.notify(mTag4, mId4, n1);
}
// Tests
private class NotificationReceivedTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_note_received);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void tearDown() {
mNm.cancelAll();
MockListener.getInstance().resetData();
deleteChannels();
}
@Override
protected void test() {
if (MockListener.getInstance().getPosted(mTag1) != null) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
}
private class LongMessageTest extends InteractiveTestCase {
private ViewGroup mParent;
@Override
protected View inflate(ViewGroup parent) {
mParent = createAutoItem(parent, R.string.nls_anr);
return mParent;
}
@Override
protected void setUp() {
createChannels();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 20000; i++) {
sb.append("\u2009\u200a" + "\u200E\u200F" + "stuff");
}
Notification.Builder builder = new Notification.Builder(
mContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_stat_alice)
.setContentTitle("This is an long notification")
.setContentText("Innocuous content")
.setStyle(new Notification.MessagingStyle("Fake person")
.addMessage("hey how is it goin", 0, "Person 1")
.addMessage("hey", 0, "Person 1")
.addMessage("u there", 0, "Person 1")
.addMessage("how you like tHIS", 0, "Person 1")
.addMessage(sb.toString(), 0, "Person 1")
);
mTag1 = UUID.randomUUID().toString();
mId1 = NOTIFICATION_ID + 1;
mPackageString = "com.android.cts.verifier";
mNm.notify(mTag1, mId1, builder.build());
status = READY;
}
@Override
protected void tearDown() {
mNm.cancelAll();
MockListener.getInstance().resetData();
deleteChannels();
}
@Override
protected void test() {
StatusBarNotification sbn = MockListener.getInstance().getPosted(mTag1);
if (sbn == null) {
logFail();
status = FAIL;
} else {
ViewGroup parent = mParent.findViewById(R.id.feedback);
parent.setVisibility(View.VISIBLE);
final Notification.Builder recoveredBuilder = Notification.Builder.recoverBuilder(
NotificationListenerVerifierActivity.this,
sbn.getNotification());
RemoteViews rv = recoveredBuilder.createContentView();
View v = rv.apply(NotificationListenerVerifierActivity.this, parent);
parent.addView(v);
}
if (MockListener.getInstance().getPosted(mTag1) != null) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
}
/**
* Creates a notification channel. Sends the user to settings to block the channel. Waits
* to receive the broadcast that the channel was blocked, and confirms that the broadcast
* contains the correct extras.
*/
protected class ReceiveChannelBlockNoticeTest extends InteractiveTestCase {
private String mChannelId;
private int mRetries = 2;
private View mView;
@Override
protected View inflate(ViewGroup parent) {
mView = createNlsSettingsItem(parent, R.string.nls_block_channel);
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(false);
return mView;
}
@Override
protected void setUp() {
mChannelId = UUID.randomUUID().toString();
NotificationChannel channel = new NotificationChannel(
mChannelId, "ReceiveChannelBlockNoticeTest", IMPORTANCE_LOW);
mNm.createNotificationChannel(channel);
status = READY;
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(true);
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
NotificationChannel channel = mNm.getNotificationChannel(mChannelId);
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
if (channel.getImportance() == IMPORTANCE_NONE) {
if (prefs.contains(mChannelId) && prefs.getBoolean(mChannelId, false)) {
status = PASS;
} else {
if (mRetries > 0) {
mRetries--;
status = RETEST;
} else {
status = FAIL;
}
}
} else {
// user hasn't jumped to settings to block the channel yet
status = WAIT_FOR_USER;
}
next();
}
protected void tearDown() {
MockListener.getInstance().resetData();
mNm.deleteNotificationChannel(mChannelId);
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.remove(mChannelId);
}
@Override
protected Intent getIntent() {
return new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
.putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName())
.putExtra(EXTRA_CHANNEL_ID, mChannelId);
}
}
/**
* Creates a notification channel group. Sends the user to settings to block the group. Waits
* to receive the broadcast that the group was blocked, and confirms that the broadcast contains
* the correct extras.
*/
protected class ReceiveGroupBlockNoticeTest extends InteractiveTestCase {
private String mGroupId;
private int mRetries = 2;
private View mView;
@Override
protected View inflate(ViewGroup parent) {
mView = createNlsSettingsItem(parent, R.string.nls_block_group);
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(false);
return mView;
}
@Override
protected void setUp() {
mGroupId = UUID.randomUUID().toString();
NotificationChannelGroup group
= new NotificationChannelGroup(mGroupId, "ReceiveChannelGroupBlockNoticeTest");
mNm.createNotificationChannelGroup(group);
NotificationChannel channel = new NotificationChannel(
mGroupId, "ReceiveChannelBlockNoticeTest", IMPORTANCE_LOW);
channel.setGroup(mGroupId);
mNm.createNotificationChannel(channel);
status = READY;
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(true);
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
NotificationChannelGroup group = mNm.getNotificationChannelGroup(mGroupId);
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
if (group.isBlocked()) {
if (prefs.contains(mGroupId) && prefs.getBoolean(mGroupId, false)) {
status = PASS;
} else {
if (mRetries > 0) {
mRetries--;
status = RETEST;
} else {
status = FAIL;
}
}
} else {
// user hasn't jumped to settings to block the group yet
status = WAIT_FOR_USER;
}
next();
}
protected void tearDown() {
MockListener.getInstance().resetData();
mNm.deleteNotificationChannelGroup(mGroupId);
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.remove(mGroupId);
}
@Override
protected Intent getIntent() {
return new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName());
}
}
/**
* Sends the user to settings to block the app. Waits to receive the broadcast that the app was
* blocked, and confirms that the broadcast contains the correct extras.
*/
protected class ReceiveAppBlockNoticeTest extends InteractiveTestCase {
private int mRetries = 2;
private View mView;
@Override
protected View inflate(ViewGroup parent) {
mView = createNlsSettingsItem(parent, R.string.nls_block_app);
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(false);
return mView;
}
@Override
protected void setUp() {
status = READY;
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(true);
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
if (!mNm.areNotificationsEnabled()) {
Log.d(TAG, "Got broadcast " + prefs.contains(mContext.getPackageName()));
Log.d(TAG, "Broadcast contains correct data? " +
prefs.getBoolean(mContext.getPackageName(), false));
if (prefs.contains(mContext.getPackageName())
&& prefs.getBoolean(mContext.getPackageName(), false)) {
status = PASS;
} else {
if (mRetries > 0) {
mRetries--;
status = RETEST;
} else {
status = FAIL;
}
}
} else {
Log.d(TAG, "Notifications still enabled");
// user hasn't jumped to settings to block the app yet
status = WAIT_FOR_USER;
}
next();
}
protected void tearDown() {
MockListener.getInstance().resetData();
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.remove(mContext.getPackageName());
}
@Override
protected Intent getIntent() {
return new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName());
}
}
/**
* Sends the user to settings to unblock the app. Waits to receive the broadcast that the app
* was unblocked, and confirms that the broadcast contains the correct extras.
*/
protected class ReceiveAppUnblockNoticeTest extends InteractiveTestCase {
private int mRetries = 2;
private View mView;
@Override
protected View inflate(ViewGroup parent) {
mView = createNlsSettingsItem(parent, R.string.nls_unblock_app);
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(false);
return mView;
}
@Override
protected void setUp() {
status = READY;
Button button = mView.findViewById(R.id.nls_action_button);
button.setEnabled(true);
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
if (mNm.areNotificationsEnabled()) {
if (prefs.contains(mContext.getPackageName())
&& !prefs.getBoolean(mContext.getPackageName(), true)) {
status = PASS;
} else {
if (mRetries > 0) {
mRetries--;
status = RETEST;
} else {
status = FAIL;
}
}
} else {
// user hasn't jumped to settings to block the app yet
status = WAIT_FOR_USER;
}
next();
}
protected void tearDown() {
MockListener.getInstance().resetData();
SharedPreferences prefs = mContext.getSharedPreferences(
NotificationListenerVerifierActivity.PREFS, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.remove(mContext.getPackageName());
}
@Override
protected Intent getIntent() {
return new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.putExtra(EXTRA_APP_PACKAGE, mContext.getPackageName());
}
}
private class DataIntactTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_payload_intact);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void test() {
List<JSONObject> result = new ArrayList<>(MockListener.getInstance().getPosted());
Set<String> found = new HashSet<String>();
if (result.size() == 0) {
status = FAIL;
return;
}
boolean pass = true;
for (JSONObject payload : result) {
try {
pass &= checkEquals(mPackageString,
payload.getString(JSON_PACKAGE),
"data integrity test: notification package (%s, %s)");
String tag = payload.getString(JSON_TAG);
if (mTag1.equals(tag)) {
found.add(mTag1);
pass &= checkEquals(mIcon1, payload.getInt(JSON_ICON),
"data integrity test: notification icon (%d, %d)");
pass &= checkFlagSet(mFlag1, payload.getInt(JSON_FLAGS),
"data integrity test: notification flags (%d, %d)");
pass &= checkEquals(mId1, payload.getInt(JSON_ID),
"data integrity test: notification ID (%d, %d)");
pass &= checkEquals(mWhen1, payload.getLong(JSON_WHEN),
"data integrity test: notification when (%d, %d)");
} else if (mTag2.equals(tag)) {
found.add(mTag2);
pass &= checkEquals(mIcon2, payload.getInt(JSON_ICON),
"data integrity test: notification icon (%d, %d)");
pass &= checkFlagSet(mFlag2, payload.getInt(JSON_FLAGS),
"data integrity test: notification flags (%d, %d)");
pass &= checkEquals(mId2, payload.getInt(JSON_ID),
"data integrity test: notification ID (%d, %d)");
pass &= checkEquals(mWhen2, payload.getLong(JSON_WHEN),
"data integrity test: notification when (%d, %d)");
} else if (mTag3.equals(tag)) {
found.add(mTag3);
pass &= checkEquals(mIcon3, payload.getInt(JSON_ICON),
"data integrity test: notification icon (%d, %d)");
pass &= checkFlagSet(mFlag3, payload.getInt(JSON_FLAGS),
"data integrity test: notification flags (%d, %d)");
pass &= checkEquals(mId3, payload.getInt(JSON_ID),
"data integrity test: notification ID (%d, %d)");
pass &= checkEquals(mWhen3, payload.getLong(JSON_WHEN),
"data integrity test: notification when (%d, %d)");
}
} catch (JSONException e) {
pass = false;
Log.e(TAG, "failed to unpack data from mocklistener", e);
}
}
pass &= found.size() >= 3;
status = pass ? PASS : FAIL;
}
@Override
protected void tearDown() {
mNm.cancelAll();
MockListener.getInstance().resetData();
deleteChannels();
}
}
private class AudiblyAlertedTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_audibly_alerted);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
sendNoisyNotification();
status = READY;
}
@Override
protected void test() {
List<JSONObject> result = new ArrayList<>(MockListener.getInstance().getPosted());
Set<String> found = new HashSet<>();
if (result.size() == 0) {
status = FAIL;
return;
}
boolean pass = true;
for (JSONObject payload : result) {
try {
String tag = payload.getString(JSON_TAG);
if (mTag4.equals(tag)) {
found.add(mTag4);
boolean lastAudiblyAlertedSet
= payload.getLong(JSON_LAST_AUDIBLY_ALERTED) > -1;
if (!lastAudiblyAlertedSet) {
logWithStack(
"noisy notification test: getLastAudiblyAlertedMillis not set");
}
pass &= lastAudiblyAlertedSet;
} else if (payload.getString(JSON_PACKAGE).equals(mPackageString)) {
found.add(tag);
boolean lastAudiblyAlertedSet
= payload.getLong(JSON_LAST_AUDIBLY_ALERTED) > 0;
if (lastAudiblyAlertedSet) {
logWithStack(
"noisy notification test: getLastAudiblyAlertedMillis set "
+ "incorrectly");
}
pass &= !lastAudiblyAlertedSet;
}
} catch (JSONException e) {
pass = false;
Log.e(TAG, "failed to unpack data from mocklistener", e);
}
}
pass &= found.size() >= 4;
status = pass ? PASS : FAIL;
}
@Override
protected void tearDown() {
mNm.cancelAll();
MockListener.getInstance().resetData();
deleteChannels();
}
}
private class DismissOneTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_clear_one);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void test() {
if (status == READY) {
MockListener.getInstance().cancelNotification(
MockListener.getInstance().getKeyForTag(mTag1));
status = RETEST;
} else {
List<String> result = new ArrayList<>(MockListener.getInstance().mRemoved);
if (result.size() != 0
&& result.contains(mTag1)
&& !result.contains(mTag2)
&& !result.contains(mTag3)) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
MockListener.getInstance().resetData();
}
}
private class DismissOneWithReasonTest extends InteractiveTestCase {
int mRetries = 3;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_clear_one_reason);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void test() {
if (status == READY) {
MockListener.getInstance().cancelNotification(
MockListener.getInstance().getKeyForTag(mTag1));
status = RETEST;
} else {
List<JSONObject> result =
new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
boolean pass = false;
for (JSONObject payload : result) {
try {
pass |= (checkEquals(mTag1,
payload.getString(JSON_TAG),
"data dismissal test: notification tag (%s, %s)")
&& checkEquals(REASON_LISTENER_CANCEL,
payload.getInt(JSON_REASON),
"data dismissal test: reason (%d, %d)"));
if(pass) {
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (pass) {
status = PASS;
} else {
if (--mRetries > 0) {
sleep(100);
status = RETEST;
} else {
status = FAIL;
}
}
}
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
MockListener.getInstance().resetData();
}
}
private class DismissOneWithStatsTest extends InteractiveTestCase {
int mRetries = 3;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_clear_one_stats);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void test() {
if (status == READY) {
MockListener.getInstance().cancelNotification(
MockListener.getInstance().getKeyForTag(mTag1));
status = RETEST;
} else {
List<JSONObject> result =
new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
boolean pass = true;
for (JSONObject payload : result) {
try {
pass &= (payload.getBoolean(JSON_STATS) == false);
} catch (JSONException e) {
e.printStackTrace();
pass = false;
}
}
if (pass) {
status = PASS;
} else {
if (--mRetries > 0) {
sleep(100);
status = RETEST;
} else {
logFail("Notification listener got populated stats object.");
status = FAIL;
}
}
}
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
MockListener.getInstance().resetData();
}
}
private class DismissAllTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_clear_all);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void test() {
if (status == READY) {
MockListener.getInstance().cancelAllNotifications();
status = RETEST;
} else {
List<String> result = new ArrayList<>(MockListener.getInstance().mRemoved);
if (result.size() != 0
&& result.contains(mTag1)
&& result.contains(mTag2)
&& result.contains(mTag3)) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
MockListener.getInstance().resetData();
}
}
private class IsDisabledTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createNlsSettingsItem(parent, R.string.nls_disable_service);
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
String listeners = Secure.getString(getContentResolver(),
ENABLED_NOTIFICATION_LISTENERS);
if (listeners == null || !listeners.contains(LISTENER_PATH)) {
status = PASS;
} else {
status = WAIT_FOR_USER;
}
}
@Override
protected void tearDown() {
MockListener.getInstance().resetData();
}
@Override
protected Intent getIntent() {
return new Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS);
}
}
private class ServiceStoppedTest extends InteractiveTestCase {
int mRetries = 3;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_service_stopped);
}
@Override
protected void test() {
if (mNm.getEffectsSuppressor() == null && (MockListener.getInstance() == null
|| !MockListener.getInstance().isConnected)) {
status = PASS;
} else {
if (--mRetries > 0) {
sleep(100);
status = RETEST;
} else {
status = FAIL;
}
}
}
@Override
protected Intent getIntent() {
return new Intent(ACTION_NOTIFICATION_LISTENER_SETTINGS);
}
}
private class NotificationNotReceivedTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_note_missed);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void test() {
if (MockListener.getInstance() == null) {
status = PASS;
} else {
if (MockListener.getInstance().mPosted.size() == 0) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
next();
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
if (MockListener.getInstance() != null) {
MockListener.getInstance().resetData();
}
}
}
private class RequestUnbindTest extends InteractiveTestCase {
int mRetries = 5;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_snooze);
}
@Override
protected void setUp() {
status = READY;
MockListener.getInstance().requestListenerHints(
MockListener.HINT_HOST_DISABLE_CALL_EFFECTS);
}
@Override
protected void test() {
if (status == READY) {
MockListener.getInstance().requestUnbind();
status = RETEST;
} else {
if (mNm.getEffectsSuppressor() == null && !MockListener.getInstance().isConnected) {
status = PASS;
} else {
if (--mRetries > 0) {
status = RETEST;
} else {
logFail();
status = FAIL;
}
}
next();
}
}
}
private class RequestBindTest extends InteractiveTestCase {
int mRetries = 5;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_unsnooze);
}
@Override
protected void test() {
if (status == READY) {
MockListener.requestRebind(MockListener.COMPONENT_NAME);
status = RETEST;
} else {
if (MockListener.getInstance().isConnected) {
status = PASS;
next();
} else {
if (--mRetries > 0) {
status = RETEST;
next();
} else {
logFail();
status = FAIL;
}
}
}
}
}
private class EnableHintsTest extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_hints);
}
@Override
protected void test() {
if (status == READY) {
MockListener.getInstance().requestListenerHints(
MockListener.HINT_HOST_DISABLE_CALL_EFFECTS);
status = RETEST;
} else {
int result = MockListener.getInstance().getCurrentListenerHints();
if ((result & MockListener.HINT_HOST_DISABLE_CALL_EFFECTS)
== MockListener.HINT_HOST_DISABLE_CALL_EFFECTS) {
status = PASS;
next();
} else {
logFail();
status = FAIL;
}
}
}
}
private class SnoozeNotificationForTimeTest extends InteractiveTestCase {
final static int READY_TO_SNOOZE = 0;
final static int SNOOZED = 1;
final static int READY_TO_CHECK_FOR_UNSNOOZE = 2;
int state = -1;
long snoozeTime = 3000;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_snooze_one_time);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
state = READY_TO_SNOOZE;
delay();
}
@Override
protected void test() {
status = RETEST;
if (state == READY_TO_SNOOZE) {
MockListener.getInstance().snoozeNotification(
MockListener.getInstance().getKeyForTag(mTag1), snoozeTime);
state = SNOOZED;
} else if (state == SNOOZED) {
List<JSONObject> result =
new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
boolean pass = false;
for (JSONObject payload : result) {
try {
pass |= (checkEquals(mTag1,
payload.getString(JSON_TAG),
"data dismissal test: notification tag (%s, %s)")
&& checkEquals(MockListener.REASON_SNOOZED,
payload.getInt(JSON_REASON),
"data dismissal test: reason (%d, %d)"));
if (pass) {
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (!pass) {
logFail();
status = FAIL;
next();
return;
} else {
state = READY_TO_CHECK_FOR_UNSNOOZE;
}
} else {
if (MockListener.getInstance().getPosted(mTag1) != null) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
MockListener.getInstance().resetData();
delay();
}
}
/**
* Posts notifications, snoozes one of them. Verifies that it is snoozed. Cancels all
* notifications and reposts them. Confirms that the original notification is still snoozed.
*/
private class SnoozeNotificationForTimeCancelTest extends InteractiveTestCase {
final static int READY_TO_SNOOZE = 0;
final static int SNOOZED = 1;
final static int READY_TO_CHECK_FOR_SNOOZE = 2;
int state = -1;
long snoozeTime = 10000;
private String tag;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_snooze_one_time);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
tag = mTag1;
status = READY;
state = READY_TO_SNOOZE;
delay();
}
@Override
protected void test() {
status = RETEST;
if (state == READY_TO_SNOOZE) {
MockListener.getInstance().snoozeNotification(
MockListener.getInstance().getKeyForTag(tag), snoozeTime);
state = SNOOZED;
} else if (state == SNOOZED) {
List<String> result = getSnoozed();
if (result.size() >= 1
&& result.contains(tag)) {
// cancel and repost
sendNotifications();
state = READY_TO_CHECK_FOR_SNOOZE;
} else {
logFail();
status = FAIL;
}
} else {
List<String> result = getSnoozed();
if (result.size() >= 1
&& result.contains(tag)) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
}
private List<String> getSnoozed() {
List<String> result = new ArrayList<>();
StatusBarNotification[] snoozed = MockListener.getInstance().getSnoozedNotifications();
for (StatusBarNotification sbn : snoozed) {
result.add(sbn.getTag());
}
return result;
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
MockListener.getInstance().resetData();
}
}
private class GetSnoozedNotificationTest extends InteractiveTestCase {
final static int READY_TO_SNOOZE = 0;
final static int SNOOZED = 1;
final static int READY_TO_CHECK_FOR_GET_SNOOZE = 2;
int state = -1;
long snoozeTime = 30000;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_get_snoozed);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
state = READY_TO_SNOOZE;
}
@Override
protected void test() {
status = RETEST;
if (state == READY_TO_SNOOZE) {
MockListener.getInstance().snoozeNotification(
MockListener.getInstance().getKeyForTag(mTag1), snoozeTime);
MockListener.getInstance().snoozeNotification(
MockListener.getInstance().getKeyForTag(mTag2), snoozeTime);
state = SNOOZED;
} else if (state == SNOOZED) {
List<JSONObject> result =
new ArrayList<>(MockListener.getInstance().mRemovedReason.values());
if (result.size() == 0) {
status = FAIL;
return;
}
boolean pass = false;
for (JSONObject payload : result) {
try {
pass |= (checkEquals(mTag1,
payload.getString(JSON_TAG),
"data dismissal test: notification tag (%s, %s)")
&& checkEquals(MockListener.REASON_SNOOZED,
payload.getInt(JSON_REASON),
"data dismissal test: reason (%d, %d)"));
if (pass) {
break;
}
} catch (JSONException e) {
e.printStackTrace();
}
}
if (!pass) {
logFail();
status = FAIL;
} else {
state = READY_TO_CHECK_FOR_GET_SNOOZE;
}
} else {
List<String> result = new ArrayList<>();
StatusBarNotification[] snoozed =
MockListener.getInstance().getSnoozedNotifications();
for (StatusBarNotification sbn : snoozed) {
result.add(sbn.getTag());
}
if (result.size() >= 2
&& result.contains(mTag1)
&& result.contains(mTag2)) {
status = PASS;
} else {
logFail();
status = FAIL;
}
}
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
MockListener.getInstance().resetData();
delay();
}
}
/** Tests that the extras {@link Bundle} in a MessagingStyle#Message is preserved. */
private class MessageBundleTest extends InteractiveTestCase {
private final String extrasKey1 = "extras_key_1";
private final CharSequence extrasValue1 = "extras_value_1";
private final String extrasKey2 = "extras_key_2";
private final CharSequence extrasValue2 = "extras_value_2";
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.msg_extras_preserved);
}
@Override
protected void setUp() {
createChannels();
sendMessagingNotification();
status = READY;
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
delay();
}
private void sendMessagingNotification() {
mTag1 = UUID.randomUUID().toString();
mNm.cancelAll();
mWhen1 = System.currentTimeMillis() + 1;
mIcon1 = R.drawable.ic_stat_alice;
mId1 = NOTIFICATION_ID + 1;
Notification.MessagingStyle.Message msg1 =
new Notification.MessagingStyle.Message("text1", 0 /* timestamp */, "sender1");
msg1.getExtras().putCharSequence(extrasKey1, extrasValue1);
Notification.MessagingStyle.Message msg2 =
new Notification.MessagingStyle.Message("text2", 1 /* timestamp */, "sender2");
msg2.getExtras().putCharSequence(extrasKey2, extrasValue2);
Notification.MessagingStyle style = new Notification.MessagingStyle("display_name");
style.addMessage(msg1);
style.addMessage(msg2);
Notification n1 = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle("ClearTest 1")
.setContentText(mTag1.toString())
.setPriority(Notification.PRIORITY_LOW)
.setSmallIcon(mIcon1)
.setWhen(mWhen1)
.setDeleteIntent(makeIntent(1, mTag1))
.setOnlyAlertOnce(true)
.setStyle(style)
.build();
mNm.notify(mTag1, mId1, n1);
mFlag1 = Notification.FLAG_ONLY_ALERT_ONCE;
}
// Returns true on success.
private boolean verifyMessage(
NotificationCompat.MessagingStyle.Message message,
String extrasKey,
CharSequence extrasValue) {
return message.getExtras() != null
&& message.getExtras().getCharSequence(extrasKey) != null
&& message.getExtras().getCharSequence(extrasKey).equals(extrasValue);
}
@Override
protected void test() {
List<Notification> result =
new ArrayList<>(MockListener.getInstance().mPostedNotifications);
if (result.size() != 1 || result.get(0) == null) {
logFail();
status = FAIL;
next();
return;
}
// Can only read in MessagingStyle using the compat class.
NotificationCompat.MessagingStyle readStyle =
NotificationCompat.MessagingStyle
.extractMessagingStyleFromNotification(
result.get(0));
if (readStyle == null || readStyle.getMessages().size() != 2) {
status = FAIL;
logFail();
next();
return;
}
if (!verifyMessage(readStyle.getMessages().get(0), extrasKey1,
extrasValue1)
|| !verifyMessage(
readStyle.getMessages().get(1), extrasKey2, extrasValue2)) {
status = FAIL;
logFail();
next();
return;
}
status = PASS;
}
}
/**
* Tests that conversation notifications appear at the top of the shade, if the device supports
* a separate conversation section
*/
private class ConversationOrderingTest extends InteractiveTestCase {
private static final String SHARE_SHORTCUT_ID = "shareShortcut";
private static final String SHORTCUT_CATEGORY =
"com.android.cts.verifier.notifications.SHORTCUT_CATEGORY";
@Override
protected void setUp() {
createChannels();
createDynamicShortcut();
sendNotifications();
status = READY;
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
delay();
}
@Override
protected View inflate(ViewGroup parent) {
return createPassFailItem(parent, R.string.conversation_section_ordering);
}
private void createDynamicShortcut() {
Person person = new Person.Builder()
.setBot(false)
.setIcon(Icon.createWithResource(mContext, R.drawable.ic_stat_alice))
.setName("Person A")
.setImportant(true)
.build();
Set<String> categorySet = new ArraySet<>();
categorySet.add(SHORTCUT_CATEGORY);
Intent shortcutIntent =
new Intent(mContext, BubbleActivity.class);
shortcutIntent.setAction(Intent.ACTION_VIEW);
ShortcutInfo shortcut = new ShortcutInfo.Builder(mContext, SHARE_SHORTCUT_ID)
.setShortLabel(SHARE_SHORTCUT_ID)
.setIcon(Icon.createWithResource(mContext, R.drawable.ic_stat_alice))
.setIntent(shortcutIntent)
.setPerson(person)
.setCategories(categorySet)
.setLongLived(true)
.build();
ShortcutManager scManager =
(ShortcutManager) mContext.getSystemService(Context.SHORTCUT_SERVICE);
scManager.addDynamicShortcuts(Arrays.asList(shortcut));
}
private void sendNotifications() {
mTag1 = UUID.randomUUID().toString();
mId1 = NOTIFICATION_ID + 1;
Person person = new Person.Builder()
.setName("Person A")
.build();
Notification n1 = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle("ConversationOrderingTest")
.setContentText(mTag1)
.setSmallIcon(R.drawable.ic_stat_alice)
.setShortcutId(SHARE_SHORTCUT_ID)
.setStyle(new Notification.MessagingStyle(person)
.setConversationTitle("Bubble Chat")
.addMessage("Hello?",
SystemClock.currentThreadTimeMillis() - 300000, person)
.addMessage("Is it me you're looking for?",
SystemClock.currentThreadTimeMillis(), person)
)
.build();
mNm.notify(mTag1, mId1, n1);
mTag2 = UUID.randomUUID().toString();
mId2 = mId1 + 1;
Notification n2 = new Notification.Builder(mContext, NOTIFICATION_CHANNEL_ID)
.setContentTitle("Non-Person Notification")
.setContentText(mTag1)
.setSmallIcon(R.drawable.ic_stat_alice)
.build();
mNm.notify(mTag2, mId2, n2);
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
status = WAIT_FOR_USER;
next();
}
}
/**
* Tests that heads-up notifications appear with the view, resources, and actions provided
* in Notification.Builder.
*/
private class HunDisplayTest extends InteractiveTestCase {
@Override
protected void setUp() {
createChannels();
sendNotifications();
status = READY;
}
@Override
protected void tearDown() {
mNm.cancelAll();
deleteChannels();
delay();
}
@Override
protected View inflate(ViewGroup parent) {
return createPassFailItem(parent, R.string.hun_display);
}
private void sendNotifications() {
mTag1 = UUID.randomUUID().toString();
mId1 = NOTIFICATION_ID + 1;
Notification n1 = new Notification.Builder(mContext, NOISY_NOTIFICATION_CHANNEL_ID)
.setContentTitle("HunDisplayTest")
.setContentText(mTag1)
.setSmallIcon(R.drawable.ic_stat_alice)
.setLargeIcon(Icon.createWithResource(mContext, R.drawable.test_pass_gradient))
.addAction(generateAction(1))
.addAction(generateAction(2))
.build();
mNm.notify(mTag1, mId1, n1);
}
private Notification.Action generateAction(int num) {
PendingIntent pi = PendingIntent.getActivity(mContext, num,
new Intent(Settings.ACTION_ALL_APPS_NOTIFICATION_SETTINGS),
PendingIntent.FLAG_IMMUTABLE);
return new Notification.Action.Builder(
Icon.createWithResource(mContext, R.drawable.ic_android),
mContext.getString(R.string.action, num), pi)
.build();
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
status = WAIT_FOR_USER;
next();
}
}
/**
* Sends the user to settings filter out silent notifications for this notification listener.
* Sends silent and not silent notifs and makes sure only the non silent is received
*/
private class NotificationTypeFilterTest extends InteractiveTestCase {
int mRetries = 3;
@Override
protected View inflate(ViewGroup parent) {
return createAutoItem(parent, R.string.nls_filter_test);
}
@Override
protected void setUp() {
createChannels();
sendNotifications();
sendNoisyNotification();
status = READY;
}
@Override
protected void tearDown() {
mNm.cancelAll();
MockListener.getInstance().resetData();
deleteChannels();
}
@Override
protected void test() {
if (MockListener.getInstance().getPosted(mTag4) == null) {
Log.d(TAG, "Could not find " + mTag4);
if (--mRetries > 0) {
sleep(100);
status = RETEST;
} else {
status = FAIL;
}
} else if (MockListener.getInstance().getPosted(mTag2) != null) {
logFail("Found" + mTag2);
status = FAIL;
} else {
status = PASS;
}
}
}
protected class SendUserToChangeFilter extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createUserItem(
parent, R.string.cp_start_settings, R.string.nls_change_type_filter);
}
@Override
protected void setUp() {
// note: it's expected that the '0' type will be ignored since we've specified a
// type in the manifest
ArrayList<String> pkgs = new ArrayList<>();
pkgs.add("com.android.settings");
MockListener.getInstance().migrateNotificationFilter(0, pkgs);
status = READY;
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
if (getIntent().resolveActivity(mPackageManager) == null) {
logFail("no settings activity");
status = FAIL;
} else {
if (buttonPressed) {
status = PASS;
} else {
status = RETEST_AFTER_LONG_DELAY;
}
next();
}
}
protected void tearDown() {
// wait for the service to start
delay();
}
@Override
protected Intent getIntent() {
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS);
intent.putExtra(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME,
MockListener.COMPONENT_NAME.flattenToString());
return intent;
}
}
protected class ResetChangeFilter extends SendUserToChangeFilter {
@Override
protected View inflate(ViewGroup parent) {
return createUserItem(
parent, R.string.cp_start_settings, R.string.nls_reset_type_filter);
}
}
protected class AskIfFilterChanged extends InteractiveTestCase {
@Override
protected View inflate(ViewGroup parent) {
return createPassFailItem(parent, R.string.nls_original_filter_verification);
}
@Override
boolean autoStart() {
return true;
}
@Override
protected void test() {
status = WAIT_FOR_USER;
next();
}
}
}