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