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.R; 61 import com.android.systemui.SysuiTestCase; 62 import com.android.systemui.broadcast.BroadcastSender; 63 import com.android.systemui.dagger.qualifiers.Background; 64 import com.android.systemui.dagger.qualifiers.Main; 65 import com.android.systemui.notetask.NoteTaskController; 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 127 MockitoAnnotations.initMocks(this); 128 mBgExecutor = MoreExecutors.directExecutor(); 129 mMainExecutor = MoreExecutors.directExecutor(); 130 mMainHandler = mContext.getMainThreadHandler(); 131 132 mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class); 133 mExpectedComponentName = ComponentName.unflattenFromString( 134 mContext.getString( 135 R.string.config_screenshotAppClipsActivityComponent)); 136 } 137 138 @After tearDown()139 public void tearDown() { 140 mActivityRule.finishActivity(); 141 } 142 143 @Test appClipsActivityConfig_shouldBeConfigured()144 public void appClipsActivityConfig_shouldBeConfigured() { 145 // Verify component name is setup - has package and class name. 146 assertThat(mExpectedComponentName).isNotNull(); 147 assertThat(mExpectedComponentName.getPackageName()).isNotEmpty(); 148 assertThat(mExpectedComponentName.getClassName()).isNotEmpty(); 149 } 150 151 @Test configComponentName_shouldResolve()152 public void configComponentName_shouldResolve() { 153 // Verify an intent when launched with configured component resolves to activity. 154 Intent appClipsActivityIntent = new Intent(); 155 appClipsActivityIntent.setComponent(mExpectedComponentName); 156 ResolveInfo resolveInfo = getContext().getPackageManager().resolveActivity( 157 appClipsActivityIntent, PackageManager.ResolveInfoFlags.of(0)); 158 ActivityInfo activityInfo = resolveInfo.activityInfo; 159 160 assertThat(activityInfo.packageName).isEqualTo( 161 mExpectedComponentName.getPackageName()); 162 assertThat(activityInfo.name).isEqualTo(mExpectedComponentName.getClassName()); 163 } 164 165 @Test queryService_returnedFailed_shouldFinishWithFailed()166 public void queryService_returnedFailed_shouldFinishWithFailed() { 167 when(mServiceConnector.postForResult(any())) 168 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_FAILED)); 169 170 mActivityRule.launchActivity(mActivityIntent); 171 172 ActivityResult actualResult = mActivityRule.getActivityResult(); 173 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 174 assertThat(getStatusCodeExtra(actualResult.getResultData())) 175 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED); 176 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 177 } 178 179 @Test queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported()180 public void queryService_returnedWindowModeUnsupported_shouldFinishWithWindowModeUnsupported() { 181 when(mServiceConnector.postForResult(any())) 182 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED)); 183 184 mActivityRule.launchActivity(mActivityIntent); 185 186 ActivityResult actualResult = mActivityRule.getActivityResult(); 187 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 188 assertThat(getStatusCodeExtra(actualResult.getResultData())) 189 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED); 190 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 191 } 192 193 @Test queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin()194 public void queryService_returnedScreenshotBlocked_shouldFinishWithBlockedByAdmin() { 195 when(mServiceConnector.postForResult(any())) 196 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN)); 197 198 mActivityRule.launchActivity(mActivityIntent); 199 200 ActivityResult actualResult = mActivityRule.getActivityResult(); 201 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 202 assertThat(getStatusCodeExtra(actualResult.getResultData())) 203 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN); 204 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 205 } 206 207 @Test startAppClipsActivity_userCanceled_shouldReturnUserCanceled()208 public void startAppClipsActivity_userCanceled_shouldReturnUserCanceled() 209 throws NameNotFoundException { 210 mockToSatisfyAllPrerequisites(); 211 212 AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent); 213 waitForIdleSync(); 214 215 Bundle bundle = new Bundle(); 216 bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, 217 CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED); 218 activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle); 219 waitForIdleSync(); 220 221 ActivityResult actualResult = mActivityRule.getActivityResult(); 222 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 223 assertThat(getStatusCodeExtra(actualResult.getResultData())) 224 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED); 225 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 226 } 227 228 @Test startAppClipsActivity_shouldReturnSuccess()229 public void startAppClipsActivity_shouldReturnSuccess() 230 throws NameNotFoundException { 231 mockToSatisfyAllPrerequisites(); 232 233 AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent); 234 waitForIdleSync(); 235 236 Bundle bundle = new Bundle(); 237 bundle.putParcelable(EXTRA_SCREENSHOT_URI, TEST_URI); 238 bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, CAPTURE_CONTENT_FOR_NOTE_SUCCESS); 239 activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle); 240 waitForIdleSync(); 241 242 ActivityResult actualResult = mActivityRule.getActivityResult(); 243 assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK); 244 assertThat(getStatusCodeExtra(actualResult.getResultData())) 245 .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS); 246 assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI); 247 assertThat(mActivityRule.getActivity().isFinishing()).isTrue(); 248 } 249 250 @Test startAppClipsActivity_shouldLogUiEvent()251 public void startAppClipsActivity_shouldLogUiEvent() 252 throws NameNotFoundException { 253 mockToSatisfyAllPrerequisites(); 254 255 mActivityRule.launchActivity(mActivityIntent); 256 waitForIdleSync(); 257 258 verify(mUiEventLogger).log(SCREENSHOT_FOR_NOTE_TRIGGERED, TEST_UID, TEST_CALLING_PACKAGE); 259 } 260 mockToSatisfyAllPrerequisites()261 private void mockToSatisfyAllPrerequisites() throws NameNotFoundException { 262 when(mServiceConnector.postForResult(any())) 263 .thenReturn(completedFuture(CAPTURE_CONTENT_FOR_NOTE_SUCCESS)); 264 265 ApplicationInfo testApplicationInfo = new ApplicationInfo(); 266 testApplicationInfo.uid = TEST_UID; 267 when(mPackageManager.getApplicationInfoAsUser(eq(TEST_CALLING_PACKAGE), 268 any(ApplicationInfoFlags.class), 269 eq(mContext.getUser().getIdentifier()))).thenReturn(testApplicationInfo); 270 } 271 272 public static final class AppClipsTrampolineActivityTestable extends 273 AppClipsTrampolineActivity { 274 275 Intent mStartedIntent; 276 UserHandle mStartingUser; 277 AppClipsTrampolineActivityTestable( ServiceConnector<IAppClipsService> serviceServiceConnector, NoteTaskController noteTaskController, PackageManager packageManager, UiEventLogger uiEventLogger, BroadcastSender broadcastSender, @Background Executor bgExecutor, @Main Executor mainExecutor, @Main Handler mainHandler)278 public AppClipsTrampolineActivityTestable( 279 ServiceConnector<IAppClipsService> serviceServiceConnector, 280 NoteTaskController noteTaskController, PackageManager packageManager, 281 UiEventLogger uiEventLogger, BroadcastSender broadcastSender, 282 @Background Executor bgExecutor, @Main Executor mainExecutor, 283 @Main Handler mainHandler) { 284 super(serviceServiceConnector, noteTaskController, packageManager, uiEventLogger, 285 broadcastSender, bgExecutor, mainExecutor, mainHandler); 286 } 287 288 @Override getCallingPackage()289 public String getCallingPackage() { 290 return TEST_CALLING_PACKAGE; 291 } 292 293 @Override startActivity(Intent unUsed)294 public void startActivity(Intent unUsed) { 295 // Ignore this intent to avoid App Clips screenshot editing activity from starting. 296 } 297 298 @Override startActivityAsUser(Intent startedIntent, UserHandle startingUser)299 public void startActivityAsUser(Intent startedIntent, UserHandle startingUser) { 300 mStartedIntent = startedIntent; 301 mStartingUser = startingUser; 302 } 303 } 304 getStatusCodeExtra(Intent intent)305 private static int getStatusCodeExtra(Intent intent) { 306 return intent.getIntExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, -100); 307 } 308 } 309