Merge changes I279e0627,Ica2f5654 into sc-dev

* changes:
  Added unit tests for NewUserDisclaimerActivity
  Added unit tests for PerUserCarDevicePolicyService
diff --git a/service/src/com/android/car/PerUserCarService.java b/service/src/com/android/car/PerUserCarService.java
index ee4dafe..6a9291a 100644
--- a/service/src/com/android/car/PerUserCarService.java
+++ b/service/src/com/android/car/PerUserCarService.java
@@ -76,7 +76,7 @@
 
         if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) {
             mPerUserCarDevicePolicyService = PerUserCarDevicePolicyService.getInstance(context);
-            mPerUserCarDevicePolicyService.registerBroadcastReceiver();
+            mPerUserCarDevicePolicyService.onCreate();
         } else if (DBG) {
             Slogf.d(TAG, "Not setting PerUserCarDevicePolicyService because device doesn't have %s",
                     PackageManager.FEATURE_DEVICE_ADMIN);
diff --git a/service/src/com/android/car/admin/NewUserDisclaimerActivity.java b/service/src/com/android/car/admin/NewUserDisclaimerActivity.java
index 4101c34..8a186a4 100644
--- a/service/src/com/android/car/admin/NewUserDisclaimerActivity.java
+++ b/service/src/com/android/car/admin/NewUserDisclaimerActivity.java
@@ -30,6 +30,7 @@
 import com.android.car.CarLog;
 import com.android.car.R;
 import com.android.car.admin.ui.ManagedDeviceTextView;
+import com.android.internal.annotations.VisibleForTesting;
 
 // TODO(b/171603586): STOPSHIP move UI related activities to CarSettings
 /**
@@ -65,6 +66,11 @@
         // and/or integrate it with UserNoticeService
     }
 
+    @VisibleForTesting
+    Button getAcceptButton() {
+        return mAcceptButton;
+    }
+
     private void accept() {
         if (DEBUG) Slog.d(TAG, "user accepted");
 
@@ -72,13 +78,8 @@
         finish();
     }
 
-    private static Intent newIntent(Context context) {
-        return new Intent(context, NewUserDisclaimerActivity.class);
-    }
-
     static void showNotification(Context context) {
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, NOTIFICATION_ID,
-                newIntent(context), PendingIntent.FLAG_IMMUTABLE, null);
+        PendingIntent pendingIntent = getPendingIntent(context, /* extraFlags= */ 0);
 
         Notification notification = NotificationHelper
                 .newNotificationBuilder(context, NotificationManager.IMPORTANCE_DEFAULT)
@@ -104,7 +105,13 @@
                     + context.getUserId());
         }
         context.getSystemService(NotificationManager.class).cancel(NOTIFICATION_ID);
-        PendingIntent.getActivity(context, NOTIFICATION_ID, newIntent(context),
-                PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT).cancel();
+        getPendingIntent(context, PendingIntent.FLAG_UPDATE_CURRENT).cancel();
+    }
+
+    @VisibleForTesting
+    static PendingIntent getPendingIntent(Context context, int extraFlags) {
+        return PendingIntent.getActivity(context, NOTIFICATION_ID,
+                new Intent(context, NewUserDisclaimerActivity.class),
+                PendingIntent.FLAG_IMMUTABLE | extraFlags);
     }
 }
diff --git a/service/src/com/android/car/admin/PerUserCarDevicePolicyService.java b/service/src/com/android/car/admin/PerUserCarDevicePolicyService.java
index 4373a74..50ff4f7 100644
--- a/service/src/com/android/car/admin/PerUserCarDevicePolicyService.java
+++ b/service/src/com/android/car/admin/PerUserCarDevicePolicyService.java
@@ -16,6 +16,7 @@
 package com.android.car.admin;
 
 import static com.android.car.admin.CarDevicePolicyService.DEBUG;
+import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
 
 import android.annotation.IntDef;
 import android.app.admin.DevicePolicyManager;
@@ -28,12 +29,14 @@
 import android.util.Slog;
 
 import com.android.car.CarLog;
