Merge "ABA should dismiss if activity to block is gone" into rvc-dev
diff --git a/service/src/com/android/car/pm/ActivityBlockingActivity.java b/service/src/com/android/car/pm/ActivityBlockingActivity.java
index 23c58e2..db50f85 100644
--- a/service/src/com/android/car/pm/ActivityBlockingActivity.java
+++ b/service/src/com/android/car/pm/ActivityBlockingActivity.java
@@ -21,6 +21,8 @@
 import static com.android.car.pm.CarPackageManagerService.BLOCKING_INTENT_EXTRA_ROOT_ACTIVITY_NAME;
 
 import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.IActivityManager;
 import android.car.Car;
 import android.car.content.pm.CarPackageManager;
 import android.car.drivingstate.CarUxRestrictions;
@@ -29,6 +31,7 @@
 import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
@@ -39,6 +42,8 @@
 import com.android.car.CarLog;
 import com.android.car.R;
 
+import java.util.List;
+
 /**
  * Default activity that will be launched when the current foreground activity is not allowed.
  * Additional information on blocked Activity should be passed as intent extras.
@@ -49,11 +54,13 @@
 
     private Car mCar;
     private CarUxRestrictionsManager mUxRManager;
+    private CarPackageManager mCarPackageManager;
 
     private Button mExitButton;
     private Button mToggleDebug;
 
     private int mBlockedTaskId;
+    private IActivityManager mAm;
 
     private final View.OnClickListener mOnExitButtonClickedListener =
             v -> {
@@ -79,6 +86,7 @@
         setContentView(R.layout.activity_blocking);
 
         mExitButton = findViewById(R.id.exit_button);
+        mAm = ActivityManager.getService();
 
         // Listen to the CarUxRestrictions so this blocking activity can be dismissed when the
         // restrictions are lifted.
@@ -89,6 +97,8 @@
                     if (!ready) {
                         return;
                     }
+                    mCarPackageManager = (CarPackageManager) car.getCarManager(
+                            Car.PACKAGE_SERVICE);
                     mUxRManager = (CarUxRestrictionsManager) car.getCarManager(
                             Car.CAR_UX_RESTRICTION_SERVICE);
                     // This activity would have been launched only in a restricted state.
@@ -112,6 +122,12 @@
         String blockedActivity = getIntent().getStringExtra(
                 BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME);
         if (!TextUtils.isEmpty(blockedActivity)) {
+            if (isTopActivityBehindAbaDistractionOptimized()) {
+                Log.e(CarLog.TAG_AM, "Top activity is already DO, so finishing");
+                finish();
+                return;
+            }
+
             if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
                 Log.d(CarLog.TAG_AM, "Blocking activity " + blockedActivity);
             }
@@ -145,6 +161,58 @@
                 : getString(R.string.exit_button_go_back);
     }
 
+    /**
+     * It is possible that the stack info has changed between when the intent to launch this
+     * activity was initiated and when this activity is started. Check whether the activity behind
+     * the ABA is distraction optimized.
+     */
+    private boolean isTopActivityBehindAbaDistractionOptimized() {
+        List<ActivityManager.StackInfo> stackInfos;
+        try {
+            stackInfos = mAm.getAllStackInfos();
+        } catch (RemoteException e) {
+            Log.e(CarLog.TAG_AM, "Unable to get stack info from ActivityManager");
+            // assume that the state is still correct, the activity behind is not DO
+            return false;
+        }
+
+        ActivityManager.StackInfo topStackBehindAba = null;
+        for (ActivityManager.StackInfo stackInfo : stackInfos) {
+            if (stackInfo.displayId != getDisplayId()) {
+                // ignore stacks on other displays
+                continue;
+            }
+
+            if (getComponentName().equals(stackInfo.topActivity)) {
+                // ignore stack with the blocking activity
+                continue;
+            }
+
+            if (topStackBehindAba == null || topStackBehindAba.position < stackInfo.position) {
+                topStackBehindAba = stackInfo;
+            }
+        }
+
+        if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
+            Log.d(CarLog.TAG_AM, String.format("Top stack behind ABA is: %s", topStackBehindAba));
+        }
+
+        if (topStackBehindAba != null && topStackBehindAba.topActivity != null) {
+            boolean isDo = mCarPackageManager.isActivityDistractionOptimized(
+                    topStackBehindAba.topActivity.getPackageName(),
+                    topStackBehindAba.topActivity.getClassName());
+            if (Log.isLoggable(CarLog.TAG_AM, Log.DEBUG)) {
+                Log.d(CarLog.TAG_AM,
+                        String.format("Top activity (%s) is DO: %s", topStackBehindAba.topActivity,
+                                isDo));
+            }
+            return isDo;
+        }
+
+        // unknown top stack / activity, default to considering it non-DO
+        return false;
+    }
+
     private void displayDebugInfo() {
         String blockedActivity = getIntent().getStringExtra(
                 BLOCKING_INTENT_EXTRA_BLOCKED_ACTIVITY_NAME);
@@ -258,9 +326,7 @@
             if (Log.isLoggable(CarLog.TAG_AM, Log.INFO)) {
                 Log.i(CarLog.TAG_AM, "Restarting task " + mBlockedTaskId);
             }
-            CarPackageManager carPm = (CarPackageManager)
-                    mCar.getCarManager(Car.PACKAGE_SERVICE);
-            carPm.restartTask(mBlockedTaskId);
+            mCarPackageManager.restartTask(mBlockedTaskId);
             finish();
         }
     }
