1 /*
2  * Copyright (C) 2020 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.shared.rotation;
18 
19 import android.annotation.DimenRes;
20 import android.annotation.IdRes;
21 import android.annotation.LayoutRes;
22 import android.annotation.StringRes;
23 import android.content.Context;
24 import android.content.pm.ActivityInfo;
25 import android.content.pm.ActivityInfo.Config;
26 import android.content.res.Resources;
27 import android.graphics.PixelFormat;
28 import android.graphics.drawable.AnimatedVectorDrawable;
29 import android.graphics.drawable.Drawable;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.WindowManager;
34 import android.view.WindowManager.LayoutParams;
35 import android.view.animation.AccelerateDecelerateInterpolator;
36 import android.widget.FrameLayout;
37 
38 import androidx.annotation.BoolRes;
39 import androidx.core.view.OneShotPreDrawListener;
40 
41 import com.android.systemui.shared.rotation.FloatingRotationButtonPositionCalculator.Position;
42 
43 /**
44  * Containing logic for the rotation button on the physical left bottom corner of the screen.
45  */
46 public class FloatingRotationButton implements RotationButton {
47 
48     private static final int MARGIN_ANIMATION_DURATION_MILLIS = 300;
49 
50     private final WindowManager mWindowManager;
51     private final ViewGroup mKeyButtonContainer;
52     private final FloatingRotationButtonView mKeyButtonView;
53 
54     private int mContainerSize;
55     private final Context mContext;
56 
57     @StringRes
58     private final int mContentDescriptionResource;
59     @DimenRes
60     private final int mMinMarginResource;
61     @DimenRes
62     private final int mRoundedContentPaddingResource;
63     @DimenRes
64     private final int mTaskbarLeftMarginResource;
65     @DimenRes
66     private final int mTaskbarBottomMarginResource;
67     @DimenRes
68     private final int mButtonDiameterResource;
69     @BoolRes
70     private final int mFloatingRotationBtnPositionLeftResource;
71 
72     private AnimatedVectorDrawable mAnimatedDrawable;
73     private boolean mIsShowing;
74     private int mDisplayRotation;
75 
76     private boolean mIsTaskbarVisible = false;
77     private boolean mIsTaskbarStashed = false;
78 
79     private FloatingRotationButtonPositionCalculator mPositionCalculator;
80 
81     private RotationButtonController mRotationButtonController;
82     private RotationButtonUpdatesCallback mUpdatesCallback;
83     private Position mPosition;
84 
FloatingRotationButton(Context context, @StringRes int contentDescriptionResource, @LayoutRes int layout, @IdRes int keyButtonId, @DimenRes int minMargin, @DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin, @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter, @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource)85     public FloatingRotationButton(Context context, @StringRes int contentDescriptionResource,
86             @LayoutRes int layout, @IdRes int keyButtonId, @DimenRes int minMargin,
87             @DimenRes int roundedContentPadding, @DimenRes int taskbarLeftMargin,
88             @DimenRes int taskbarBottomMargin, @DimenRes int buttonDiameter,
89             @DimenRes int rippleMaxWidth, @BoolRes int floatingRotationBtnPositionLeftResource) {
90         mWindowManager = context.getSystemService(WindowManager.class);
91         mKeyButtonContainer = (ViewGroup) LayoutInflater.from(context).inflate(layout, null);
92         mKeyButtonView = mKeyButtonContainer.findViewById(keyButtonId);
93         mKeyButtonView.setVisibility(View.VISIBLE);
94         mKeyButtonView.setContentDescription(context.getString(contentDescriptionResource));
95         mKeyButtonView.setRipple(rippleMaxWidth);
96 
97         mContext = context;
98 
99         mContentDescriptionResource = contentDescriptionResource;
100         mMinMarginResource = minMargin;
101         mRoundedContentPaddingResource = roundedContentPadding;
102         mTaskbarLeftMarginResource = taskbarLeftMargin;
103         mTaskbarBottomMarginResource = taskbarBottomMargin;
104         mButtonDiameterResource = buttonDiameter;
105         mFloatingRotationBtnPositionLeftResource = floatingRotationBtnPositionLeftResource;
106 
107         updateDimensionResources();
108     }
109 
updateDimensionResources()110     private void updateDimensionResources() {
111         Resources res = mContext.getResources();
112 
113         int defaultMargin = Math.max(
114                 res.getDimensionPixelSize(mMinMarginResource),
115                 res.getDimensionPixelSize(mRoundedContentPaddingResource));
116 
117         int taskbarMarginLeft =
118                 res.getDimensionPixelSize(mTaskbarLeftMarginResource);
119         int taskbarMarginBottom =
120                 res.getDimensionPixelSize(mTaskbarBottomMarginResource);
121 
122         boolean floatingRotationButtonPositionLeft =
123                 res.getBoolean(mFloatingRotationBtnPositionLeftResource);
124 
125         mPositionCalculator = new FloatingRotationButtonPositionCalculator(defaultMargin,
126                 taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft);
127 
128         final int diameter = res.getDimensionPixelSize(mButtonDiameterResource);
129         mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft,
130                 taskbarMarginBottom));
131     }
132 
133     @Override
setRotationButtonController(RotationButtonController rotationButtonController)134     public void setRotationButtonController(RotationButtonController rotationButtonController) {
135         mRotationButtonController = rotationButtonController;
136         updateIcon(mRotationButtonController.getLightIconColor(),
137                 mRotationButtonController.getDarkIconColor());
138     }
139 
140     @Override
setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback)141     public void setUpdatesCallback(RotationButtonUpdatesCallback updatesCallback) {
142         mUpdatesCallback = updatesCallback;
143     }
144 
145     @Override
getCurrentView()146     public View getCurrentView() {
147         return mKeyButtonView;
148     }
149 
150     @Override
show()151     public boolean show() {
152         if (mIsShowing) {
153             return false;
154         }
155 
156         mIsShowing = true;
157 
158         final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams();
159         mWindowManager.addView(mKeyButtonContainer, layoutParams);
160 
161         if (mAnimatedDrawable != null) {
162             mAnimatedDrawable.reset();
163             mAnimatedDrawable.start();
164         }
165 
166         // Notify about visibility only after first traversal so we can properly calculate
167         // the touch region for the button
168         OneShotPreDrawListener.add(mKeyButtonView, () -> {
169             if (mIsShowing && mUpdatesCallback != null) {
170                 mUpdatesCallback.onVisibilityChanged(true);
171             }
172         });
173 
174         return true;
175     }
176 
177     @Override
hide()178     public boolean hide() {
179         if (!mIsShowing) {
180             return false;
181         }
182         mWindowManager.removeViewImmediate(mKeyButtonContainer);
183         mIsShowing = false;
184         if (mUpdatesCallback != null) {
185             mUpdatesCallback.onVisibilityChanged(false);
186         }
187         return true;
188     }
189 
190     @Override
isVisible()191     public boolean isVisible() {
192         return mIsShowing;
193     }
194 
195     @Override
updateIcon(int lightIconColor, int darkIconColor)196     public void updateIcon(int lightIconColor, int darkIconColor) {
197         mAnimatedDrawable = (AnimatedVectorDrawable) mKeyButtonView.getContext()
198                 .getDrawable(mRotationButtonController.getIconResId());
199         mKeyButtonView.setImageDrawable(mAnimatedDrawable);
200         mKeyButtonView.setColors(lightIconColor, darkIconColor);
201     }
202 
203     @Override
setOnClickListener(View.OnClickListener onClickListener)204     public void setOnClickListener(View.OnClickListener onClickListener) {
205         mKeyButtonView.setOnClickListener(onClickListener);
206     }
207 
208     @Override
setOnHoverListener(View.OnHoverListener onHoverListener)209     public void setOnHoverListener(View.OnHoverListener onHoverListener) {
210         mKeyButtonView.setOnHoverListener(onHoverListener);
211     }
212 
213     @Override
getImageDrawable()214     public Drawable getImageDrawable() {
215         return mAnimatedDrawable;
216     }
217 
218     @Override
setDarkIntensity(float darkIntensity)219     public void setDarkIntensity(float darkIntensity) {
220         mKeyButtonView.setDarkIntensity(darkIntensity);
221     }
222 
223     @Override
onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed)224     public void onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed) {
225         mIsTaskbarVisible = taskbarVisible;
226         mIsTaskbarStashed = taskbarStashed;
227 
228         if (!mIsShowing) return;
229 
230         final Position newPosition = mPositionCalculator
231                 .calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed);
232 
233         if (newPosition.getTranslationX() != mPosition.getTranslationX()
234                 || newPosition.getTranslationY() != mPosition.getTranslationY()) {
235             updateTranslation(newPosition, /* animate */ true);
236             mPosition = newPosition;
237         }
238     }
239 
240     /**
241      * Updates resources that could be changed in runtime, should be called on configuration
242      * change with changes diff integer mask
243      * @param configurationChanges - configuration changes with flags from ActivityInfo e.g.
244      * {@link android.content.pm.ActivityInfo#CONFIG_DENSITY}
245      */
onConfigurationChanged(@onfig int configurationChanges)246     public void onConfigurationChanged(@Config int configurationChanges) {
247         if ((configurationChanges & ActivityInfo.CONFIG_DENSITY) != 0
248                 || (configurationChanges & ActivityInfo.CONFIG_SCREEN_SIZE) != 0) {
249             updateDimensionResources();
250 
251             if (mIsShowing) {
252                 final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams();
253                 mWindowManager.updateViewLayout(mKeyButtonContainer, layoutParams);
254             }
255         }
256 
257         if ((configurationChanges & ActivityInfo.CONFIG_LOCALE) != 0) {
258             mKeyButtonView.setContentDescription(mContext.getString(mContentDescriptionResource));
259         }
260     }
261 
adjustViewPositionAndCreateLayoutParams()262     private LayoutParams adjustViewPositionAndCreateLayoutParams() {
263         final LayoutParams lp = new LayoutParams(
264                 mContainerSize,
265                 mContainerSize,
266                 /* xpos */ 0, /* ypos */ 0, LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
267                 LayoutParams.FLAG_NOT_FOCUSABLE,
268                 PixelFormat.TRANSLUCENT);
269 
270         lp.privateFlags |= LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
271         lp.setTitle("FloatingRotationButton");
272         lp.setFitInsetsTypes(/* types */ 0);
273 
274         mDisplayRotation = mWindowManager.getDefaultDisplay().getRotation();
275         mPosition = mPositionCalculator
276                 .calculatePosition(mDisplayRotation, mIsTaskbarVisible, mIsTaskbarStashed);
277 
278         lp.gravity = mPosition.getGravity();
279         ((FrameLayout.LayoutParams) mKeyButtonView.getLayoutParams()).gravity =
280                 mPosition.getGravity();
281 
282         updateTranslation(mPosition, /* animate */ false);
283 
284         return lp;
285     }
286 
updateTranslation(Position position, boolean animate)287     private void updateTranslation(Position position, boolean animate) {
288         final int translationX = position.getTranslationX();
289         final int translationY = position.getTranslationY();
290 
291         if (animate) {
292             mKeyButtonView
293                     .animate()
294                     .translationX(translationX)
295                     .translationY(translationY)
296                     .setDuration(MARGIN_ANIMATION_DURATION_MILLIS)
297                     .setInterpolator(new AccelerateDecelerateInterpolator())
298                     .withEndAction(() -> {
299                         if (mUpdatesCallback != null && mIsShowing) {
300                             mUpdatesCallback.onPositionChanged();
301                         }
302                     })
303                     .start();
304         } else {
305             mKeyButtonView.setTranslationX(translationX);
306             mKeyButtonView.setTranslationY(translationY);
307         }
308     }
309 }
310