+import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
 
-// TODO(b/175057848) add unit tests
 /**
  * User-specific {@code CarDevicePolicyManagerService}.
  */
@@ -92,6 +95,8 @@
      * Gests the singleton instance, creating it if necessary.
      */
     public static PerUserCarDevicePolicyService getInstance(Context context) {
+        Objects.requireNonNull(context, "context cannot be null");
+
         synchronized (SLOCK) {
             if (sInstance == null) {
                 sInstance = new PerUserCarDevicePolicyService(context.getApplicationContext());
@@ -102,14 +107,15 @@
         }
     }
 
-    private PerUserCarDevicePolicyService(Context context) {
+    @VisibleForTesting
+    PerUserCarDevicePolicyService(Context context) {
         mContext = context;
     }
 
     /**
-     * Register a broadcast receiver to receive the proper events.
+     * Callback for when the service is created.
      */
-    public void registerBroadcastReceiver() {
+    public void onCreate() {
         if (DEBUG) Slog.d(TAG, "registering BroadcastReceiver");
 
         mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(
@@ -130,6 +136,7 @@
     /**
      * Dump its contents.
      */
+    @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
     public void dump(IndentingPrintWriter pw) {
         synchronized (SLOCK) {
             pw.printf("mNewUserDisclaimerStatus: %s\n",
@@ -166,7 +173,16 @@
         }
     }
 
-    private String newUserDisclaimerStatusToString(@NewUserDisclaimerStatus int status) {
+    @VisibleForTesting
+    @NewUserDisclaimerStatus
+    int getNewUserDisclaimerStatus() {
+        synchronized (SLOCK) {
+            return mNewUserDisclaimerStatus;
+        }
+    }
+
+    @VisibleForTesting
+    static String newUserDisclaimerStatusToString(@NewUserDisclaimerStatus int status) {
         return DebugUtils.constantToString(PerUserCarDevicePolicyService.class,
                 PREFIX_NEW_USER_DISCLAIMER_STATUS, status);
     }
diff --git a/tests/carservice_unit_test/src/com/android/car/admin/NewUserDisclaimerActivityTest.java b/tests/carservice_unit_test/src/com/android/car/admin/NewUserDisclaimerActivityTest.java
new file mode 100644
index 0000000..b1e8b72
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/admin/NewUserDisclaimerActivityTest.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2021 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.admin;
+
+import static android.app.Notification.EXTRA_TEXT;
+import static android.app.Notification.EXTRA_TITLE;
+import static android.app.Notification.FLAG_ONGOING_EVENT;
+
+import static com.android.car.admin.NotificationHelper.CHANNEL_ID_DEFAULT;
+import static com.android.car.admin.NotificationHelper.NEW_USER_DISCLAIMER_NOTIFICATION_ID;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.UiAutomation;
+import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.car.test.mocks.JavaMockitoHelper;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.widget.Button;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.car.R;
+import com.android.car.admin.ui.ManagedDeviceTextView;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+import java.util.concurrent.CountDownLatch;
+
+@RunWith(AndroidJUnit4.class)
+public final class NewUserDisclaimerActivityTest extends AbstractExtendedMockitoTestCase {
+
+    private static final String TAG = NewUserDisclaimerActivityTest.class.getSimpleName();
+
+    private static final long TIMEOUT_MS = 1_000;
+
+    private final Context mRealContext = InstrumentationRegistry.getInstrumentation()
+            .getContext();
+
+    private final UiAutomation mUiAutomation =
+            InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+    // NOTE: Cannot launch activity automatically as we need to mock
+    // PerUserCarDevicePolicyService.getInstance() first
+    @Rule
+    public ActivityTestRule<NewUserDisclaimerActivity> mActivityRule = new ActivityTestRule(
+            NewUserDisclaimerActivity.class,  /* initialTouchMode= */ false,
+            /* launchActivity= */ false);
+
+    private NewUserDisclaimerActivity mActivity;
+
+    private Context mSpiedContext;
+
+    @Mock
+    private PerUserCarDevicePolicyService mService;
+
+    @Mock
+    private NotificationManager mNotificationManager;
+
+    @Override
+    protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
+        session.spyStatic(PerUserCarDevicePolicyService.class);
+    }
+
+    @Before
+    public void setFixtures() {
+        Log.v(TAG, "setFixtures(): mocking PerUserCarDevicePolicyService.getInstance()");
+        doReturn(mService).when(() -> PerUserCarDevicePolicyService.getInstance(any()));
+        mSpiedContext = spy(mRealContext);
+
+        when(mSpiedContext.getSystemService(NotificationManager.class))
+                .thenReturn(mNotificationManager);
+
+        Log.v(TAG, "setFixtures(): launching activitiy");
+        mActivity = mActivityRule.launchActivity(/* intent= */ null);
+
+        // It's called onResume()
+        verify(mService).setShown();
+    }
+
+    @Test
+    public void testAccept() throws Exception {
+        CountDownLatch latch = new CountDownLatch(1);
+        mActivity.runOnUiThread(() -> {
+            mActivity.onCreate(/* savedInstanceState= */ null);
+            Button button = mActivity.getAcceptButton();
+            Log.d(TAG, "Clicking accept button: " + button);
+            button.performClick();
+            latch.countDown();
+        });
+        JavaMockitoHelper.await(latch, TIMEOUT_MS);
+
+        verify(mService).setAcknowledged();
+        assertWithMessage("activity is finishing").that(mActivity.isFinishing()).isTrue();
+    }
+
+    @Test
+    public void testShowNotification() {
+        NewUserDisclaimerActivity.showNotification(mSpiedContext);
+
+        ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
+        verify(mNotificationManager).notify(eq(NEW_USER_DISCLAIMER_NOTIFICATION_ID),
+                captor.capture());
+
+        Notification notification = captor.getValue();
+        assertWithMessage("notification").that(notification).isNotNull();
+        assertNotificationContents(notification);
+    }
+
+    @Test
+    public void testCancelNotification() throws Exception {
+        PendingIntent pendingIntent = NewUserDisclaimerActivity.getPendingIntent(mSpiedContext,
+                /* extraFlags = */ 0);
+        CountDownLatch cancelLatch = new CountDownLatch(1);
+        pendingIntent.registerCancelListener(pi -> cancelLatch.countDown());
+
+        NewUserDisclaimerActivity.cancelNotification(mSpiedContext);
+
+        verify(mNotificationManager).cancel(NEW_USER_DISCLAIMER_NOTIFICATION_ID);
+
+        // Assert pending intent was canceled (latch is counted down by the CancelListener)
+        JavaMockitoHelper.await(cancelLatch, TIMEOUT_MS);
+    }
+
+    private void assertNotificationContents(Notification notification) {
+        assertWithMessage("notification icon").that(notification.getSmallIcon()).isNotNull();
+        assertWithMessage("notification channel").that(notification.getChannelId())
+                .isEqualTo(CHANNEL_ID_DEFAULT);
+        assertWithMessage("notification flags has FLAG_ONGOING_EVENT")
+                .that(notification.flags & FLAG_ONGOING_EVENT).isEqualTo(FLAG_ONGOING_EVENT);
+
+        assertWithMessage("notification content pending intent")
+                .that(notification.contentIntent)
+                .isNotNull();
+        assertWithMessage("notification content pending intent is immutable")
+                .that(notification.contentIntent.isImmutable()).isTrue();
+        // Need android.permission.GET_INTENT_SENDER_INTENT to get the Intent
+        Intent intent;
+        mUiAutomation.adoptShellPermissionIdentity();
+        try {
+            intent = notification.contentIntent.getIntent();
+        } finally {
+            mUiAutomation.dropShellPermissionIdentity();
+        }
+        assertWithMessage("content intent").that(intent).isNotNull();
+        assertWithMessage("content intent component").that(intent.getComponent())
+                .isEqualTo(mActivity.getComponentName());
+
+        assertWithMessage("notification extras").that(notification.extras).isNotNull();
+        assertWithMessage("value of extra %s", EXTRA_TITLE)
+                .that(notification.extras.getString(EXTRA_TITLE))
+                .isEqualTo(mRealContext.getString(R.string.new_user_managed_notification_title));
+        assertWithMessage("value of extra %s", EXTRA_TEXT)
+                .that(notification.extras.getString(EXTRA_TEXT))
+                .isEqualTo(ManagedDeviceTextView.getManagedDeviceText(mRealContext));
+    }
+}
diff --git a/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperTest.java b/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperTest.java
index c4c481f..8610265 100644
--- a/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperTest.java
@@ -30,7 +30,6 @@
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
-
 @RunWith(MockitoJUnitRunner.class)
 public final class NotificationHelperTest {
 
diff --git a/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperValidImportanceTest.java b/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperValidImportanceTest.java
index daefcaf..7997954 100644
--- a/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperValidImportanceTest.java
+++ b/tests/carservice_unit_test/src/com/android/car/admin/NotificationHelperValidImportanceTest.java
@@ -62,7 +62,7 @@
     @Rule
     public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
-    private Context mRealContext = InstrumentationRegistry.getInstrumentation().getContext();
+    private final Context mRealContext = InstrumentationRegistry.getInstrumentation().getContext();
 
     private Context mSpiedContext;
 
diff --git a/tests/carservice_unit_test/src/com/android/car/admin/PerUserCarDevicePolicyServiceTest.java b/tests/carservice_unit_test/src/com/android/car/admin/PerUserCarDevicePolicyServiceTest.java
new file mode 100644
index 0000000..796afd5
--- /dev/null
+++ b/tests/carservice_unit_test/src/com/android/car/admin/PerUserCarDevicePolicyServiceTest.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2021 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.admin;
+
+import static android.app.admin.DevicePolicyManager.ACTION_SHOW_NEW_USER_DISCLAIMER;
+
+import static com.android.car.admin.PerUserCarDevicePolicyService.NEW_USER_DISCLAIMER_STATUS_ACKED;
+import static com.android.car.admin.PerUserCarDevicePolicyService.NEW_USER_DISCLAIMER_STATUS_NEVER_RECEIVED;
+import static com.android.car.admin.PerUserCarDevicePolicyService.NEW_USER_DISCLAIMER_STATUS_NOTIFICATION_SENT;
+import static com.android.car.admin.PerUserCarDevicePolicyService.NEW_USER_DISCLAIMER_STATUS_RECEIVED;
+import static com.android.car.admin.PerUserCarDevicePolicyService.NEW_USER_DISCLAIMER_STATUS_SHOWN;
+import static com.android.car.admin.PerUserCarDevicePolicyService.newUserDisclaimerStatusToString;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.expectThrows;
+
+import android.app.admin.DevicePolicyManager;
+import android.car.test.mocks.AbstractExtendedMockitoTestCase;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.car.admin.PerUserCarDevicePolicyService.NewUserDisclaimerStatus;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+
+public final class PerUserCarDevicePolicyServiceTest extends AbstractExtendedMockitoTestCase {
+
+    @Mock
+    private Context mContext;
+
+    private PerUserCarDevicePolicyService mInstance;
+
+    @Mock
+    private DevicePolicyManager mDpm;
+
+    @Override
+    protected void onSessionBuilder(CustomMockitoSessionBuilder session) {
+        session.spyStatic(NewUserDisclaimerActivity.class);
+    }
+
+    @Before
+    public void setFixtures() {
+        when(mContext.getApplicationContext()).thenReturn(mContext);
+        when(mContext.getSystemService(DevicePolicyManager.class)).thenReturn(mDpm);
+
+        mInstance = new PerUserCarDevicePolicyService(mContext);
+        assertStatusString(NEW_USER_DISCLAIMER_STATUS_NEVER_RECEIVED);
+    }
+
+    @Test
+    public void testGetInstance() {
+        PerUserCarDevicePolicyService instance1 = PerUserCarDevicePolicyService
+                .getInstance(mContext);
+        assertWithMessage("getInstance()#1").that(instance1).isNotNull();
+        assertWithMessage("getInstance()#1").that(instance1).isNotSameInstanceAs(mInstance);
+
+        PerUserCarDevicePolicyService instance2 = PerUserCarDevicePolicyService
+                .getInstance(mContext);
+        assertWithMessage("getInstance()#2").that(instance2).isNotNull();
+        assertWithMessage("getInstance()#2").that(instance2).isNotSameInstanceAs(mInstance);
+
+        assertWithMessage("getInstance()#2").that(instance2).isSameInstanceAs(instance1);
+        assertWithMessage("getInstance()#1").that(instance1).isSameInstanceAs(instance2);
+    }
+
+    @Test
+    public void testGetInstance_nullContext() {
+        NullPointerException exception = expectThrows(NullPointerException.class,
+                () -> PerUserCarDevicePolicyService.getInstance(null));
+        assertWithMessage("exception message").that(exception.getMessage()).contains("context");
+    }
+
+    @Test
+    public void testCreateAndDestroy() {
+        BroadcastReceiver receiver = callOnCreate();
+
+        callOnDestroy(receiver);
+    }
+
+    @Test
+    public void testShowWhenIntentReceived() {
+        doAnswer((inv) -> {
+            assertStatusString(NEW_USER_DISCLAIMER_STATUS_RECEIVED);
+            return null;
+        }).when(() -> NewUserDisclaimerActivity.showNotification(any()));
+        BroadcastReceiver receiver  = callOnCreate();
+
+        sendShowNewUserDisclaimerBroadcast(receiver);
+
+        assertStatusString(NEW_USER_DISCLAIMER_STATUS_NOTIFICATION_SENT);
+        verify(() -> NewUserDisclaimerActivity.showNotification(mContext));
+    }
+
+    @Test
+    public void testSetShown() {
+        mInstance.setShown();
+
+        assertStatusString(NEW_USER_DISCLAIMER_STATUS_SHOWN);
+    }
+
+    @Test
+    public void testSetAcknowledged() {
+        doNothing().when(() -> NewUserDisclaimerActivity.cancelNotification(any()));
+
+        mInstance.setAcknowledged();
+
+        assertStatusString(NEW_USER_DISCLAIMER_STATUS_ACKED);
+        verify(() -> NewUserDisclaimerActivity.cancelNotification(mContext));
+
+        verify(mDpm).resetNewUserDisclaimer();
+    }
+
+    private BroadcastReceiver callOnCreate() {
+        ArgumentCaptor<BroadcastReceiver> captor = ArgumentCaptor.forClass(BroadcastReceiver.class);
+
+        mInstance.onCreate();
+
+        verify(mContext).registerReceiver(captor.capture(), any());
+        BroadcastReceiver receiver = captor.getValue();
+        assertWithMessage("BroadcastReceiver captured on onCreate()").that(receiver).isNotNull();
+
+        return receiver;
+    }
+
+    private void callOnDestroy(BroadcastReceiver receiver) {
+        mInstance.onDestroy();
+
+        verify(mContext).unregisterReceiver(receiver);
+    }
+
+    private void sendShowNewUserDisclaimerBroadcast(BroadcastReceiver receiver) {
+        receiver.onReceive(mContext, new Intent(ACTION_SHOW_NEW_USER_DISCLAIMER));
+    }
+
+    private void assertStatusString(@NewUserDisclaimerStatus int expectedStatus) {
+        int actualStatus = mInstance.getNewUserDisclaimerStatus();
+        assertWithMessage("newUserDisclaimerStatus (%s=%s, %s=%s)",
+                expectedStatus, newUserDisclaimerStatusToString(expectedStatus),
+                actualStatus, newUserDisclaimerStatusToString(actualStatus))
+                        .that(actualStatus).isEqualTo(expectedStatus);
+    }
+}