diff --git a/tests/carservice_test/Android.mk b/tests/carservice_test/Android.mk
index f5ebe83..10884f9 100644
--- a/tests/carservice_test/Android.mk
+++ b/tests/carservice_test/Android.mk
@@ -51,7 +51,8 @@
     mockito-target-extended \
     testng \
     truth-prebuilt \
-    vehicle-hal-support-lib-for-test
+    vehicle-hal-support-lib-for-test \
+    compatibility-device-util-axt
 
 
 LOCAL_JAVA_LIBRARIES := \
diff --git a/tests/carservice_test/AndroidManifest.xml b/tests/carservice_test/AndroidManifest.xml
index e359e7c..d960a72 100644
--- a/tests/carservice_test/AndroidManifest.xml
+++ b/tests/carservice_test/AndroidManifest.xml
@@ -52,6 +52,13 @@
         <activity android:name="com.android.car.SystemActivityMonitoringServiceTest$BlockingActivity"
                   android:taskAffinity="com.android.car.carservicetest.block"/>
         <activity android:name="com.android.car.CarUxRestrictionsManagerServiceTest$ActivityViewTestActivity"/>
+        <activity android:name="com.android.car.pm.ActivityBlockingActivityTest$NonDoNoHistoryActivity"
+                  android:noHistory="true"/>
+        <activity android:name="com.android.car.pm.ActivityBlockingActivityTest$NonDoActivity"/>
+        <activity android:name="com.android.car.pm.ActivityBlockingActivityTest$DoActivity"
+            android:label="DoActivity">
+            <meta-data android:name="distractionOptimized" android:value="true"/>
+        </activity>
 
         <receiver android:name="com.android.car.CarStorageMonitoringBroadcastReceiver"
             android:exported="true"
