| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.media; |
| |
| import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT; |
| import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT; |
| import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL; |
| import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY; |
| import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; |
| |
| import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREEN; |
| import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP; |
| |
| import android.annotation.Nullable; |
| import android.app.Activity; |
| import android.app.ActivityManager; |
| import android.app.AlertDialog; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.PackageManager; |
| import android.graphics.Typeface; |
| import android.media.projection.IMediaProjection; |
| import android.media.projection.MediaProjectionManager; |
| import android.media.projection.ReviewGrantedConsentResult; |
| import android.os.Bundle; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.text.BidiFormatter; |
| import android.text.SpannableString; |
| import android.text.TextPaint; |
| import android.text.TextUtils; |
| import android.text.style.StyleSpan; |
| import android.util.Log; |
| import android.view.Window; |
| |
| import com.android.systemui.R; |
| import com.android.systemui.flags.FeatureFlags; |
| import com.android.systemui.flags.Flags; |
| import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; |
| import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; |
| import com.android.systemui.screenrecord.MediaProjectionPermissionDialog; |
| import com.android.systemui.screenrecord.ScreenShareOption; |
| import com.android.systemui.statusbar.phone.SystemUIDialog; |
| import com.android.systemui.util.Utils; |
| |
| import dagger.Lazy; |
| |
| import javax.inject.Inject; |
| |
| public class MediaProjectionPermissionActivity extends Activity |
| implements DialogInterface.OnClickListener { |
| private static final String TAG = "MediaProjectionPermissionActivity"; |
| private static final float MAX_APP_NAME_SIZE_PX = 500f; |
| private static final String ELLIPSIS = "\u2026"; |
| |
| private final FeatureFlags mFeatureFlags; |
| private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver; |
| |
| private String mPackageName; |
| private int mUid; |
| |
| private AlertDialog mDialog; |
| |
| // Indicates if user must review already-granted consent that the MediaProjection app is |
| // attempting to re-use. |
| private boolean mReviewGrantedConsentRequired = false; |
| // Indicates if the user has consented to record, but is continuing in another activity to |
| // select a particular task to capture. |
| private boolean mUserSelectingTask = false; |
| |
| @Inject |
| public MediaProjectionPermissionActivity(FeatureFlags featureFlags, |
| Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) { |
| mFeatureFlags = featureFlags; |
| mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver; |
| } |
| |
| @Override |
| public void onCreate(Bundle icicle) { |
| super.onCreate(icicle); |
| |
| final Intent launchingIntent = getIntent(); |
| mReviewGrantedConsentRequired = launchingIntent.getBooleanExtra( |
| EXTRA_USER_REVIEW_GRANTED_CONSENT, false); |
| |
| mPackageName = getCallingPackage(); |
| |
| // This activity is launched directly by an app, or system server. System server provides |
| // the package name through the intent if so. |
| if (mPackageName == null) { |
| if (launchingIntent.hasExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT)) { |
| mPackageName = launchingIntent.getStringExtra( |
| EXTRA_PACKAGE_REUSING_GRANTED_CONSENT); |
| } else { |
| setResult(RESULT_CANCELED); |
| finish(RECORD_CANCEL, /* projection= */ null); |
| return; |
| } |
| } |
| |
| PackageManager packageManager = getPackageManager(); |
| ApplicationInfo aInfo; |
| try { |
| aInfo = packageManager.getApplicationInfo(mPackageName, 0); |
| mUid = aInfo.uid; |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.e(TAG, "Unable to look up package name", e); |
| setResult(RESULT_CANCELED); |
| finish(RECORD_CANCEL, /* projection= */ null); |
| return; |
| } |
| |
| try { |
| if (MediaProjectionServiceHelper.hasProjectionPermission(mUid, mPackageName)) { |
| final IMediaProjection projection = |
| MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName, |
| mReviewGrantedConsentRequired); |
| // Automatically grant consent if a system-privileged component is recording. |
| final Intent intent = new Intent(); |
| intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, |
| projection.asBinder()); |
| setResult(RESULT_OK, intent); |
| finish(RECORD_CONTENT_DISPLAY, projection); |
| return; |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error checking projection permissions", e); |
| setResult(RESULT_CANCELED); |
| finish(RECORD_CANCEL, /* projection= */ null); |
| return; |
| } |
| |
| if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) { |
| if (showScreenCaptureDisabledDialogIfNeeded()) { |
| setResult(RESULT_CANCELED); |
| finish(RECORD_CANCEL, /* projection= */ null); |
| return; |
| } |
| } |
| |
| TextPaint paint = new TextPaint(); |
| paint.setTextSize(42); |
| |
| CharSequence dialogText = null; |
| CharSequence dialogTitle = null; |
| String appName = null; |
| if (Utils.isHeadlessRemoteDisplayProvider(packageManager, mPackageName)) { |
| dialogText = getString(R.string.media_projection_sys_service_dialog_warning); |
| dialogTitle = getString(R.string.media_projection_sys_service_dialog_title); |
| } else { |
| String label = aInfo.loadLabel(packageManager).toString(); |
| |
| // If the label contains new line characters it may push the security |
| // message below the fold of the dialog. Labels shouldn't have new line |
| // characters anyways, so just truncate the message the first time one |
| // is seen. |
| final int labelLength = label.length(); |
| int offset = 0; |
| while (offset < labelLength) { |
| final int codePoint = label.codePointAt(offset); |
| final int type = Character.getType(codePoint); |
| if (type == Character.LINE_SEPARATOR |
| || type == Character.CONTROL |
| || type == Character.PARAGRAPH_SEPARATOR) { |
| label = label.substring(0, offset) + ELLIPSIS; |
| break; |
| } |
| offset += Character.charCount(codePoint); |
| } |
| |
| if (label.isEmpty()) { |
| label = mPackageName; |
| } |
| |
| String unsanitizedAppName = TextUtils.ellipsize(label, |
| paint, MAX_APP_NAME_SIZE_PX, TextUtils.TruncateAt.END).toString(); |
| appName = BidiFormatter.getInstance().unicodeWrap(unsanitizedAppName); |
| |
| String actionText = getString(R.string.media_projection_dialog_warning, appName); |
| SpannableString message = new SpannableString(actionText); |
| |
| int appNameIndex = actionText.indexOf(appName); |
| if (appNameIndex >= 0) { |
| message.setSpan(new StyleSpan(Typeface.BOLD), |
| appNameIndex, appNameIndex + appName.length(), 0); |
| } |
| dialogText = message; |
| dialogTitle = getString(R.string.media_projection_dialog_title, appName); |
| } |
| |
| if (isPartialScreenSharingEnabled()) { |
| mDialog = new MediaProjectionPermissionDialog(this, () -> { |
| ScreenShareOption selectedOption = |
| ((MediaProjectionPermissionDialog) mDialog).getSelectedScreenShareOption(); |
| grantMediaProjectionPermission(selectedOption.getMode()); |
| }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName); |
| } else { |
| AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this, |
| R.style.Theme_SystemUI_Dialog) |
| .setTitle(dialogTitle) |
| .setIcon(R.drawable.ic_media_projection_permission) |
| .setMessage(dialogText) |
| .setPositiveButton(R.string.media_projection_action_text, this) |
| .setNeutralButton(android.R.string.cancel, this); |
| mDialog = dialogBuilder.create(); |
| } |
| |
| setUpDialog(mDialog); |
| mDialog.show(); |
| } |
| |
| @Override |
| protected void onDestroy() { |
| super.onDestroy(); |
| if (mDialog != null) { |
| mDialog.dismiss(); |
| } |
| } |
| |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| if (which == AlertDialog.BUTTON_POSITIVE) { |
| grantMediaProjectionPermission(ENTIRE_SCREEN); |
| } else { |
| if (mDialog != null) { |
| mDialog.dismiss(); |
| } |
| setResult(RESULT_CANCELED); |
| finish(RECORD_CANCEL, /* projection= */ null); |
| } |
| } |
| |
| private void setUpDialog(AlertDialog dialog) { |
| SystemUIDialog.registerDismissListener(dialog); |
| SystemUIDialog.applyFlags(dialog); |
| SystemUIDialog.setDialogSize(dialog); |
| |
| dialog.setOnCancelListener(this::onDialogDismissedOrCancelled); |
| dialog.setOnDismissListener(this::onDialogDismissedOrCancelled); |
| dialog.create(); |
| dialog.getButton(DialogInterface.BUTTON_POSITIVE).setFilterTouchesWhenObscured(true); |
| |
| final Window w = dialog.getWindow(); |
| w.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); |
| } |
| |
| private boolean showScreenCaptureDisabledDialogIfNeeded() { |
| final UserHandle hostUserHandle = getHostUserHandle(); |
| if (mScreenCaptureDevicePolicyResolver.get() |
| .isScreenCaptureCompletelyDisabled(hostUserHandle)) { |
| AlertDialog dialog = new ScreenCaptureDisabledDialog(this); |
| setUpDialog(dialog); |
| dialog.show(); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void grantMediaProjectionPermission(int screenShareMode) { |
| try { |
| if (screenShareMode == ENTIRE_SCREEN) { |
| final IMediaProjection projection = |
| MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName, |
| mReviewGrantedConsentRequired); |
| final Intent intent = new Intent(); |
| intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, |
| projection.asBinder()); |
| setResult(RESULT_OK, intent); |
| finish(RECORD_CONTENT_DISPLAY, projection); |
| } |
| if (isPartialScreenSharingEnabled() && screenShareMode == SINGLE_APP) { |
| IMediaProjection projection = MediaProjectionServiceHelper.createOrReuseProjection( |
| mUid, mPackageName, mReviewGrantedConsentRequired); |
| final Intent intent = new Intent(this, |
| MediaProjectionAppSelectorActivity.class); |
| intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, |
| projection.asBinder()); |
| intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE, |
| getHostUserHandle()); |
| intent.putExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, mReviewGrantedConsentRequired); |
| intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); |
| |
| // Start activity from the current foreground user to avoid creating a separate |
| // SystemUI process without access to recent tasks because it won't have |
| // WM Shell running inside. |
| mUserSelectingTask = true; |
| startActivityAsUser(intent, UserHandle.of(ActivityManager.getCurrentUser())); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error granting projection permission", e); |
| setResult(RESULT_CANCELED); |
| finish(RECORD_CANCEL, /* projection= */ null); |
| } finally { |
| if (mDialog != null) { |
| mDialog.dismiss(); |
| } |
| } |
| } |
| |
| private UserHandle getHostUserHandle() { |
| return UserHandle.getUserHandleForUid(getLaunchedFromUid()); |
| } |
| |
| @Override |
| public void finish() { |
| // Default to cancelling recording when user needs to review consent. |
| // Don't send cancel if the user has moved on to the next activity. |
| if (!mUserSelectingTask) { |
| finish(RECORD_CANCEL, /* projection= */ null); |
| } |
| } |
| |
| private void finish(@ReviewGrantedConsentResult int consentResult, |
| @Nullable IMediaProjection projection) { |
| MediaProjectionServiceHelper.setReviewedConsentIfNeeded( |
| consentResult, mReviewGrantedConsentRequired, projection); |
| super.finish(); |
| } |
| |
| private void onDialogDismissedOrCancelled(DialogInterface dialogInterface) { |
| if (!isFinishing()) { |
| finish(); |
| } |
| } |
| |
| private boolean isPartialScreenSharingEnabled() { |
| return mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING); |
| } |
| } |