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