diff --git a/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java b/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
new file mode 100644
index 0000000..c7781d5
--- /dev/null
+++ b/tests/carservice_test/src/com/android/car/pm/ActivityBlockingActivityTest.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2020 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.car.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.car.Car;
+import android.car.drivingstate.CarDrivingStateEvent;
+import android.car.drivingstate.CarDrivingStateManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.view.Display;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class ActivityBlockingActivityTest {
+    private static final String ACTIVITY_BLOCKING_ACTIVITY_TEXTVIEW_ID =
+            "com.android.car:id/blocking_text";
+
+    private static final int UI_TIMEOUT_MS = 2000;
+    private static final int NOT_FOUND_UI_TIMEOUT_MS = 1000;
+    private static final long ACTIVITY_TIMEOUT_MS = 5000;
+
+    private CarDrivingStateManager mCarDrivingStateManager;
+
+    private UiDevice mDevice;
+
+    @Before
+    public void setUp() throws Exception {
+        Car car = Car.createCar(getContext());
+        mCarDrivingStateManager = (CarDrivingStateManager)
+                car.getCarManager(Car.CAR_DRIVING_STATE_SERVICE);
+        assertNotNull(mCarDrivingStateManager);
+
+        mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+
+        setDrivingStateMoving();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        setDrivingStateParked();
+    }
+
+    @Test
+    public void testBlockingActivity_doActivity_isNotBlocked() throws Exception {
+        startActivity(toComponentName(getTestContext(), DoActivity.class));
+
+        assertThat(mDevice.wait(Until.findObject(By.text(
+                DoActivity.class.getSimpleName())),
+                UI_TIMEOUT_MS)).isNotNull();
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_nonDoActivity_isBlocked() throws Exception {
+        startNonDoActivity(NonDoActivity.EXTRA_DO_NOTHING);
+
+        assertThat(mDevice.wait(Until.findObject(By.res(ACTIVITY_BLOCKING_ACTIVITY_TEXTVIEW_ID)),
+                UI_TIMEOUT_MS)).isNotNull();
+    }
+
+    @Test
+    public void testBlockingActivity_nonDoFinishesOnCreate_noBlockingActivity()
+            throws Exception {
+        startNonDoActivity(NonDoActivity.EXTRA_ONCREATE_FINISH_IMMEDIATELY);
+
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_nonDoLaunchesDoOnCreate_noBlockingActivity()
+            throws Exception {
+        startNonDoActivity(NonDoActivity.EXTRA_ONCREATE_LAUNCH_DO_IMMEDIATELY);
+
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_nonDoFinishesOnResume_noBlockingActivity()
+            throws Exception {
+        startNonDoActivity(NonDoActivity.EXTRA_ONRESUME_FINISH_IMMEDIATELY);
+
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_nonDoLaunchesDoOnResume_noBlockingActivity()
+            throws Exception {
+        startNonDoActivity(NonDoActivity.EXTRA_ONRESUME_LAUNCH_DO_IMMEDIATELY);
+
+        assertBlockingActivityNotFound();
+    }
+
+    @Test
+    public void testBlockingActivity_nonDoNoHistory_isBlocked() throws Exception {
+        startActivity(toComponentName(getTestContext(), NonDoNoHistoryActivity.class));
+
+        assertThat(mDevice.wait(Until.findObject(By.res(ACTIVITY_BLOCKING_ACTIVITY_TEXTVIEW_ID)),
+                UI_TIMEOUT_MS)).isNotNull();
+    }
+
+    private void assertBlockingActivityNotFound() {
+        assertThat(mDevice.wait(Until.findObject(By.res(ACTIVITY_BLOCKING_ACTIVITY_TEXTVIEW_ID)),
+                NOT_FOUND_UI_TIMEOUT_MS)).isNull();
+    }
+
+    private void startActivity(ComponentName name) {
+        Intent intent = new Intent();
+        intent.setComponent(name);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
+
+        getContext().startActivity(intent, options.toBundle());
+    }
+
+    private void startNonDoActivity(int firstActionFlag) {
+        ComponentName activity = toComponentName(getTestContext(), NonDoActivity.class);
+        Intent intent = new Intent();
+        intent.setComponent(activity);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(NonDoActivity.EXTRA_FIRST_ACTION, firstActionFlag);
+
+        ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(Display.DEFAULT_DISPLAY);
+
+        getContext().startActivity(intent, options.toBundle());
+    }
+
+
+    private void setDrivingStateMoving() {
+        mCarDrivingStateManager.injectDrivingState(CarDrivingStateEvent.DRIVING_STATE_MOVING);
+    }
+
+    private void setDrivingStateParked() {
+        mCarDrivingStateManager.injectDrivingState(CarDrivingStateEvent.DRIVING_STATE_PARKED);
+    }
+
+    private static ComponentName toComponentName(Context ctx, Class<?> cls) {
+        return ComponentName.createRelative(ctx, cls.getName());
+    }
+
+    public static class NonDoActivity extends TempActivity {
+
+        static final String EXTRA_FIRST_ACTION = "first_action";
+
+        static final int EXTRA_DO_NOTHING = 0;
+        static final int EXTRA_ONCREATE_FINISH_IMMEDIATELY = 1;
+        static final int EXTRA_ONCREATE_LAUNCH_DO_IMMEDIATELY = 2;
+        static final int EXTRA_ONRESUME_FINISH_IMMEDIATELY = 3;
+        static final int EXTRA_ONRESUME_LAUNCH_DO_IMMEDIATELY = 4;
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            Bundle extras = getIntent().getExtras();
+            if (extras != null) {
+                switch (extras.getInt(EXTRA_FIRST_ACTION, EXTRA_DO_NOTHING)) {
+                    case EXTRA_ONCREATE_LAUNCH_DO_IMMEDIATELY:
+                        startActivity(new Intent(this, DoActivity.class));
+                        finish();
+                        break;
+                    case EXTRA_ONCREATE_FINISH_IMMEDIATELY:
+                        finish();
+                        break;
+                    default:
+                        // do nothing
+                }
+            }
+        }
+
+        @Override
+        protected void onResume() {
+            super.onResume();
+            Bundle extras = getIntent().getExtras();
+            if (extras != null) {
+                switch (extras.getInt(EXTRA_FIRST_ACTION, EXTRA_DO_NOTHING)) {
+                    case EXTRA_ONRESUME_LAUNCH_DO_IMMEDIATELY:
+                        startActivity(new Intent(this, DoActivity.class));
+                        finish();
+                        break;
+                    case EXTRA_ONRESUME_FINISH_IMMEDIATELY:
+                        finish();
+                        break;
+                    default:
+                        // do nothing
+                }
+            }
+        }
+    }
+
+    public static class NonDoNoHistoryActivity extends TempActivity {
+    }
+
+    public static class DoActivity extends TempActivity {
+    }
+
+    /** Activity that closes itself after some timeout to clean up the screen. */
+    public static class TempActivity extends Activity {
+        @Override
+        protected void onResume() {
+            super.onResume();
+            getMainThreadHandler().postDelayed(this::finish, ACTIVITY_TIMEOUT_MS);
+        }
+    }
+
+    private Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    private Context getTestContext() {
+        return InstrumentationRegistry.getInstrumentation().getContext();
+    }
+}