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