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