• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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