1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.screenshot.appclips; 18 19 import static android.app.Instrumentation.ActivityResult; 20 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN; 21 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED; 22 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS; 23 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED; 24 import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED; 25 import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE; 26 27 import static com.android.internal.infra.AndroidFuture.completedFuture; 28 import static com.android.systemui.screenshot.appclips.AppClipsEvent.SCREENSHOT_FOR_NOTE_TRIGGERED; 29 import static com.android.systemui.screenshot.appclips.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI; 30 31 import static com.google.common.truth.Truth.assertThat; 32 33 import static org.junit.Assume.assumeFalse; 34 import static org.mockito.ArgumentMatchers.any; 35 import static org.mockito.ArgumentMatchers.eq; 36 import static org.mockito.Mockito.verify; 37 import static org.mockito.Mockito.when; 38 39 import android.app.Activity; 40 import android.content.ComponentName; 41 import android.content.Intent; 42 import android.content.pm.ActivityInfo; 43 import android.content.pm.ApplicationInfo; 44 import android.content.pm.PackageManager; 45 import android.content.pm.PackageManager.ApplicationInfoFlags; 46 import android.content.pm.PackageManager.NameNotFoundException; 47 import android.content.pm.ResolveInfo; 48 import android.net.Uri; 49 import android.os.Bundle; 50 import android.os.Handler; 51 import android.os.UserHandle; 52 import android.testing.AndroidTestingRunner; 53 54 import androidx.test.rule.ActivityTestRule; 55 import androidx.test.runner.intercepting.SingleActivityFactory; 56 57 import com.android.internal.infra.ServiceConnector; 58 import com.android.internal.logging.UiEventLogger; 59 import com.android.internal.statusbar.IAppClipsService; 60 import com.android.systemui.SysuiTestCase; 61 import com.android.systemui.broadcast.BroadcastSender; 62 import com.android.systemui.dagger.qualifiers.Background; 63 import com.android.systemui.dagger.qualifiers.Main; 64 import com.android.systemui.notetask.NoteTaskController; 65 import com.android.systemui.res.R; 66 67 import com.google.common.util.concurrent.MoreExecutors; 68 69 import org.junit.After; 70 import org.junit.Before; 71 import org.junit.Rule; 72 import org.junit.Test; 73 import org.junit.runner.RunWith; 74 import org.mockito.Mock; 75 import org.mockito.MockitoAnnotations; 76 77 import java.util.concurrent.Executor; 78 79 @RunWith(AndroidTestingRunner.class) 80 public final class AppClipsTrampolineActivityTest extends SysuiTestCase { 81 82 private static final String TEST_URI_STRING = "www.test-uri.com"; 83 private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING); 84 private static final int TEST_UID = 42; 85 private static final String TEST_CALLING_PACKAGE = "test-calling-package"; 86 87 @Mock private ServiceConnector<IAppClipsService> mServiceConnector; 88 @Mock 89 private NoteTaskController mNoteTaskController; 90 @Mock 91 private PackageManager mPackageManager; 92 @Mock 93 private UiEventLogger mUiEventLogger; 94 @Mock 95 private BroadcastSender mBroadcastSender; 96 @Background 97 private Executor mBgExecutor; 98 @Main 99 private Executor mMainExecutor; 100 @Main 101 private Handler mMainHandler; 102 103 // Using the deprecated ActivityTestRule and SingleActivityFactory to help with injecting mocks 104 // and getting result from activity both of which are difficult to do in newer APIs. 105 private final SingleActivityFactory<AppClipsTrampolineActivityTestable> mFactory = 106 new SingleActivityFactory<>(AppClipsTrampolineActivityTestable.class) { 107 @Override 108 protected AppClipsTrampolineActivityTestable create(Intent unUsed) { 109 return new AppClipsTrampolineActivityTestable(mServiceConnector, 110 mNoteTaskController, mPackageManager, mUiEventLogger, mBroadcastSender, 111 mBgExecutor, mMainExecutor, mMainHandler); 112 } 113 }; 114 115 @Rule 116 public final ActivityTestRule<AppClipsTrampolineActivityTestable> mActivityRule = 117 new ActivityTestRule<>(mFactory, false, false); 118 119 private Intent mActivityIntent; 120 private ComponentName mExpectedComponentName; 121 122 @Before setUp()123 public void setUp() { 124 assumeFalse("Skip test: does not apply to watches", 125 mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)); 126 assumeFalse("Skip test: does not apply to TVs", 127 mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)); 128 129 MockitoAnnotations.initMocks(this); 130 mBgExecutor = MoreExecutors.directExecutor(); 131 mMainExecutor = MoreExecutors.directExecutor(); 132 mMainHandler = mContext.getMainThreadHandler(); 133 134 mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class); 135 mExpectedComponentName = ComponentName.unflattenFromString( 136 mContext.getString( 137 R.string.config_screenshotAppClipsActivityComponent)); 138 } 139 140 @After tearDown()141 public void tearDown() { 142 mActivityRule.finishActivity(); 143 } 144 145 @Test appClipsActivityConfig_shouldBeConfigured()146 public void appClipsActivityConfig_shouldBeConfigured() { 147 // Verify component name is setup - has package and class name. 148 assertThat(mExpectedComponentName).isNotNull(); 149 assertThat(mExpectedComponentName.getPackageName()).isNotEmpty(); 150 assertThat(mExpectedComponentName.getClassName()).isNotEmpty(); 151 } 152 153 @Test configComponentName_shouldResolve()154 public void configComponentName_shouldResolve() { 155 // Verify an intent when launched with configured component resolves to activity. 156 Intent appClipsActivityIntent = new Intent(); 157 appClipsActivityIntent.setComponent(mExpectedComponentName); 158 ResolveInfo resolveInfo = getContext().getPackageManager().resolveActivity( 159 appClipsActivityIntent, PackageManager.ResolveInfoFlags.of(0)); 160 ActivityInfo activityInfo = resolveInfo.activityInfo; 161 162 assertThat(activityInfo.packageName).isEqualTo( 163 mExpectedComponentName.getPackageName()); 164 assertThat(activityInfo.name).isEqualTo(mExpectedComponentName.getClassName()); 165 } 166 167 @Test queryService_returnedFailed_shouldFinishWithFailed()168 public void queryService_returnedFailed_shouldFinishWithFailed() { 169 when(mServiceConnector.postForResult(any())) 170 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_FAILED)); 171 172 mActivityRule.launchActivity(mActivityIntent); 173 174 ActivityResult actualResult = mActivityRule.getActivityResult(); 175 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 176 assertThat(getStatusCodeExtra(actualResult.getResultData())) 177 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); 178 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 179 } 180 181 @Test queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported()182 public void queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported() { 183 when(mServiceConnector.postForResult(any())) 184 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED)); 185 186 mActivityRule.launchActivity(mActivityIntent); 187 188 ActivityResult actualResult = mActivityRule.getActivityResult(); 189 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 190 assertThat(getStatusCodeExtra(actualResult.getResultData())) 191 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); 192 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 193 } 194 195 @Test queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin()196 public void queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin() { 197 when(mServiceConnector.postForResult(any())) 198 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN)); 199 200 mActivityRule.launchActivity(mActivityIntent); 201 202 ActivityResult actualResult = mActivityRule.getActivityResult(); 203 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 204 assertThat(getStatusCodeExtra(actualResult.getResultData())) 205 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); 206 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 207 } 208 209 @Test startAppClipsActivity_userCanceled_shouldReturnUserCanceled()210 public void startAppClipsActivity_userCanceled_shouldReturnUserCanceled() 211 throws NameNotFoundException { 212 mockToSatisfyAllPrerequisites(); 213 214 AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent); 215 waitForIdleSync(); 216 217 Bundle bundle = new Bundle(); 218 bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, 219 CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED); 220 activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle); 221 waitForIdleSync(); 222 223 ActivityResult actualResult = mActivityRule.getActivityResult(); 224 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 225 assertThat(getStatusCodeExtra(actualResult.getResultData())) 226 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED); 227 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 228 } 229 230 @Test startAppClipsActivity_shouldReturnSuccess()231 public void startAppClipsActivity_shouldReturnSuccess() 232 throws NameNotFoundException { 233 mockToSatisfyAllPrerequisites(); 234 235 AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent); 236 waitForIdleSync(); 237 238 Bundle bundle = new Bundle(); 239 bundle.putParcelable(EXTRA_SCREENSHOT_URI, TEST_URI); 240 bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, CAPTURE_CONTENT_FOR_NOTE_SUCCESS); 241 activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle); 242 waitForIdleSync(); 243 244 ActivityResult actualResult = mActivityRule.getActivityResult(); 245 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 246 assertThat(getStatusCodeExtra(actualResult.getResultData())) 247 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS); 248 assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI); 249 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 250 } 251 252 @Test startAppClipsActivity_shouldLogUiEvent()253 public void startAppClipsActivity_shouldLogUiEvent() 254 throws NameNotFoundException { 255 mockToSatisfyAllPrerequisites(); 256 257 mActivityRule.launchActivity(mActivityIntent); 258 waitForIdleSync(); 259 260 verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE); 261 } 262 mockToSatisfyAllPrerequisites()263 private void mockToSatisfyAllPrerequisites() throws NameNotFoundException { 264 when(mServiceConnector.postForResult(any())) 265 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_SUCCESS)); 266 267 ApplicationInfo testApplicationInfo = new ApplicationInfo(); 268 testApplicationInfo.uid = TEST_UID; 269 when(mPackageManager.getApplicationInfoAsUser(eq(TEST_CALLING_PACKAGE), 270 any(ApplicationInfoFlags.class), 271 eq(mContext.getUser().getIdentifier()))).thenReturn(testApplicationInfo); 272 } 273 274 public static final class AppClipsTrampolineActivityTestable extends 275 AppClipsTrampolineActivity { 276 277 Intent mStartedIntent; 278 UserHandle mStartingUser; 279 AppClipsTrampolineActivityTestable( ServiceConnector<IAppClipsService> serviceServiceConnector, NoteTaskController noteTaskController, PackageManager packageManager, UiEventLogger uiEventLogger, BroadcastSender broadcastSender, @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler)280 public AppClipsTrampolineActivityTestable( 281 ServiceConnector<IAppClipsService> serviceServiceConnector, 282 NoteTaskController noteTaskController, PackageManager packageManager, 283 UiEventLogger uiEventLogger, BroadcastSender broadcastSender, 284 @Background Executor bgExecutor, @Main Executor mainExecutor, 285 @Main Handler mainHandler) { 286 super(serviceServiceConnector, noteTaskController, packageManager, uiEventLogger, 287 broadcastSender, bgExecutor, mainExecutor, mainHandler); 288 } 289 290 @Override getCallingPackage()291 public String getCallingPackage() { 292 return TEST_CALLING_PACKAGE; 293 } 294 295 @Override startActivity(Intent unUsed)296 public void startActivity(Intent unUsed) { 297 // Ignore this intent to avoid App Clips screenshot editing activity from starting. 298 } 299 300 @Override startActivityAsUser(Intent startedIntent, UserHandle startingUser)301 public void startActivityAsUser(Intent startedIntent, UserHandle startingUser) { 302 mStartedIntent = startedIntent; 303 mStartingUser = startingUser; 304 } 305 } 306 getStatusCodeExtra(Intent intent)307 private static int getStatusCodeExtra(Intent intent) { 308 return intent.getIntExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, -100); 309 } 310 } 311