Cts verifier to prevent force-stop on task removed
When an app task is removed from recents, it should not be forced
stopped without explicit user consent.
Test: Cts Verifier > Other > Recent Task Removal Test
Bug: 128455040
Change-Id: I07dd1d862089eb83fc6949154357b47f019d6488
diff --git a/apps/CtsVerifier/Android.mk b/apps/CtsVerifier/Android.mk
index 50f9d5a..af6dc5b 100644
--- a/apps/CtsVerifier/Android.mk
+++ b/apps/CtsVerifier/Android.mk
@@ -23,7 +23,8 @@
LOCAL_MULTILIB := both
-LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-under, src)
+LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-Iaidl-files-under, src) \
+ ../ForceStopHelperApp/src/com/android/cts/forcestophelper/Constants.java
LOCAL_AIDL_INCLUDES := \
frameworks/native/aidl/gui
@@ -109,6 +110,7 @@
CtsEmptyDeviceAdmin \
CtsEmptyDeviceOwner \
CtsPermissionApp \
+ CtsForceStopHelper \
NotificationBot
# Apps to be installed as Instant App using adb install --instant
diff --git a/apps/CtsVerifier/AndroidManifest.xml b/apps/CtsVerifier/AndroidManifest.xml
index a796faa..cc1bfe6 100644
--- a/apps/CtsVerifier/AndroidManifest.xml
+++ b/apps/CtsVerifier/AndroidManifest.xml
@@ -152,6 +152,16 @@
android:theme="@style/OverlayTheme"
android:label="Overlaying Activity"/>
+ <activity android:name=".forcestop.RecentTaskRemovalTestActivity"
+ android:label="@string/remove_from_recents_test"
+ android:configChanges="keyboardHidden|orientation|screenSize">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.cts.intent.category.MANUAL_TEST" />
+ </intent-filter>
+ <meta-data android:name="test_required_configs" android:value="config_has_recents"/>
+ </activity>
+
<activity android:name=".companion.CompanionDeviceTestActivity"
android:label="@string/companion_test"
android:configChanges="keyboardHidden|orientation|screenSize">
diff --git a/apps/CtsVerifier/res/layout/force_stop_recents_main.xml b/apps/CtsVerifier/res/layout/force_stop_recents_main.xml
new file mode 100644
index 0000000..ef5664e
--- /dev/null
+++ b/apps/CtsVerifier/res/layout/force_stop_recents_main.xml
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/RootLayoutPadding"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Install test app -->
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/fs_test_app_install_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="10dip"
+ android:padding="10dip"/>
+
+ <TextView
+ android:id="@+id/fs_test_app_install_instructions"
+ style="@style/InstructionsSmallFont"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/fs_test_app_install_status"
+ android:layout_marginTop="10dip"
+ android:text="@string/fs_test_app_install_instructions"/>
+ </RelativeLayout>
+
+ <!-- Launch test activity -->
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/fs_test_app_launch_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="10dip"
+ android:padding="10dip"/>
+
+ <TextView
+ android:id="@+id/fs_test_app_launch_instructions"
+ style="@style/InstructionsSmallFont"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/fs_test_app_launch_status"
+ android:layout_marginTop="10dip"
+ android:text="@string/fs_test_app_launch_instructions"/>
+
+ <Button
+ android:id="@+id/fs_launch_test_app_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_below="@id/fs_test_app_launch_instructions"
+ android:layout_marginTop="10dip"
+ android:layout_marginLeft="20dip"
+ android:layout_marginRight="20dip"
+ android:layout_toRightOf="@id/fs_test_app_launch_status"
+ android:text="@string/fs_launch_test_app_button_text"/>
+ </RelativeLayout>
+
+ <!-- Remove test activity task from recents -->
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/fs_test_app_recents_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="10dip"
+ android:padding="10dip"
+ android:visibility="visible"/>
+
+ <TextView
+ android:id="@+id/fs_test_app_recents_instructions"
+ style="@style/InstructionsSmallFont"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/fs_test_app_recents_status"
+ android:layout_marginTop="10dip"
+ android:visibility="visible"
+ android:text="@string/fs_test_app_recents_instructions"/>
+ </RelativeLayout>
+
+ <!-- Verify that app wasn't force-stopped -->
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/fs_force_stop_status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="10dip"
+ android:visibility="gone"
+ android:padding="10dip"/>
+
+ <TextView
+ android:id="@+id/fs_force_stop_verification"
+ style="@style/InstructionsSmallFont"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@id/fs_force_stop_status"
+ android:layout_marginTop="10dip"
+ android:visibility="gone"
+ android:text="@string/fs_force_stop_verification_pending"/>
+ </RelativeLayout>
+ </LinearLayout>
+
+ <include android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ layout="@layout/pass_fail_buttons"/>
+</RelativeLayout>
diff --git a/apps/CtsVerifier/res/values/strings.xml b/apps/CtsVerifier/res/values/strings.xml
index e06e5ad..592e3b3 100755
--- a/apps/CtsVerifier/res/values/strings.xml
+++ b/apps/CtsVerifier/res/values/strings.xml
@@ -62,6 +62,9 @@
<!-- Strings for ReportViewerActivity -->
<string name="report_viewer">Report Viewer</string>
+ <string name="result_success">Test passed!</string>
+ <string name="result_failure">Test failed!</string>
+
<!-- String shared between BackupTestActivity and BackupAccessibilityTestActivity -->
<string name="bu_loading">Loading...</string>
<string name="bu_generate_error">Error occurred while generating test data...</string>
@@ -150,6 +153,24 @@
</string>
<string name="da_tapjacking_button_text">Enable device admin</string>
+ <!-- Strings for RecentTaskRemovalTestActivity -->
+ <string name="remove_from_recents_test">Recent Task Removal Test</string>
+ <string name="remove_from_recents_test_info">
+ This test verifies that an app whose task is removed from recents is not also force-stopped
+ without explicit user consent. This test requires CtsForceStopHelper.apk to be installed.
+ </string>
+ <string name="fs_test_app_install_instructions">Please install the \'Force stop helper app\' on the device.</string>
+ <string name="fs_test_app_installed_text">\'Force stop helper app\' installed on device. Proceed to the following steps.</string>
+ <string name="fs_test_app_launch_instructions">
+ Tap the button to launch the helper activity. Then return to this screen.
+ </string>
+ <string name="fs_launch_test_app_button_text">Launch test activity</string>
+ <string name="fs_test_app_recents_instructions">
+ Open recents and remove the task of the activity started in the previous step and return to this screen.
+ Deny any dialog that is shown asking for permission to force-stop or kill the app.
+ </string>
+ <string name="fs_force_stop_verification_pending">Verifying... Please wait.</string>
+
<!-- Strings for BiometricTest -->
<string name="biometric_test">Biometric Test</string>
<string name="biometric_test_info">
@@ -854,9 +875,6 @@
<string name="nfc_reading_tag">Reading NFC tag...</string>
<string name="nfc_reading_tag_error">Error reading NFC tag...</string>
- <string name="nfc_result_success">Test passed!</string>
- <string name="nfc_result_failure">Test failed!</string>
-
<string name="nfc_result_message">Written data:\n%1$s\n\nRead data:\n%2$s</string>
<string name="nfc_ndef_content">Id: %1$s\nMime: %2$s\nPayload: %3$s</string>
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
index a339a43..47514b8 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/ManifestTestListAdapter.java
@@ -21,6 +21,7 @@
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.util.Log;
@@ -31,7 +32,6 @@
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -104,6 +104,8 @@
private static final String CONFIG_VOICE_CAPABLE = "config_voice_capable";
+ private static final String CONFIG_HAS_RECENTS = "config_has_recents";
+
private final HashSet<String> mDisabledTests;
private Context mContext;
@@ -326,14 +328,23 @@
private boolean matchAllConfigs(String[] configs) {
if (configs != null) {
- TelephonyManager telephonyManager =
- (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
for (String config : configs) {
- switch(config) {
+ switch (config) {
case CONFIG_VOICE_CAPABLE:
+ TelephonyManager telephonyManager = mContext.getSystemService(
+ TelephonyManager.class);
if (!telephonyManager.isVoiceCapable()) {
return false;
}
+ break;
+ case CONFIG_HAS_RECENTS:
+ final Resources systemRes = mContext.getResources().getSystem();
+ final int id = systemRes.getIdentifier("config_hasRecents", "bool",
+ "android");
+ if (id == Resources.ID_NULL || !systemRes.getBoolean(id)) {
+ return false;
+ }
+ break;
default:
break;
}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/forcestop/RecentTaskRemovalTestActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/forcestop/RecentTaskRemovalTestActivity.java
new file mode 100644
index 0000000..0718d41
--- /dev/null
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/forcestop/RecentTaskRemovalTestActivity.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2019 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.forcestop;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.cts.forcestophelper.Constants;
+import com.android.cts.verifier.PassFailButtons;
+import com.android.cts.verifier.R;
+
+/**
+ * Tests that an app is not killed when it is swiped away from recents.
+ * Requires CtsForceStopHelper.apk to be installed.
+ */
+public class RecentTaskRemovalTestActivity extends PassFailButtons.Activity implements
+ View.OnClickListener {
+ private static final String HELPER_APP_NAME = Constants.PACKAGE_NAME;
+ private static final String HELPER_ACTIVITY_NAME = Constants.ACTIVITY_CLASS_NAME;
+
+ private static final String HELPER_APP_INSTALLED_KEY = "helper_installed";
+
+ private static final String ACTION_REPORT_TASK_REMOVED = "report_task_removed";
+ private static final String ACTION_REPORT_ALARM = "report_alarm";
+
+ private static final long EXTRA_WAIT_FOR_ALARM = 2_000;
+
+ private ImageView mInstallStatus;
+ private TextView mInstallTestAppText;
+
+ private ImageView mLaunchStatus;
+ private Button mLaunchTestAppButton;
+
+ private ImageView mRemoveFromRecentsStatus;
+ private TextView mRemoveFromRecentsInstructions;
+
+ private ImageView mForceStopStatus;
+ private TextView mForceStopVerificationResult;
+
+ private volatile boolean mTestAppInstalled;
+ private volatile boolean mTestTaskLaunched;
+ private volatile boolean mTestTaskRemoved;
+ private volatile boolean mTestAppForceStopped;
+ private volatile boolean mTestAlarmReceived;
+ private volatile boolean mWaitingForAlarm;
+
+ private final PackageStateReceiver mPackageChangesListener = new PackageStateReceiver();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.force_stop_recents_main);
+ setInfoResources(R.string.remove_from_recents_test, R.string.remove_from_recents_test_info,
+ -1);
+ setPassFailButtonClickListeners();
+
+ if (savedInstanceState != null) {
+ mTestAppInstalled = savedInstanceState.getBoolean(HELPER_APP_INSTALLED_KEY, false);
+ } else {
+ mTestAppInstalled = isPackageInstalled();
+ }
+ mInstallStatus = findViewById(R.id.fs_test_app_install_status);
+ mInstallTestAppText = findViewById(R.id.fs_test_app_install_instructions);
+
+ mRemoveFromRecentsStatus = findViewById(R.id.fs_test_app_recents_status);
+ mRemoveFromRecentsInstructions = findViewById(R.id.fs_test_app_recents_instructions);
+
+ mLaunchStatus = findViewById(R.id.fs_test_app_launch_status);
+ mLaunchTestAppButton = findViewById(R.id.fs_launch_test_app_button);
+ mLaunchTestAppButton.setOnClickListener(this);
+
+ mForceStopStatus = findViewById(R.id.fs_force_stop_status);
+ mForceStopVerificationResult = findViewById(R.id.fs_force_stop_verification);
+
+ mPackageChangesListener.register(mForceStopStatus.getHandler());
+ }
+
+ private boolean isPackageInstalled() {
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = getPackageManager().getPackageInfo(HELPER_APP_NAME, 0);
+ } catch (PackageManager.NameNotFoundException exc) {
+ // fall through
+ }
+ return packageInfo != null;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == mLaunchTestAppButton) {
+ mTestTaskLaunched = true;
+
+ final Intent reportTaskRemovedIntent = new Intent(ACTION_REPORT_TASK_REMOVED)
+ .setPackage(getPackageName())
+ .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ final PendingIntent onTaskRemoved = PendingIntent.getBroadcast(this, 0,
+ reportTaskRemovedIntent, 0);
+
+ final Intent reportAlarmIntent = new Intent(ACTION_REPORT_ALARM)
+ .setPackage(getPackageName())
+ .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ final PendingIntent onAlarm = PendingIntent.getBroadcast(this, 0, reportAlarmIntent, 0);
+
+ final Intent testActivity = new Intent()
+ .setClassName(HELPER_APP_NAME, HELPER_ACTIVITY_NAME)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .putExtra(Constants.EXTRA_ON_TASK_REMOVED, onTaskRemoved)
+ .putExtra(Constants.EXTRA_ON_ALARM, onAlarm);
+ startActivity(testActivity);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateWidgets();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle icicle) {
+ icicle.putBoolean(HELPER_APP_INSTALLED_KEY, mTestAppInstalled);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mPackageChangesListener.unregister();
+ }
+
+ private void updateWidgets() {
+ mInstallStatus.setImageResource(
+ mTestAppInstalled ? R.drawable.fs_good : R.drawable.fs_indeterminate);
+ mInstallTestAppText.setText(mTestAppInstalled ? R.string.fs_test_app_installed_text
+ : R.string.fs_test_app_install_instructions);
+ mInstallStatus.invalidate();
+
+ mLaunchStatus.setImageResource(
+ mTestTaskLaunched ? R.drawable.fs_good : R.drawable.fs_indeterminate);
+ mLaunchTestAppButton.setEnabled(mTestAppInstalled && !mTestTaskLaunched);
+ mLaunchStatus.invalidate();
+
+ mRemoveFromRecentsStatus.setImageResource(
+ mTestTaskRemoved ? R.drawable.fs_good : R.drawable.fs_indeterminate);
+ mRemoveFromRecentsInstructions.setText(R.string.fs_test_app_recents_instructions);
+ mRemoveFromRecentsStatus.invalidate();
+
+ if (mTestTaskRemoved) {
+ if (mWaitingForAlarm) {
+ mForceStopStatus.setImageResource(R.drawable.fs_clock);
+ mForceStopVerificationResult.setText(R.string.fs_force_stop_verification_pending);
+ } else {
+ mForceStopStatus.setImageResource(
+ (mTestAppForceStopped || !mTestAlarmReceived) ? R.drawable.fs_error
+ : R.drawable.fs_good);
+ mForceStopVerificationResult.setText((mTestAppForceStopped || !mTestAlarmReceived)
+ ? R.string.result_failure
+ : R.string.result_success);
+ }
+ mForceStopStatus.invalidate();
+ mForceStopStatus.setVisibility(View.VISIBLE);
+ mForceStopVerificationResult.setVisibility(View.VISIBLE);
+ } else {
+ mForceStopStatus.setVisibility(View.GONE);
+ mForceStopVerificationResult.setVisibility(View.GONE);
+ }
+
+ getPassButton().setEnabled(mTestAlarmReceived && !mTestAppForceStopped);
+ }
+
+ private final class PackageStateReceiver extends BroadcastReceiver {
+
+ void register(Handler handler) {
+ final IntentFilter packageFilter = new IntentFilter();
+ packageFilter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ packageFilter.addDataScheme("package");
+ registerReceiver(this, packageFilter);
+
+ final IntentFilter commsFilter = new IntentFilter();
+ commsFilter.addAction(ACTION_REPORT_TASK_REMOVED);
+ commsFilter.addAction(ACTION_REPORT_ALARM);
+ registerReceiver(this, commsFilter, null, handler);
+ }
+
+ void unregister() {
+ unregisterReceiver(this);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final Uri uri = intent.getData();
+ boolean testPackageAffected = (uri != null && HELPER_APP_NAME.equals(
+ uri.getSchemeSpecificPart()));
+ switch (intent.getAction()) {
+ case Intent.ACTION_PACKAGE_ADDED:
+ case Intent.ACTION_PACKAGE_REMOVED:
+ if (testPackageAffected) {
+ mTestAppInstalled = Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction());
+ }
+ break;
+ case Intent.ACTION_PACKAGE_RESTARTED:
+ if (testPackageAffected) {
+ mTestAppForceStopped = true;
+ }
+ break;
+ case ACTION_REPORT_TASK_REMOVED:
+ mTestTaskRemoved = true;
+ mWaitingForAlarm = true;
+ mForceStopStatus.postDelayed(() -> {
+ mWaitingForAlarm = false;
+ updateWidgets();
+ }, Constants.ALARM_DELAY + EXTRA_WAIT_FOR_ALARM);
+ break;
+ case ACTION_REPORT_ALARM:
+ mTestAlarmReceived = true;
+ mWaitingForAlarm = false;
+ break;
+ }
+ updateWidgets();
+ }
+ }
+}
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
index 0697be2..377d068 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/NdefPushReceiverActivity.java
@@ -123,7 +123,7 @@
// be set in onPrepareDialog.
return new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_info)
- .setTitle(R.string.nfc_result_failure)
+ .setTitle(R.string.result_failure)
.setMessage("")
.setPositiveButton(android.R.string.ok, null)
.show();
@@ -140,8 +140,8 @@
boolean isMatch = args.getBoolean(IS_MATCH_ARG);
AlertDialog alert = (AlertDialog) dialog;
alert.setTitle(isMatch
- ? R.string.nfc_result_success
- : R.string.nfc_result_failure);
+ ? R.string.result_success
+ : R.string.result_failure);
alert.setMessage(isMatch
? getString(R.string.nfc_ndef_push_receive_success)
: getString(R.string.nfc_ndef_push_receive_failure));
diff --git a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
index 85a9de5..d9166a5 100644
--- a/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
+++ b/apps/CtsVerifier/src/com/android/cts/verifier/nfc/TagVerifierActivity.java
@@ -352,7 +352,7 @@
// Placeholder title and message that will be set properly in onPrepareDialog
return new AlertDialog.Builder(this)
.setIcon(android.R.drawable.ic_dialog_alert)
- .setTitle(R.string.nfc_result_failure)
+ .setTitle(R.string.result_failure)
.setMessage("")
.setPositiveButton(android.R.string.ok, null)
.create();
@@ -378,8 +378,8 @@
AlertDialog alert = (AlertDialog) dialog;
alert.setTitle(isMatch
- ? R.string.nfc_result_success
- : R.string.nfc_result_failure);
+ ? R.string.result_success
+ : R.string.result_failure);
alert.setMessage(getString(R.string.nfc_result_message, expectedContent, actualContent));
}
diff --git a/apps/ForceStopHelperApp/Android.mk b/apps/ForceStopHelperApp/Android.mk
new file mode 100644
index 0000000..7ae586a
--- /dev/null
+++ b/apps/ForceStopHelperApp/Android.mk
@@ -0,0 +1,35 @@
+# Copyright (C) 2019 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := CtsForceStopHelper
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR += $(LOCAL_PATH)/res
+
+LOCAL_SDK_VERSION := current
+LOCAL_MIN_SDK_VERSION := 12
+
+# tag this module as a cts test artifact
+LOCAL_COMPATIBILITY_SUITE := cts vts general-tests
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/apps/ForceStopHelperApp/AndroidManifest.xml b/apps/ForceStopHelperApp/AndroidManifest.xml
new file mode 100644
index 0000000..2bd9b0d
--- /dev/null
+++ b/apps/ForceStopHelperApp/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.cts.forcestophelper" >
+
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+
+ <application android:label="Force stop helper app">
+ <activity android:name=".RecentTaskActivity"
+ android:exported="true" />
+ <service android:name=".TaskRemovedListenerService"
+ android:stopWithTask="false"/>
+ <receiver android:name=".AlarmReceiver"
+ android:exported="true" />
+ </application>
+</manifest>
diff --git a/apps/ForceStopHelperApp/res/drawable-hdpi/cts_verifier.png b/apps/ForceStopHelperApp/res/drawable-hdpi/cts_verifier.png
new file mode 100644
index 0000000..98a3246
--- /dev/null
+++ b/apps/ForceStopHelperApp/res/drawable-hdpi/cts_verifier.png
Binary files differ
diff --git a/apps/ForceStopHelperApp/res/layout/main.xml b/apps/ForceStopHelperApp/res/layout/main.xml
new file mode 100644
index 0000000..ce2c29d
--- /dev/null
+++ b/apps/ForceStopHelperApp/res/layout/main.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 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.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/status_message"
+ android:text="@string/status_finished"
+ android:visibility="invisible" />
+
+ </LinearLayout>
+
+</RelativeLayout>
diff --git a/apps/ForceStopHelperApp/res/values/strings.xml b/apps/ForceStopHelperApp/res/values/strings.xml
new file mode 100644
index 0000000..d88c976
--- /dev/null
+++ b/apps/ForceStopHelperApp/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 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.
+ -->
+
+<resources>
+ <string name="status_finished">Setup complete. Please return to the cts verifier</string>
+</resources>
diff --git a/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/AlarmReceiver.java b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/AlarmReceiver.java
new file mode 100644
index 0000000..a7927aa
--- /dev/null
+++ b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/AlarmReceiver.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 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.forcestophelper;
+
+import static com.android.cts.forcestophelper.Constants.ACTION_ALARM;
+import static com.android.cts.forcestophelper.Constants.EXTRA_ON_ALARM;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class AlarmReceiver extends BroadcastReceiver {
+
+ private static final String TAG = AlarmReceiver.class.getSimpleName();
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION_ALARM.equals(intent.getAction())) {
+ final PendingIntent onAlarm = intent.getParcelableExtra(EXTRA_ON_ALARM);
+ if (onAlarm != null) {
+ try {
+ onAlarm.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "onAlarm pending intent was canceled", e);
+ }
+ } else {
+ Log.e(TAG, "Could not find pending intent extra " + EXTRA_ON_ALARM);
+ }
+ }
+ }
+
+ public static PendingIntent createAlarmPendingIntent(Context context, PendingIntent onAlarm) {
+ final Intent alarmIntent = new Intent(ACTION_ALARM)
+ .setClass(context, AlarmReceiver.class)
+ .putExtra(EXTRA_ON_ALARM, onAlarm);
+ return PendingIntent.getBroadcast(context, 0, alarmIntent,
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+}
diff --git a/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/Constants.java b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/Constants.java
new file mode 100644
index 0000000..a8e39d2
--- /dev/null
+++ b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/Constants.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 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.forcestophelper;
+
+public interface Constants {
+ String PACKAGE_NAME = "com.android.cts.forcestophelper";
+ String ACTIVITY_CLASS_NAME = "com.android.cts.forcestophelper.RecentTaskActivity";
+ String ACTION_ALARM = PACKAGE_NAME + ".action.ACTION_ALARM";
+ String EXTRA_ON_TASK_REMOVED = PACKAGE_NAME + ".extra.ON_TASK_REMOVED";
+ String EXTRA_ON_ALARM = PACKAGE_NAME + ".extra.ON_ALARM";
+ long ALARM_DELAY = 5_000;
+}
diff --git a/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/RecentTaskActivity.java b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/RecentTaskActivity.java
new file mode 100644
index 0000000..6f853ee
--- /dev/null
+++ b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/RecentTaskActivity.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.forcestophelper;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Test activity to show up as a task in recents.
+ */
+public class RecentTaskActivity extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+ final Bundle extras = getIntent().getExtras();
+
+ final Intent serviceIntent = new Intent();
+ serviceIntent.setClass(this, TaskRemovedListenerService.class);
+ if (extras != null) {
+ serviceIntent.putExtras(extras);
+ }
+ startService(serviceIntent);
+
+ final TextView status = findViewById(R.id.status_message);
+ status.setVisibility(View.VISIBLE);
+ }
+}
diff --git a/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/TaskRemovedListenerService.java b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/TaskRemovedListenerService.java
new file mode 100644
index 0000000..3955d45
--- /dev/null
+++ b/apps/ForceStopHelperApp/src/com/android/cts/forcestophelper/TaskRemovedListenerService.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2019 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.forcestophelper;
+
+import static com.android.cts.forcestophelper.Constants.ALARM_DELAY;
+import static com.android.cts.forcestophelper.Constants.EXTRA_ON_ALARM;
+import static com.android.cts.forcestophelper.Constants.EXTRA_ON_TASK_REMOVED;
+import static com.android.cts.forcestophelper.AlarmReceiver.createAlarmPendingIntent;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+/**
+ * Service that listens for {@link #onTaskRemoved(Intent)} for any task in this application.
+ */
+public class TaskRemovedListenerService extends Service {
+ private static final String TAG = TaskRemovedListenerService.class.getSimpleName();
+
+ private PendingIntent mOnTaskRemoved;
+ private PendingIntent mOnAlarm;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ mOnTaskRemoved = intent.getParcelableExtra(EXTRA_ON_TASK_REMOVED);
+ mOnAlarm = intent.getParcelableExtra(EXTRA_ON_ALARM);
+
+ if (mOnTaskRemoved != null && mOnAlarm != null) {
+ final NotificationManager nm = getSystemService(NotificationManager.class);
+ nm.createNotificationChannel(
+ new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_DEFAULT));
+ final Notification notification = new Notification.Builder(this, TAG)
+ .setSmallIcon(R.drawable.cts_verifier)
+ .setContentTitle("Test Service")
+ .build();
+ startForeground(1, notification);
+ } else {
+ Log.e(TAG, "Need pending intents for onAlarm and onTaskRemoved. Stopping service.");
+ stopSelf();
+ }
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mOnTaskRemoved != null) {
+ Log.e(TAG, "Stopping without delivering mOnTaskRemoved");
+ }
+ }
+
+ @Override
+ public void onTaskRemoved(Intent rootIntent) {
+ super.onTaskRemoved(rootIntent);
+ if (mOnTaskRemoved != null) {
+ try {
+ mOnTaskRemoved.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "mOnTaskRemoved was canceled", e);
+ }
+ mOnTaskRemoved = null;
+ }
+ final PendingIntent alarmPi = createAlarmPendingIntent(this, mOnAlarm);
+ final AlarmManager am = getSystemService(AlarmManager.class);
+ am.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, ALARM_DELAY, alarmPi);
+
+ stopSelf();
+ }
+}