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();
+    }
+}