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