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.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; 20 21 import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREEN; 22 import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP; 23 24 import android.app.Activity; 25 import android.app.ActivityManager; 26 import android.app.AlertDialog; 27 import android.content.DialogInterface; 28 import android.content.Intent; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.graphics.Typeface; 32 import android.media.projection.IMediaProjection; 33 import android.media.projection.IMediaProjectionManager; 34 import android.media.projection.MediaProjectionManager; 35 import android.os.Bundle; 36 import android.os.IBinder; 37 import android.os.RemoteException; 38 import android.os.ServiceManager; 39 import android.os.UserHandle; 40 import android.text.BidiFormatter; 41 import android.text.SpannableString; 42 import android.text.TextPaint; 43 import android.text.TextUtils; 44 import android.text.style.StyleSpan; 45 import android.util.Log; 46 import android.view.Window; 47 48 import com.android.systemui.R; 49 import com.android.systemui.flags.FeatureFlags; 50 import com.android.systemui.flags.Flags; 51 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; 52 import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; 53 import com.android.systemui.screenrecord.MediaProjectionPermissionDialog; 54 import com.android.systemui.screenrecord.ScreenShareOption; 55 import com.android.systemui.statusbar.phone.SystemUIDialog; 56 import com.android.systemui.util.Utils; 57 58 import javax.inject.Inject; 59 60 import dagger.Lazy; 61 62 public class MediaProjectionPermissionActivity extends Activity 63 implements DialogInterface.OnClickListener { 64 private static final String TAG = "MediaProjectionPermissionActivity"; 65 private static final float MAX_APP_NAME_SIZE_PX = 500f; 66 private static final String ELLIPSIS = "\u2026"; 67 68 private final FeatureFlags mFeatureFlags; 69 private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver; 70 71 private String mPackageName; 72 private int mUid; 73 private IMediaProjectionManager mService; 74 75 private AlertDialog mDialog; 76 77 @Inject MediaProjectionPermissionActivity(FeatureFlags featureFlags, Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver)78 public MediaProjectionPermissionActivity(FeatureFlags featureFlags, 79 Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) { 80 mFeatureFlags = featureFlags; 81 mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver; 82 } 83 84 @Override onCreate(Bundle icicle)85 public void onCreate(Bundle icicle) { 86 super.onCreate(icicle); 87 88 mPackageName = getCallingPackage(); 89 IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); 90 mService = IMediaProjectionManager.Stub.asInterface(b); 91 92 if (mPackageName == null) { 93 finish(); 94 return; 95 } 96 97 PackageManager packageManager = getPackageManager(); 98 ApplicationInfo aInfo; 99 try { 100 aInfo = packageManager.getApplicationInfo(mPackageName, 0); 101 mUid = aInfo.uid; 102 } catch (PackageManager.NameNotFoundException e) { 103 Log.e(TAG, "unable to look up package name", e); 104 finish(); 105 return; 106 } 107 108 try { 109 if (mService.hasProjectionPermission(mUid, mPackageName)) { 110 setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName)); 111 finish(); 112 return; 113 } 114 } catch (RemoteException e) { 115 Log.e(TAG, "Error checking projection permissions", e); 116 finish(); 117 return; 118 } 119 120 if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) { 121 if (showScreenCaptureDisabledDialogIfNeeded()) { 122 return; 123 } 124 } 125 126 TextPaint paint = new TextPaint(); 127 paint.setTextSize(42); 128 129 CharSequence dialogText = null; 130 CharSequence dialogTitle = null; 131 String appName = null; 132 if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) { 133 dialogText = getString(R.string.media_projection_dialog_service_text); 134 dialogTitle = getString(R.string.media_projection_dialog_service_title); 135 } else { 136 String label = aInfo.loadLabel(packageManager).toString(); 137 138 // If the label contains new line characters it may push the security 139 // message below the fold of the dialog. Labels shouldn't have new line 140 // characters anyways, so just truncate the message the first time one 141 // is seen. 142 final int labelLength = label.length(); 143 int offset = 0; 144 while (offset < labelLength) { 145 final int codePoint = label.codePointAt(offset); 146 final int type = Character.getType(codePoint); 147 if (type == Character.LINE_SEPARATOR 148 || type == Character.CONTROL 149 || type == Character.PARAGRAPH_SEPARATOR) { 150 label = label.substring(0, offset) + ELLIPSIS; 151 break; 152 } 153 offset += Character.charCount(codePoint); 154 } 155 156 if (label.isEmpty()) { 157 label = mPackageName; 158 } 159 160 String unsanitizedAppName = TextUtils.ellipsize(label, 161 paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString(); 162 appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName); 163 164 String actionText = getString(R.string.media_projection_dialog_text, appName); 165 SpannableString message = new SpannableString(actionText); 166 167 int appNameIndex = actionText.indexOf(appName); 168 if (appNameIndex >= 0) { 169 message.setSpan(new StyleSpan(Typeface.BOLD), 170 appNameIndex, appNameIndex + appName.length(), 0); 171 } 172 dialogText = message; 173 dialogTitle = getString(R.string.media_projection_dialog_title, appName); 174 } 175 176 if (isPartialScreenSharingEnabled()) { 177 mDialog = new MediaProjectionPermissionDialog(this, () -> { 178 ScreenShareOption selectedOption = 179 ((MediaProjectionPermissionDialog) mDialog).getSelectedScreenShareOption(); 180 grantMediaProjectionPermission(selectedOption.getMode()); 181 }, appName); 182 } else { 183 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this, 184 R.style.Theme_SystemUI_Dialog) 185 .setTitle(dialogTitle) 186 .setIcon(R.drawable.ic_media_projection_permission) 187 .setMessage(dialogText) 188 .setPositiveButton(R.string.media_projection_action_text, this) 189 .setNeutralButton(android.R.string.cancel, this); 190 mDialog = dialogBuilder.create(); 191 } 192 193 setUpDialog(mDialog); 194 195 mDialog.show(); 196 } 197 198 @Override onDestroy()199 protected void onDestroy() { 200 super.onDestroy(); 201 if (mDialog != null) { 202 mDialog.dismiss(); 203 } 204 } 205 206 @Override onClick(DialogInterface dialog, int which)207 public void onClick(DialogInterface dialog, int which) { 208 if (which == AlertDialog.BUTTON_POSITIVE) { 209 grantMediaProjectionPermission(ENTIRE_SCREEN); 210 } 211 } 212 setUpDialog(AlertDialog dialog)213 private void setUpDialog(AlertDialog dialog) { 214 SystemUIDialog.registerDismissListener(dialog); 215 SystemUIDialog.applyFlags(dialog); 216 SystemUIDialog.setDialogSize(dialog); 217 218 dialog.setOnCancelListener(this::onDialogDismissedOrCancelled); 219 dialog.setOnDismissListener(this::onDialogDismissedOrCancelled); 220 dialog.create(); 221 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); 222 223 final Window w = dialog.getWindow(); 224 w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); 225 } 226 showScreenCaptureDisabledDialogIfNeeded()227 private boolean showScreenCaptureDisabledDialogIfNeeded() { 228 final UserHandle hostUserHandle = getHostUserHandle(); 229 if (mScreenCaptureDevicePolicyResolver.get() 230 .isScreenCaptureCompletelyDisabled(hostUserHandle)) { 231 AlertDialog dialog = new ScreenCaptureDisabledDialog(this); 232 setUpDialog(dialog); 233 dialog.show(); 234 return true; 235 } 236 237 return false; 238 } 239 grantMediaProjectionPermission(int screenShareMode)240 private void grantMediaProjectionPermission(int screenShareMode) { 241 try { 242 if (screenShareMode == ENTIRE_SCREEN) { 243 setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName)); 244 } 245 if (isPartialScreenSharingEnabled() && screenShareMode == SINGLE_APP) { 246 IMediaProjection projection = createProjection(mUid, mPackageName); 247 final Intent intent = new Intent(this, MediaProjectionAppSelectorActivity.class); 248 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, 249 projection.asBinder()); 250 intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, 251 getHostUserHandle()); 252 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 253 254 // Start activity from the current foreground user to avoid creating a separate 255 // SystemUI process without access to recent tasks because it won't have 256 // WM Shell running inside. 257 startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser())); 258 } 259 } catch (RemoteException e) { 260 Log.e(TAG, "Error granting projection permission", e); 261 setResult(RESULT_CANCELED); 262 } finally { 263 if (mDialog != null) { 264 mDialog.dismiss(); 265 } 266 finish(); 267 } 268 } 269 getHostUserHandle()270 private UserHandle getHostUserHandle() { 271 return UserHandle.getUserHandleForUid(getLaunchedFromUid()); 272 } 273 createProjection(int uid, String packageName)274 private IMediaProjection createProjection(int uid, String packageName) throws RemoteException { 275 return mService.createProjection(uid, packageName, 276 MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */); 277 } 278 getMediaProjectionIntent(int uid, String packageName)279 private Intent getMediaProjectionIntent(int uid, String packageName) 280 throws RemoteException { 281 IMediaProjection projection = createProjection(uid, packageName); 282 Intent intent = new Intent(); 283 intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder()); 284 return intent; 285 } 286 onDialogDismissedOrCancelled(DialogInterface dialogInterface)287 private void onDialogDismissedOrCancelled(DialogInterface dialogInterface) { 288 if (!isFinishing()) { 289 finish(); 290 } 291 } 292 isPartialScreenSharingEnabled()293 private boolean isPartialScreenSharingEnabled() { 294 return mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING); 295 } 296 } 297