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