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