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.recents; 18 19 import static com.android.systemui.shared.recents.utilities.Utilities.isLargeScreen; 20 import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE; 21 import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE; 22 import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE; 23 24 import android.animation.ArgbEvaluator; 25 import android.animation.ValueAnimator; 26 import android.app.ActivityManager; 27 import android.app.ActivityTaskManager; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.res.Configuration; 33 import android.graphics.PixelFormat; 34 import android.graphics.drawable.ColorDrawable; 35 import android.os.Binder; 36 import android.os.RemoteException; 37 import android.text.SpannableStringBuilder; 38 import android.text.style.BulletSpan; 39 import android.util.DisplayMetrics; 40 import android.util.Log; 41 import android.view.Gravity; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.view.WindowManager; 45 import android.view.WindowManagerGlobal; 46 import android.view.accessibility.AccessibilityManager; 47 import android.view.animation.DecelerateInterpolator; 48 import android.widget.Button; 49 import android.widget.FrameLayout; 50 import android.widget.ImageView; 51 import android.widget.LinearLayout; 52 import android.widget.TextView; 53 54 import androidx.annotation.NonNull; 55 56 import com.android.systemui.R; 57 import com.android.systemui.broadcast.BroadcastDispatcher; 58 import com.android.systemui.navigationbar.NavigationBarView; 59 import com.android.systemui.navigationbar.NavigationModeController; 60 import com.android.systemui.settings.UserTracker; 61 import com.android.systemui.shared.system.QuickStepContract; 62 import com.android.systemui.statusbar.phone.CentralSurfaces; 63 import com.android.systemui.util.leak.RotationUtils; 64 65 import java.util.ArrayList; 66 import java.util.Optional; 67 68 import javax.inject.Inject; 69 70 import dagger.Lazy; 71 72 public class ScreenPinningRequest implements View.OnClickListener, 73 NavigationModeController.ModeChangedListener { 74 private static final String TAG = "ScreenPinningRequest"; 75 76 private final Context mContext; 77 private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; 78 79 private final AccessibilityManager mAccessibilityService; 80 private final WindowManager mWindowManager; 81 private final BroadcastDispatcher mBroadcastDispatcher; 82 private final UserTracker mUserTracker; 83 84 private RequestWindowView mRequestWindow; 85 private int mNavBarMode; 86 87 /** ID of task to be pinned or locked. */ 88 private int taskId; 89 90 private final UserTracker.Callback mUserChangedCallback = 91 new UserTracker.Callback() { 92 @Override 93 public void onUserChanged(int newUser, @NonNull Context userContext) { 94 clearPrompt(); 95 } 96 }; 97 98 @Inject ScreenPinningRequest( Context context, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, NavigationModeController navigationModeController, BroadcastDispatcher broadcastDispatcher, UserTracker userTracker)99 public ScreenPinningRequest( 100 Context context, 101 Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, 102 NavigationModeController navigationModeController, 103 BroadcastDispatcher broadcastDispatcher, 104 UserTracker userTracker) { 105 mContext = context; 106 mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; 107 mAccessibilityService = (AccessibilityManager) 108 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 109 mWindowManager = (WindowManager) 110 mContext.getSystemService(Context.WINDOW_SERVICE); 111 mNavBarMode = navigationModeController.addListener(this); 112 mBroadcastDispatcher = broadcastDispatcher; 113 mUserTracker = userTracker; 114 } 115 clearPrompt()116 public void clearPrompt() { 117 if (mRequestWindow != null) { 118 mWindowManager.removeView(mRequestWindow); 119 mRequestWindow = null; 120 } 121 } 122 showPrompt(int taskId, boolean allowCancel)123 public void showPrompt(int taskId, boolean allowCancel) { 124 try { 125 clearPrompt(); 126 } catch (IllegalArgumentException e) { 127 // If the call to show the prompt fails due to the request window not already being 128 // attached, then just ignore the error since we will be re-adding it below. 129 } 130 131 this.taskId = taskId; 132 133 mRequestWindow = new RequestWindowView(mContext, allowCancel); 134 135 mRequestWindow.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 136 137 // show the confirmation 138 WindowManager.LayoutParams lp = getWindowLayoutParams(); 139 mWindowManager.addView(mRequestWindow, lp); 140 } 141 142 @Override onNavigationModeChanged(int mode)143 public void onNavigationModeChanged(int mode) { 144 mNavBarMode = mode; 145 } 146 onConfigurationChanged()147 public void onConfigurationChanged() { 148 if (mRequestWindow != null) { 149 mRequestWindow.onConfigurationChanged(); 150 } 151 } 152 getWindowLayoutParams()153 protected WindowManager.LayoutParams getWindowLayoutParams() { 154 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 155 ViewGroup.LayoutParams.MATCH_PARENT, 156 ViewGroup.LayoutParams.MATCH_PARENT, 157 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 158 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 159 | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, 160 PixelFormat.TRANSLUCENT); 161 lp.token = new Binder(); 162 lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; 163 lp.setTitle("ScreenPinningConfirmation"); 164 lp.gravity = Gravity.FILL; 165 lp.setFitInsetsTypes(0 /* types */); 166 return lp; 167 } 168 169 @Override onClick(View v)170 public void onClick(View v) { 171 if (v.getId() == R.id.screen_pinning_ok_button || mRequestWindow == v) { 172 try { 173 ActivityTaskManager.getService().startSystemLockTaskMode(taskId); 174 } catch (RemoteException e) {} 175 } 176 clearPrompt(); 177 } 178 getRequestLayoutParams(int rotation)179 public FrameLayout.LayoutParams getRequestLayoutParams(int rotation) { 180 return new FrameLayout.LayoutParams( 181 ViewGroup.LayoutParams.WRAP_CONTENT, 182 ViewGroup.LayoutParams.WRAP_CONTENT, 183 rotation == ROTATION_SEASCAPE ? (Gravity.CENTER_VERTICAL | Gravity.LEFT) : 184 rotation == ROTATION_LANDSCAPE ? (Gravity.CENTER_VERTICAL | Gravity.RIGHT) 185 : (Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM)); 186 } 187 188 private class RequestWindowView extends FrameLayout { 189 private static final int OFFSET_DP = 96; 190 191 private final ColorDrawable mColor = new ColorDrawable(0); 192 private ViewGroup mLayout; 193 private final boolean mShowCancel; 194 RequestWindowView(Context context, boolean showCancel)195 private RequestWindowView(Context context, boolean showCancel) { 196 super(context); 197 setClickable(true); 198 setOnClickListener(ScreenPinningRequest.this); 199 setBackground(mColor); 200 mShowCancel = showCancel; 201 } 202 203 @Override onAttachedToWindow()204 public void onAttachedToWindow() { 205 DisplayMetrics metrics = new DisplayMetrics(); 206 mWindowManager.getDefaultDisplay().getMetrics(metrics); 207 float density = metrics.density; 208 int rotation = getRotation(mContext); 209 210 inflateView(rotation); 211 int bgColor = mContext.getColor( 212 R.color.screen_pinning_request_window_bg); 213 if (ActivityManager.isHighEndGfx()) { 214 mLayout.setAlpha(0f); 215 if (rotation == ROTATION_SEASCAPE) { 216 mLayout.setTranslationX(-OFFSET_DP * density); 217 } else if (rotation == ROTATION_LANDSCAPE) { 218 mLayout.setTranslationX(OFFSET_DP * density); 219 } else { 220 mLayout.setTranslationY(OFFSET_DP * density); 221 } 222 mLayout.animate() 223 .alpha(1f) 224 .translationX(0) 225 .translationY(0) 226 .setDuration(300) 227 .setInterpolator(new DecelerateInterpolator()) 228 .start(); 229 230 ValueAnimator colorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, bgColor); 231 colorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 232 @Override 233 public void onAnimationUpdate(ValueAnimator animation) { 234 final int c = (Integer) animation.getAnimatedValue(); 235 mColor.setColor(c); 236 } 237 }); 238 colorAnim.setDuration(1000); 239 colorAnim.start(); 240 } else { 241 mColor.setColor(bgColor); 242 } 243 244 IntentFilter filter = new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); 245 filter.addAction(Intent.ACTION_SCREEN_OFF); 246 mBroadcastDispatcher.registerReceiver(mReceiver, filter); 247 mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); 248 } 249 inflateView(int rotation)250 private void inflateView(int rotation) { 251 // We only want this landscape orientation on <600dp, so rather than handle 252 // resource overlay for -land and -sw600dp-land, just inflate this 253 // other view for this single case. 254 mLayout = (ViewGroup) View.inflate(getContext(), 255 rotation == ROTATION_SEASCAPE ? R.layout.screen_pinning_request_sea_phone : 256 rotation == ROTATION_LANDSCAPE ? R.layout.screen_pinning_request_land_phone 257 : R.layout.screen_pinning_request, 258 null); 259 // Catch touches so they don't trigger cancel/activate, like outside does. 260 mLayout.setClickable(true); 261 // Status bar is always on the right. 262 mLayout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); 263 // Buttons and text do switch sides though. 264 mLayout.findViewById(R.id.screen_pinning_text_area) 265 .setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); 266 View buttons = mLayout.findViewById(R.id.screen_pinning_buttons); 267 if (!QuickStepContract.isGesturalMode(mNavBarMode) 268 && hasSoftNavigationBar(mContext.getDisplayId()) && !isLargeScreen(mContext)) { 269 buttons.setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); 270 swapChildrenIfRtlAndVertical(buttons); 271 } else { 272 buttons.setVisibility(View.GONE); 273 } 274 275 ((Button) mLayout.findViewById(R.id.screen_pinning_ok_button)) 276 .setOnClickListener(ScreenPinningRequest.this); 277 if (mShowCancel) { 278 ((Button) mLayout.findViewById(R.id.screen_pinning_cancel_button)) 279 .setOnClickListener(ScreenPinningRequest.this); 280 } else { 281 ((Button) mLayout.findViewById(R.id.screen_pinning_cancel_button)) 282 .setVisibility(View.INVISIBLE); 283 } 284 285 final Optional<CentralSurfaces> centralSurfacesOptional = 286 mCentralSurfacesOptionalLazy.get(); 287 boolean recentsVisible = 288 centralSurfacesOptional.map(CentralSurfaces::isOverviewEnabled).orElse(false); 289 boolean touchExplorationEnabled = mAccessibilityService.isTouchExplorationEnabled(); 290 int descriptionStringResId; 291 if (QuickStepContract.isGesturalMode(mNavBarMode)) { 292 descriptionStringResId = R.string.screen_pinning_description_gestural; 293 } else if (recentsVisible) { 294 mLayout.findViewById(R.id.screen_pinning_recents_group).setVisibility(VISIBLE); 295 mLayout.findViewById(R.id.screen_pinning_home_bg_light).setVisibility(INVISIBLE); 296 mLayout.findViewById(R.id.screen_pinning_home_bg).setVisibility(INVISIBLE); 297 descriptionStringResId = touchExplorationEnabled 298 ? R.string.screen_pinning_description_accessible 299 : R.string.screen_pinning_description; 300 } else { 301 mLayout.findViewById(R.id.screen_pinning_recents_group).setVisibility(INVISIBLE); 302 mLayout.findViewById(R.id.screen_pinning_home_bg_light).setVisibility(VISIBLE); 303 mLayout.findViewById(R.id.screen_pinning_home_bg).setVisibility(VISIBLE); 304 descriptionStringResId = touchExplorationEnabled 305 ? R.string.screen_pinning_description_recents_invisible_accessible 306 : R.string.screen_pinning_description_recents_invisible; 307 } 308 309 NavigationBarView navigationBarView = 310 centralSurfacesOptional.map(CentralSurfaces::getNavigationBarView).orElse(null); 311 if (navigationBarView != null) { 312 ((ImageView) mLayout.findViewById(R.id.screen_pinning_back_icon)) 313 .setImageDrawable(navigationBarView.getBackDrawable()); 314 ((ImageView) mLayout.findViewById(R.id.screen_pinning_home_icon)) 315 .setImageDrawable(navigationBarView.getHomeDrawable()); 316 } 317 318 // Create a bulleted list of the default description plus the two security notes. 319 int gapWidth = getResources().getDimensionPixelSize( 320 R.dimen.screen_pinning_description_bullet_gap_width); 321 SpannableStringBuilder description = new SpannableStringBuilder(); 322 description.append(getContext().getText(descriptionStringResId), 323 new BulletSpan(gapWidth), /* flags */ 0); 324 description.append(System.lineSeparator()); 325 description.append(getContext().getText(R.string.screen_pinning_exposes_personal_data), 326 new BulletSpan(gapWidth), /* flags */ 0); 327 description.append(System.lineSeparator()); 328 description.append(getContext().getText(R.string.screen_pinning_can_open_other_apps), 329 new BulletSpan(gapWidth), /* flags */ 0); 330 ((TextView) mLayout.findViewById(R.id.screen_pinning_description)).setText(description); 331 332 final int backBgVisibility = touchExplorationEnabled ? View.INVISIBLE : View.VISIBLE; 333 mLayout.findViewById(R.id.screen_pinning_back_bg).setVisibility(backBgVisibility); 334 mLayout.findViewById(R.id.screen_pinning_back_bg_light).setVisibility(backBgVisibility); 335 336 addView(mLayout, getRequestLayoutParams(rotation)); 337 } 338 339 /** 340 * @param displayId the id of display to check if there is a software navigation bar. 341 * 342 * @return whether there is a soft nav bar on specific display. 343 */ hasSoftNavigationBar(int displayId)344 private boolean hasSoftNavigationBar(int displayId) { 345 try { 346 return WindowManagerGlobal.getWindowManagerService().hasNavigationBar(displayId); 347 } catch (RemoteException e) { 348 Log.e(TAG, "Failed to check soft navigation bar", e); 349 return false; 350 } 351 } 352 swapChildrenIfRtlAndVertical(View group)353 private void swapChildrenIfRtlAndVertical(View group) { 354 if (mContext.getResources().getConfiguration().getLayoutDirection() 355 != View.LAYOUT_DIRECTION_RTL) { 356 return; 357 } 358 LinearLayout linearLayout = (LinearLayout) group; 359 if (linearLayout.getOrientation() == LinearLayout.VERTICAL) { 360 int childCount = linearLayout.getChildCount(); 361 ArrayList<View> childList = new ArrayList<>(childCount); 362 for (int i = 0; i < childCount; i++) { 363 childList.add(linearLayout.getChildAt(i)); 364 } 365 linearLayout.removeAllViews(); 366 for (int i = childCount - 1; i >= 0; i--) { 367 linearLayout.addView(childList.get(i)); 368 } 369 } 370 } 371 372 @Override onDetachedFromWindow()373 public void onDetachedFromWindow() { 374 mBroadcastDispatcher.unregisterReceiver(mReceiver); 375 mUserTracker.removeCallback(mUserChangedCallback); 376 } 377 onConfigurationChanged()378 protected void onConfigurationChanged() { 379 removeAllViews(); 380 inflateView(getRotation(mContext)); 381 } 382 getRotation(Context context)383 private int getRotation(Context context) { 384 Configuration config = context.getResources().getConfiguration(); 385 if (config.smallestScreenWidthDp >= 600) { 386 return ROTATION_NONE; 387 } 388 389 return RotationUtils.getRotation(context); 390 } 391 392 private final Runnable mUpdateLayoutRunnable = new Runnable() { 393 @Override 394 public void run() { 395 if (mLayout != null && mLayout.getParent() != null) { 396 mLayout.setLayoutParams(getRequestLayoutParams(getRotation(mContext))); 397 } 398 } 399 }; 400 401 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 402 @Override 403 public void onReceive(Context context, Intent intent) { 404 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { 405 post(mUpdateLayoutRunnable); 406 } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) { 407 clearPrompt(); 408 } 409 } 410 }; 411 } 412 } 413