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