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