1 /*
2  * Copyright (C) 2024 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 android.server.wm;
18 
19 import static android.content.Context.BIND_AUTO_CREATE;
20 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
21 
22 import android.app.Activity;
23 import android.app.ActivityOptions.LaunchCookie;
24 import android.app.Instrumentation;
25 import android.app.Notification;
26 import android.app.NotificationChannel;
27 import android.app.NotificationManager;
28 import android.app.Service;
29 import android.app.UiAutomation;
30 import android.content.ComponentName;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.ServiceConnection;
34 import android.graphics.Bitmap;
35 import android.graphics.Canvas;
36 import android.graphics.Color;
37 import android.graphics.drawable.Icon;
38 import android.media.projection.MediaProjection;
39 import android.media.projection.MediaProjectionManager;
40 import android.os.Binder;
41 import android.os.Bundle;
42 import android.os.IBinder;
43 
44 import androidx.annotation.Nullable;
45 import androidx.test.platform.app.InstrumentationRegistry;
46 
47 import com.android.compatibility.common.util.SystemUtil;
48 
49 import java.util.Objects;
50 import java.util.concurrent.CountDownLatch;
51 import java.util.concurrent.TimeUnit;
52 
53 /**
54  * Helper class for going through the MediaProjection Intent based authorization flow.
55  */
56 public class MediaProjectionHelper {
57 
58     private static final String EXTRA_LAUNCH_COOKIE = "android.server.wm.extra.EXTRA_LAUNCH_COOKIE";
59 
60     /**
61      * Activity that launches the MediaProjection screenCaptureIntent. If
62      * {@link #EXTRA_LAUNCH_COOKIE} is set, media projection will record the task
63      * associated with that launch cookie. Otherwise, media projection will record the full display.
64      */
65     public static class MediaProjectionActivity extends Activity {
66 
67         private static final int REQUEST_MEDIA_PROJECTION = 1;
68 
69         private int mResultCode;
70         private Intent mResultIntent;
71         private final CountDownLatch mStopLatch = new CountDownLatch(1);
72 
73         @Override
onCreate(Bundle savedInstanceState)74         public void onCreate(Bundle savedInstanceState) {
75             super.onCreate(savedInstanceState);
76 
77             MediaProjectionManager mediaProjectionManager = getSystemService(
78                     MediaProjectionManager.class);
79             Objects.requireNonNull(mediaProjectionManager);
80 
81             // Adopt shell permissions so we have CAPTURE_VIDEO_OUTPUT. This lets us bypass the
82             // MediaProjection authorization dialog.
83             UiAutomation uiAutomation =
84                     InstrumentationRegistry.getInstrumentation().getUiAutomation();
85             uiAutomation.adoptShellPermissionIdentity();
86 
87             LaunchCookie launchCookie = getIntent().getParcelableExtra(EXTRA_LAUNCH_COOKIE,
88                     LaunchCookie.class);
89             Intent screenCaptureIntent =
90                     launchCookie == null ? mediaProjectionManager.createScreenCaptureIntent()
91                             : mediaProjectionManager.createScreenCaptureIntent(launchCookie);
92             startActivityForResult(screenCaptureIntent, REQUEST_MEDIA_PROJECTION);
93         }
94 
95         @Override
onStop()96         public void onStop() {
97             super.onStop();
98             mStopLatch.countDown();
99         }
100 
101         @Override
onActivityResult(int requestCode, int resultCode, Intent data)102         public void onActivityResult(int requestCode, int resultCode, Intent data) {
103             UiAutomation uiAutomation =
104                     InstrumentationRegistry.getInstrumentation().getUiAutomation();
105             uiAutomation.dropShellPermissionIdentity();
106             if (requestCode != REQUEST_MEDIA_PROJECTION) {
107                 throw new IllegalStateException("Unexpected request code " + requestCode);
108             }
109             if (resultCode != RESULT_OK) {
110                 throw new IllegalStateException("MediaProjection request denied");
111             }
112             mResultCode = resultCode;
113             mResultIntent = data;
114             finish();
115         }
116 
getResultCode()117         int getResultCode() {
118             return mResultCode;
119         }
120 
getResultData()121         Intent getResultData() {
122             return mResultIntent;
123         }
124 
getStopLatch()125         CountDownLatch getStopLatch() {
126             return mStopLatch;
127         }
128     }
129 
130     /**
131      * Foreground MediaProjection service. MediaProjection requires a foreground service before
132      * recording begins.
133      */
134     public static class MediaProjectionService extends Service {
135 
136         private static final String CHANNEL_ID = "ScreenRecordingCallbackTests";
137         private static final String CHANNEL_NAME = "MediaProjectionService";
138         private static final int NOTIFICATION_ID = 1;
139 
140         private final IBinder mBinder = new ServiceBinder(this);
141         private final CountDownLatch mForegroundStarted = new CountDownLatch(1);
142         private Bitmap mIconBitmap;
143 
144         @Override
onBind(Intent intent)145         public IBinder onBind(Intent intent) {
146             return mBinder;
147         }
148 
149         @Override
onStartCommand(Intent intent, int flags, int startId)150         public int onStartCommand(Intent intent, int flags, int startId) {
151             NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME,
152                     NotificationManager.IMPORTANCE_NONE);
153             channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
154 
155             NotificationManager notificationManager = getSystemService(NotificationManager.class);
156             notificationManager.createNotificationChannel(channel);
157 
158             Notification.Builder notificationBuilder = new Notification.Builder(this, CHANNEL_ID);
159 
160             mIconBitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
161             Canvas canvas = new Canvas(mIconBitmap);
162             canvas.drawColor(Color.BLUE);
163             Icon icon = Icon.createWithBitmap(mIconBitmap);
164 
165             Notification notification = notificationBuilder.setOngoing(true).setContentTitle(
166                     "App is running").setSmallIcon(icon).setCategory(
167                     Notification.CATEGORY_SERVICE).setContentText("Context").build();
168 
169             startForeground(NOTIFICATION_ID, notification);
170             mForegroundStarted.countDown();
171 
172             return super.onStartCommand(intent, flags, startId);
173         }
174 
175         @Override
onDestroy()176         public void onDestroy() {
177             if (mIconBitmap != null) {
178                 mIconBitmap.recycle();
179                 mIconBitmap = null;
180             }
181             super.onDestroy();
182         }
183 
waitForForegroundService()184         void waitForForegroundService() {
185             await(mForegroundStarted, "MediaProjectionService failed to start foreground service");
186         }
187     }
188 
189     private static class ServiceBinder extends Binder {
190         final MediaProjectionService mService;
191 
ServiceBinder(MediaProjectionService service)192         ServiceBinder(MediaProjectionService service) {
193             mService = service;
194         }
195     }
196 
197     private int mResultCode;
198     private Intent mResultData;
199 
await(CountDownLatch latch, String failureMessage)200     private static void await(CountDownLatch latch, String failureMessage) {
201         try {
202             if (!latch.await(100, TimeUnit.SECONDS)) {
203                 throw new IllegalStateException(failureMessage);
204             }
205         } catch (InterruptedException e) {
206             throw new IllegalStateException(failureMessage, e);
207         }
208     }
209 
210     /**
211      * See {@link #authorizeMediaProjection(LaunchCookie)}.
212      */
authorizeMediaProjection()213     public void authorizeMediaProjection() {
214         authorizeMediaProjection(/*launchCookie=*/null);
215     }
216 
217     /**
218      * Launches an activity that triggers MediaProjection authorization.
219      *
220      * @param launchCookie  Optional launch cookie to specify the task to record.
221      */
authorizeMediaProjection(@ullable LaunchCookie launchCookie)222     public void authorizeMediaProjection(@Nullable LaunchCookie launchCookie) {
223         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
224         Context targetContext = instrumentation.getTargetContext();
225 
226         Intent intent = new Intent(targetContext, MediaProjectionActivity.class).addFlags(
227                 FLAG_ACTIVITY_NEW_TASK);
228         intent.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie);
229 
230         MediaProjectionActivity[] activity = new MediaProjectionActivity[1];
231         SystemUtil.runWithShellPermissionIdentity(() -> {
232             activity[0] = (MediaProjectionActivity) instrumentation.startActivitySync(intent);
233         });
234         await(activity[0].getStopLatch(), "Failed to wait for MediaProjectionActivity to stop");
235 
236         CountDownLatch serviceLatch = new CountDownLatch(1);
237         MediaProjectionService[] service = new MediaProjectionService[1];
238 
239         Intent serviceIntent = new Intent(targetContext, MediaProjectionService.class);
240         targetContext.bindService(serviceIntent, new ServiceConnection() {
241             @Override
242             public void onServiceConnected(ComponentName name, IBinder binder) {
243                 service[0] = ((ServiceBinder) binder).mService;
244                 serviceLatch.countDown();
245             }
246 
247             @Override
248             public void onServiceDisconnected(ComponentName name) {
249             }
250         }, BIND_AUTO_CREATE);
251         targetContext.startForegroundService(serviceIntent);
252 
253         await(serviceLatch, "Failed to connect to MediaProjectionService");
254         service[0].waitForForegroundService();
255 
256         mResultCode = activity[0].getResultCode();
257         mResultData = activity[0].getResultData();
258     }
259 
260     /**
261      * Start MediaProjection. {@link #authorizeMediaProjection(LaunchCookie)} must be called before
262      * calling this method.
263      *
264      * @return the {@link MediaProjection} instance returned by {@link MediaProjectionManager}.
265      */
startMediaProjection()266     public MediaProjection startMediaProjection() {
267         if (mResultData == null) {
268             throw new IllegalStateException(
269                     "authorizeMediaProjection not called before startMediaProjection");
270         }
271 
272         Context targetContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
273         MediaProjectionManager mediaProjectionManager = targetContext.getSystemService(
274                 MediaProjectionManager.class);
275         Objects.requireNonNull(mediaProjectionManager);
276         return mediaProjectionManager.getMediaProjection(mResultCode, mResultData);
277     }
278 }
279