1 /* 2 * Copyright (C) 2014 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.media; 18 19 import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT; 20 import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT; 21 import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL; 22 import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY; 23 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 24 25 import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREEN; 26 import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP; 27 28 import android.annotation.Nullable; 29 import android.app.Activity; 30 import android.app.ActivityManager; 31 import android.app.AlertDialog; 32 import android.app.StatusBarManager; 33 import android.content.Context; 34 import android.content.DialogInterface; 35 import android.content.Intent; 36 import android.content.pm.ApplicationInfo; 37 import android.content.pm.PackageManager; 38 import android.graphics.Typeface; 39 import android.media.projection.IMediaProjection; 40 import android.media.projection.MediaProjectionConfig; 41 import android.media.projection.MediaProjectionManager; 42 import android.media.projection.ReviewGrantedConsentResult; 43 import android.os.Bundle; 44 import android.os.RemoteException; 45 import android.os.UserHandle; 46 import android.text.BidiFormatter; 47 import android.text.SpannableString; 48 import android.text.TextPaint; 49 import android.text.TextUtils; 50 import android.text.style.StyleSpan; 51 import android.util.Log; 52 import android.view.Window; 53 54 import com.android.systemui.R; 55 import com.android.systemui.flags.FeatureFlags; 56 import com.android.systemui.flags.Flags; 57 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; 58 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; 59 import com.android.systemui.screenrecord.MediaProjectionPermissionDialog; 60 import com.android.systemui.screenrecord.ScreenShareOption; 61 import com.android.systemui.statusbar.phone.SystemUIDialog; 62 import com.android.systemui.util.Utils; 63 64 import dagger.Lazy; 65 66 import javax.inject.Inject; 67 68 public class MediaProjectionPermissionActivity extends Activity 69 implements DialogInterface.OnClickListener { 70 private static final String TAG = "MediaProjectionPermissionActivity"; 71 private static final float MAX_APP_NAME_SIZE_PX = 500f; 72 private static final String ELLIPSIS = "\u2026"; 73 74 private final FeatureFlags mFeatureFlags; 75 private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver; 76 private final StatusBarManager mStatusBarManager; 77 78 private String mPackageName; 79 private int mUid; 80 81 private AlertDialog mDialog; 82 83 // Indicates if user must review already-granted consent that the MediaProjection app is 84 // attempting to re-use. 85 private boolean mReviewGrantedConsentRequired = false; 86 // Indicates if the user has consented to record, but is continuing in another activity to 87 // select a particular task to capture. 88 private boolean mUserSelectingTask = false; 89 90 @Inject MediaProjectionPermissionActivity(FeatureFlags featureFlags, Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver, StatusBarManager statusBarManager)91 public MediaProjectionPermissionActivity(FeatureFlags featureFlags, 92 Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver, 93 StatusBarManager statusBarManager) { 94 mFeatureFlags = featureFlags; 95 mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver; 96 mStatusBarManager = statusBarManager; 97 } 98 99 @Override onCreate(Bundle icicle)100 public void onCreate(Bundle icicle) { 101 super.onCreate(icicle); 102 103 final Intent launchingIntent = getIntent(); 104 mReviewGrantedConsentRequired = launchingIntent.getBooleanExtra( 105 EXTRA_USER_REVIEW_GRANTED_CONSENT, false); 106 107 mPackageName = getCallingPackage(); 108 109 // This activity is launched directly by an app, or system server. System server provides 110 // the package name through the intent if so. 111 if (mPackageName == null) { 112 if (launchingIntent.hasExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT)) { 113 mPackageName = launchingIntent.getStringExtra( 114 EXTRA_PACKAGE_REUSING_GRANTED_CONSENT); 115 } else { 116 setResult(RESULT_CANCELED); 117 finish(RECORD_CANCEL, /* projection= */ null); 118 return; 119 } 120 } 121 122 PackageManager packageManager = getPackageManager(); 123 ApplicationInfo aInfo; 124 try { 125 aInfo = packageManager.getApplicationInfo(mPackageName, 0); 126 mUid = aInfo.uid; 127 } catch (PackageManager.NameNotFoundException e) { 128 Log.e(TAG, "Unable to look up package name", e); 129 setResult(RESULT_CANCELED); 130 finish(RECORD_CANCEL, /* projection= */ null); 131 return; 132 } 133 134 try { 135 if (MediaProjectionServiceHelper.hasProjectionPermission(mUid, mPackageName)) { 136 final IMediaProjection projection = 137 MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName, 138 mReviewGrantedConsentRequired); 139 // Automatically grant consent if a system-privileged component is recording. 140 final Intent intent = new Intent(); 141 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, 142 projection.asBinder()); 143 setResult(RESULT_OK, intent); 144 finish(RECORD_CONTENT_DISPLAY, projection); 145 return; 146 } 147 } catch (RemoteException e) { 148 Log.e(TAG, "Error checking projection permissions", e); 149 setResult(RESULT_CANCELED); 150 finish(RECORD_CANCEL, /* projection= */ null); 151 return; 152 } 153 154 if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) { 155 if (showScreenCaptureDisabledDialogIfNeeded()) { 156 setResult(RESULT_CANCELED); 157 finish(RECORD_CANCEL, /* projection= */ null); 158 return; 159 } 160 } 161 162 TextPaint paint = new TextPaint(); 163 paint.setTextSize(42); 164 165 CharSequence dialogText = null; 166 CharSequence dialogTitle = null; 167 String appName = null; 168 if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) { 169 dialogText = getString(R.string.media_projection_sys_service_dialog_warning); 170 dialogTitle = getString(R.string.media_projection_sys_service_dialog_title); 171 } else { 172 String label = aInfo.loadLabel(packageManager).toString(); 173 174 // If the label contains new line characters it may push the security 175 // message below the fold of the dialog. Labels shouldn't have new line 176 // characters anyways, so just truncate the message the first time one 177 // is seen. 178 final int labelLength = label.length(); 179 int offset = 0; 180 while (offset < labelLength) { 181 final int codePoint = label.codePointAt(offset); 182 final int type = Character.getType(codePoint); 183 if (type == Character.LINE_SEPARATOR 184 || type == Character.CONTROL 185 || type == Character.PARAGRAPH_SEPARATOR) { 186 label = label.substring(0, offset) + ELLIPSIS; 187 break; 188 } 189 offset += Character.charCount(codePoint); 190 } 191 192 if (label.isEmpty()) { 193 label = mPackageName; 194 } 195 196 String unsanitizedAppName = TextUtils.ellipsize(label, 197 paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString(); 198 appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName); 199 200 String actionText = getString(R.string.media_projection_dialog_warning, appName); 201 SpannableString message = new SpannableString(actionText); 202 203 int appNameIndex = actionText.indexOf(appName); 204 if (appNameIndex >= 0) { 205 message.setSpan(new StyleSpan(Typeface.BOLD), 206 appNameIndex, appNameIndex + appName.length(), 0); 207 } 208 dialogText = message; 209 dialogTitle = getString(R.string.media_projection_dialog_title, appName); 210 } 211 212 // Using application context for the dialog, instead of the activity context, so we get 213 // the correct screen width when in split screen. 214 Context dialogContext = getApplicationContext(); 215 if (isPartialScreenSharingEnabled()) { 216 mDialog = new MediaProjectionPermissionDialog(dialogContext, getMediaProjectionConfig(), 217 () -> { 218 MediaProjectionPermissionDialog dialog = 219 (MediaProjectionPermissionDialog) mDialog; 220 ScreenShareOption selectedOption = dialog.getSelectedScreenShareOption(); 221 grantMediaProjectionPermission(selectedOption.getMode()); 222 }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName); 223 } else { 224 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(dialogContext, 225 R.style.Theme_SystemUI_Dialog) 226 .setTitle(dialogTitle) 227 .setIcon(R.drawable.ic_media_projection_permission) 228 .setMessage(dialogText) 229 .setPositiveButton(R.string.media_projection_action_text, this) 230 .setNeutralButton(android.R.string.cancel, this); 231 mDialog = dialogBuilder.create(); 232 } 233 234 setUpDialog(mDialog); 235 mDialog.show(); 236 } 237 238 @Override onDestroy()239 protected void onDestroy() { 240 super.onDestroy(); 241 if (mDialog != null) { 242 mDialog.dismiss(); 243 } 244 } 245 246 @Override onClick(DialogInterface dialog, int which)247 public void onClick(DialogInterface dialog, int which) { 248 if (which == AlertDialog.BUTTON_POSITIVE) { 249 grantMediaProjectionPermission(ENTIRE_SCREEN); 250 } else { 251 if (mDialog != null) { 252 mDialog.dismiss(); 253 } 254 setResult(RESULT_CANCELED); 255 finish(RECORD_CANCEL, /* projection= */ null); 256 } 257 } 258 setUpDialog(AlertDialog dialog)259 private void setUpDialog(AlertDialog dialog) { 260 SystemUIDialog.registerDismissListener(dialog); 261 SystemUIDialog.applyFlags(dialog); 262 SystemUIDialog.setDialogSize(dialog); 263 264 dialog.setOnCancelListener(this::onDialogDismissedOrCancelled); 265 dialog.setOnDismissListener(this::onDialogDismissedOrCancelled); 266 dialog.create(); 267 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); 268 269 final Window w = dialog.getWindow(); 270 w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 271 } 272 showScreenCaptureDisabledDialogIfNeeded()273 private boolean showScreenCaptureDisabledDialogIfNeeded() { 274 final UserHandle hostUserHandle = getHostUserHandle(); 275 if (mScreenCaptureDevicePolicyResolver.get() 276 .isScreenCaptureCompletelyDisabled(hostUserHandle)) { 277 // Using application context for the dialog, instead of the activity context, so we get 278 // the correct screen width when in split screen. 279 Context dialogContext = getApplicationContext(); 280 AlertDialog dialog = new ScreenCaptureDisabledDialog(dialogContext); 281 setUpDialog(dialog); 282 dialog.show(); 283 return true; 284 } 285 286 return false; 287 } 288 grantMediaProjectionPermission(int screenShareMode)289 private void grantMediaProjectionPermission(int screenShareMode) { 290 try { 291 if (screenShareMode == ENTIRE_SCREEN) { 292 final IMediaProjection projection = 293 MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName, 294 mReviewGrantedConsentRequired); 295 final Intent intent = new Intent(); 296 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, 297 projection.asBinder()); 298 setResult(RESULT_OK, intent); 299 finish(RECORD_CONTENT_DISPLAY, projection); 300 } 301 if (isPartialScreenSharingEnabled() && screenShareMode == SINGLE_APP) { 302 IMediaProjection projection = MediaProjectionServiceHelper.createOrReuseProjection( 303 mUid, mPackageName, mReviewGrantedConsentRequired); 304 final Intent intent = new Intent(this, 305 MediaProjectionAppSelectorActivity.class); 306 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, 307 projection.asBinder()); 308 intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, 309 getHostUserHandle()); 310 intent.putExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, mReviewGrantedConsentRequired); 311 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 312 313 // Start activity from the current foreground user to avoid creating a separate 314 // SystemUI process without access to recent tasks because it won't have 315 // WM Shell running inside. 316 mUserSelectingTask = true; 317 startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser())); 318 // close shade if it's open 319 mStatusBarManager.collapsePanels(); 320 } 321 } catch (RemoteException e) { 322 Log.e(TAG, "Error granting projection permission", e); 323 setResult(RESULT_CANCELED); 324 finish(RECORD_CANCEL, /* projection= */ null); 325 } finally { 326 if (mDialog != null) { 327 mDialog.dismiss(); 328 } 329 } 330 } 331 getHostUserHandle()332 private UserHandle getHostUserHandle() { 333 return UserHandle.getUserHandleForUid(getLaunchedFromUid()); 334 } 335 336 @Override finish()337 public void finish() { 338 // Default to cancelling recording when user needs to review consent. 339 // Don't send cancel if the user has moved on to the next activity. 340 if (!mUserSelectingTask) { 341 finish(RECORD_CANCEL, /* projection= */ null); 342 } else { 343 super.finish(); 344 } 345 } 346 finish(@eviewGrantedConsentResult int consentResult, @Nullable IMediaProjection projection)347 private void finish(@ReviewGrantedConsentResult int consentResult, 348 @Nullable IMediaProjection projection) { 349 MediaProjectionServiceHelper.setReviewedConsentIfNeeded( 350 consentResult, mReviewGrantedConsentRequired, projection); 351 super.finish(); 352 } 353 onDialogDismissedOrCancelled(DialogInterface dialogInterface)354 private void onDialogDismissedOrCancelled(DialogInterface dialogInterface) { 355 if (!isFinishing()) { 356 finish(); 357 } 358 } 359 360 @Nullable getMediaProjectionConfig()361 private MediaProjectionConfig getMediaProjectionConfig() { 362 Intent intent = getIntent(); 363 if (intent == null) { 364 return null; 365 } 366 return intent.getParcelableExtra( 367 MediaProjectionManager.EXTRA_MEDIA_PROJECTION_CONFIG); 368 } 369 isPartialScreenSharingEnabled()370 private boolean isPartialScreenSharingEnabled() { 371 return mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING); 372 } 373 } 374