Initial commit of rotation contextual button.
UI fixing todo: b/187754252
Bug: 180046394
Test: Works in basic cases with on home and
in app taskbar.
Change-Id: I69985b57e38a9fdb7935fcafb097b1e6da73a6b9
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_0.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_0.xml
new file mode 100644
index 0000000..ff5cb9e
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_0.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_90.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_90.xml
new file mode 100644
index 0000000..90fedb1
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_90.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_0.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_0.xml
new file mode 100644
index 0000000..a89e7a3
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_0.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="-1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_90.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_90.xml
new file mode 100644
index 0000000..0dc67b0
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_90.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="-1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index e680233..d32c115 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -29,7 +29,7 @@
android:layout_gravity="bottom" >
<LinearLayout
- android:id="@+id/system_button_layout"
+ android:id="@+id/nav_button_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
@@ -44,6 +44,15 @@
android:forceHasOverlappingRendering="false"
android:gravity="center" />
+ <LinearLayout
+ android:id="@+id/contextual_button_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
+ android:paddingRight="@dimen/taskbar_nav_buttons_spacing"
+ android:forceHasOverlappingRendering="false"
+ android:gravity="center" />
+
</com.android.launcher3.taskbar.TaskbarView>
<com.android.launcher3.taskbar.ImeBarView
diff --git a/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java b/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java
index 540f748..86ac39f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java
+++ b/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java
@@ -27,6 +27,7 @@
import com.android.launcher3.R;
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
+import com.android.launcher3.taskbar.contextual.RotationContextButton;
/**
* Creates Buttons for Taskbar for 3 button nav.
@@ -68,6 +69,11 @@
return getButtonForDrawable(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH);
}
+ public RotationContextButton getContextualRotation() {
+ // Rotation suggestion button
+ return new RotationContextButton(mContext);
+ }
+
private View getButtonForDrawable(@DrawableRes int drawableId, @TaskbarButton int buttonType) {
ImageView buttonView = new ImageView(mContext);
buttonView.setImageResource(drawableId);
diff --git a/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java b/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java
index 287caab..d581302 100644
--- a/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java
@@ -25,7 +25,6 @@
public class ImeBarView extends RelativeLayout {
- private ButtonProvider mButtonProvider;
private View mImeView;
public ImeBarView(Context context) {
@@ -41,8 +40,8 @@
}
public void init(ButtonProvider buttonProvider) {
- mButtonProvider = buttonProvider;
-
+ // TODO (b/187966005), maybe need to replace ime switcher button with
+ // RotationContextButton when device rotates
ActivityContext context = getActivityContext();
RelativeLayout.LayoutParams imeParams = new RelativeLayout.LayoutParams(
context.getDeviceProfile().iconSizePx,
@@ -56,13 +55,13 @@
downParams.addRule(ALIGN_PARENT_START);
// Down Arrow
- View downView = mButtonProvider.getDown();
+ View downView = buttonProvider.getDown();
downView.setLayoutParams(downParams);
downView.setRotation(-90);
addView(downView);
// IME switcher button
- mImeView = mButtonProvider.getImeSwitcher();
+ mImeView = buttonProvider.getImeSwitcher();
mImeView.setLayoutParams(imeParams);
addView(mImeView);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index ee57dd9..f124de7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -32,7 +32,6 @@
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.states.StateAnimationConfig;
-
/**
* A data source which integrates with a Launcher instance
* TODO: Rename to have Launcher prefix
@@ -51,6 +50,7 @@
private @Nullable Animator mAnimator;
private boolean mIsAnimatingToLauncher;
+ private ContextualRotationNotifier mContextualRotationNotifier;
public LauncherTaskbarUIController(
BaseQuickstepLauncher launcher, TaskbarActivityContext context) {
@@ -67,7 +67,8 @@
}
@Override
- protected void onCreate() {
+ protected void onCreate(ContextualRotationNotifier notifier) {
+ mContextualRotationNotifier = notifier;
mTaskbarStateHandler.setAnimationController(mTaskbarAnimationController);
mTaskbarAnimationController.init();
mHotseatController.init();
@@ -82,6 +83,7 @@
// End this first, in case it relies on properties that are about to be cleaned up.
mAnimator.end();
}
+ mContextualRotationNotifier = null;
mTaskbarStateHandler.setAnimationController(null);
mTaskbarAnimationController.cleanup();
mHotseatController.cleanup();
@@ -105,6 +107,9 @@
@Override
public void updateTaskbarVisibilityAlpha(float alpha) {
mTaskbarView.setAlpha(alpha);
+ if (mContextualRotationNotifier != null) {
+ mContextualRotationNotifier.onTaskbarVisibilityChanged(alpha == 1);
+ }
}
@Override
@@ -272,4 +277,8 @@
void updateTaskbarScale(float scale);
void updateTaskbarTranslationY(float translationY);
}
+
+ public interface ContextualRotationNotifier {
+ void onTaskbarVisibilityChanged(boolean showing);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 10ffefb..d51506c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -51,6 +51,7 @@
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
+import com.android.launcher3.taskbar.contextual.RotationButtonController;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Themes;
@@ -88,7 +89,9 @@
private int mLastRequestedNonFullscreenHeight;
private final SysUINavigationMode.Mode mNavMode;
+ private final SystemTaskbarNotificationManager mSystemTaskbarNotificationManager;
private final TaskbarNavButtonController mNavButtonController;
+ private final RotationButtonController mRotationButtonController;
private final boolean mIsSafeModeEnabled;
@@ -98,12 +101,43 @@
private final View.OnClickListener mOnTaskbarIconClickListener;
private final View.OnLongClickListener mOnTaskbarIconLongClickListener;
+ private final TaskbarManager.SystemTaskbarNotifier mSystemTaskbarNotifier =
+ new TaskbarManager.SystemTaskbarNotifier() {
+ @Override
+ public void updateImeStatus(int displayId, int vis, int backDisposition,
+ boolean showImeSwitcher) {
+ /*
+ * When in 3 button nav, sysui flags don't get called since we prevent
+ * sysui nav bar from instantiating at all, which is what's responsible for
+ * sending sysui state flags over.
+ */
+ mIconController.updateImeStatus(displayId, vis, showImeSwitcher);
+ }
+
+ @Override
+ public void onRotationProposal(int rotation, boolean isValid) {
+ mRotationButtonController.onRotationProposal(rotation, isValid);
+ }
+
+ @Override
+ public void disable(int displayId, int state1, int state2, boolean animate) {
+ mRotationButtonController.onDisable2FlagChanged(state2);
+ }
+
+ @Override
+ public void onSystemBarAttributesChanged(int displayId, int behavior) {
+ mRotationButtonController.onBehaviorChanged(displayId, behavior);
+ }
+ };
+
public TaskbarActivityContext(Context windowContext, DeviceProfile dp,
- TaskbarNavButtonController buttonController) {
+ TaskbarNavButtonController buttonController,
+ SystemTaskbarNotificationManager systemTaskbarNotificationManager) {
super(windowContext, Themes.getActivityThemeRes(windowContext));
mDeviceProfile = dp;
mNavButtonController = buttonController;
mNavMode = SysUINavigationMode.getMode(windowContext);
+ mSystemTaskbarNotificationManager = systemTaskbarNotificationManager;
mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> getPackageManager().isSafeMode());
@@ -118,7 +152,11 @@
mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
mDragLayer = (TaskbarDragLayer) mLayoutInflater
.inflate(R.layout.taskbar, null, false);
- mIconController = new TaskbarIconController(this, mDragLayer);
+
+ mRotationButtonController = new RotationButtonController(this,
+ R.color.popup_color_primary_light, R.color.popup_color_primary_light);
+ mIconController = new TaskbarIconController(this, mDragLayer,
+ mRotationButtonController);
Display display = windowContext.getDisplay();
Context c = display.getDisplayId() == Display.DEFAULT_DISPLAY
@@ -149,8 +187,13 @@
new int[] { ITYPE_EXTRA_NAVIGATION_BAR, ITYPE_BOTTOM_TAPPABLE_ELEMENT }
);
- mIconController.init(mOnTaskbarIconClickListener, mOnTaskbarIconLongClickListener);
+ mIconController.init(mOnTaskbarIconClickListener, mOnTaskbarIconLongClickListener,
+ mNavMode);
mWindowManager.addView(mDragLayer, mWindowLayoutParams);
+ if (mNavMode == Mode.THREE_BUTTONS) {
+ mSystemTaskbarNotificationManager
+ .registerSystemTaskbarNotifications(mSystemTaskbarNotifier);
+ }
}
public boolean canShowNavButtons() {
@@ -189,7 +232,10 @@
mUIController.onDestroy();
mUIController = uiController;
mIconController.setUIController(mUIController);
- mUIController.onCreate();
+ mUIController.onCreate(mRotationButtonController::onTaskBarVisibilityChange);
+ if (mNavMode == Mode.THREE_BUTTONS) {
+ mRotationButtonController.init();
+ }
}
/**
@@ -199,6 +245,11 @@
setUIController(TaskbarUIController.DEFAULT);
mIconController.onDestroy();
mWindowManager.removeViewImmediate(mDragLayer);
+ if (mNavMode == Mode.THREE_BUTTONS) {
+ mSystemTaskbarNotificationManager.removeSystemTaskbarNotifications(
+ mSystemTaskbarNotifier);
+ mRotationButtonController.cleanup();
+ }
}
void onNavigationButtonClick(@TaskbarButton int buttonType) {
@@ -213,16 +264,6 @@
}
/**
- * When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
- * instantiating at all, which is what's responsible for sending sysui state flags over.
- *
- * @param vis IME visibility flag
- */
- public void updateImeStatus(int displayId, int vis, boolean showImeSwitcher) {
- mIconController.updateImeStatus(displayId, vis, showImeSwitcher);
- }
-
- /**
* Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
*/
public void setTaskbarWindowFullscreen(boolean fullscreen) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java
index cf0694b..5d4b8b7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java
@@ -31,6 +31,8 @@
import com.android.launcher3.R;
import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.taskbar.contextual.RotationButtonController;
+import com.android.quickstep.SysUINavigationMode;
import com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo;
/**
@@ -45,18 +47,22 @@
private final TaskbarView mTaskbarView;
private final ImeBarView mImeBarView;
+ private final RotationButtonController mRotationButtonController;
@NonNull
private TaskbarUIController mUIController = TaskbarUIController.DEFAULT;
- TaskbarIconController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
+ TaskbarIconController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer,
+ RotationButtonController rotationButtonController) {
mActivity = activity;
mDragLayer = dragLayer;
mTaskbarView = mDragLayer.findViewById(R.id.taskbar_view);
mImeBarView = mDragLayer.findViewById(R.id.ime_bar_view);
+ mRotationButtonController = rotationButtonController;
}
- public void init(OnClickListener clickListener, OnLongClickListener longClickListener) {
+ public void init(OnClickListener clickListener, OnLongClickListener longClickListener,
+ SysUINavigationMode.Mode navMode) {
mDragLayer.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) ->
mUIController.alignRealHotseatWithTaskbar());
@@ -67,6 +73,9 @@
mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
mDragLayer.init(new TaskbarDragLayerCallbacks(), mTaskbarView);
+ if (navMode == SysUINavigationMode.Mode.THREE_BUTTONS) {
+ mRotationButtonController.setRotationButton(mTaskbarView.getContextualRotationButton());
+ }
}
public void onDestroy() {
@@ -127,6 +136,12 @@
mTaskbarView.mSystemButtonContainer, mTempRect);
insetsInfo.touchableRegion.set(mTempRect);
}
+ if (mTaskbarView.mContextualButtonContainer.getVisibility() == VISIBLE) {
+ mDragLayer.getDescendantRectRelativeToSelf(
+ mTaskbarView.mContextualButtonContainer, mTempRect);
+ insetsInfo.touchableRegion.union(mTempRect);
+ }
+
insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index d026bfb..b9acee8 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -40,11 +40,14 @@
import com.android.quickstep.SysUINavigationMode.Mode;
import com.android.quickstep.TouchInteractionService;
+import java.util.ArrayList;
+import java.util.List;
+
/**
- * Class to manager taskbar lifecycle
+ * Class to manage taskbar lifecycle
*/
public class TaskbarManager implements DisplayController.DisplayInfoChangeListener,
- SysUINavigationMode.NavigationModeChangeListener {
+ SysUINavigationMode.NavigationModeChangeListener, SystemTaskbarNotificationManager {
private final Context mContext;
private final DisplayController mDisplayController;
@@ -59,6 +62,8 @@
private boolean mUserUnlocked = false;
+ private List<SystemTaskbarNotifier> mSystemTaskbarNotifiers = new ArrayList<>();
+
public TaskbarManager(TouchInteractionService service) {
mDisplayController = DisplayController.INSTANCE.get(service);
mSysUINavigationMode = SysUINavigationMode.INSTANCE.get(service);
@@ -124,7 +129,7 @@
return;
}
mTaskbarActivityContext = new TaskbarActivityContext(
- mContext, dp.copy(mContext), mNavButtonController);
+ mContext, dp.copy(mContext), mNavButtonController, this);
mTaskbarActivityContext.init();
if (mLauncher != null) {
mTaskbarActivityContext.setUIController(
@@ -132,6 +137,9 @@
}
}
+ // TODO - I don't think this is the best place for these pass through methods,
+ // maybe directly in TaskbarIconController?
+
/**
* See {@link com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags}
* @param systemUiStateFlags The latest SystemUiStateFlags
@@ -143,6 +151,16 @@
}
}
+ public void registerSystemTaskbarNotifications(SystemTaskbarNotifier notifier) {
+ if (!mSystemTaskbarNotifiers.contains(notifier)) {
+ mSystemTaskbarNotifiers.add(notifier);
+ }
+ }
+
+ public void removeSystemTaskbarNotifications(SystemTaskbarNotifier notifier) {
+ mSystemTaskbarNotifiers.remove(notifier);
+ }
+
/**
* When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
* instantiating at all, which is what's responsible for sending sysui state flags over.
@@ -153,8 +171,26 @@
*/
public void updateImeStatus(int displayId, int vis, int backDisposition,
boolean showImeSwitcher) {
- if (mTaskbarActivityContext != null) {
- mTaskbarActivityContext.updateImeStatus(displayId, vis, showImeSwitcher);
+ for (SystemTaskbarNotifier notifier : mSystemTaskbarNotifiers) {
+ notifier.updateImeStatus(displayId, vis, backDisposition, showImeSwitcher);
+ }
+ }
+
+ public void onRotationProposal(int rotation, boolean isValid) {
+ for (SystemTaskbarNotifier notifier : mSystemTaskbarNotifiers) {
+ notifier.onRotationProposal(rotation, isValid);
+ }
+ }
+
+ public void disable(int displayId, int state1, int state2, boolean animate) {
+ for (SystemTaskbarNotifier notifier : mSystemTaskbarNotifiers) {
+ notifier.disable(displayId, state1, state2, animate);
+ }
+ }
+
+ public void onSystemBarAttributesChanged(int displayId, int behavior) {
+ for (SystemTaskbarNotifier notifier : mSystemTaskbarNotifiers) {
+ notifier.onSystemBarAttributesChanged(displayId, behavior);
}
}
@@ -166,4 +202,18 @@
mDisplayController.removeChangeListener(this);
mSysUINavigationMode.removeModeChangeListener(this);
}
+
+ public interface SystemTaskbarNotifier {
+ void updateImeStatus(int displayId, int vis, int backDisposition,
+ boolean showImeSwitcher);
+ void onRotationProposal(int rotation, boolean isValid);
+ void disable(int displayId, int state1, int state2, boolean animate);
+ void onSystemBarAttributesChanged(int displayId, int behavior);
+
+ }
}
+
+interface SystemTaskbarNotificationManager {
+ void registerSystemTaskbarNotifications(TaskbarManager.SystemTaskbarNotifier notifier);
+ void removeSystemTaskbarNotifications(TaskbarManager.SystemTaskbarNotifier notifier);
+}
\ No newline at end of file
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 50adead..f7a5618 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -27,7 +27,7 @@
*/
public void alignRealHotseatWithTaskbar() { }
- protected void onCreate() { }
+ protected void onCreate(LauncherTaskbarUIController.ContextualRotationNotifier notifier) { }
protected void onDestroy() { }
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index ac70358..5c89f8c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -42,6 +42,7 @@
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.taskbar.contextual.RotationContextButton;
import com.android.launcher3.views.ActivityContext;
/**
@@ -67,6 +68,7 @@
LinearLayout mSystemButtonContainer;
LinearLayout mHotseatIconsContainer;
+ LinearLayout mContextualButtonContainer;
// Delegate touches to the closest view if within mIconTouchSize.
private boolean mDelegateTargeted;
@@ -79,6 +81,7 @@
/** Provider of buttons added to taskbar in 3 button nav */
private ButtonProvider mButtonProvider;
+ private RotationContextButton mContextualRotationButton;
private boolean mDisableRelayout;
private boolean mAreHolesAllowed;
@@ -112,8 +115,9 @@
@Override
protected void onFinishInflate() {
super.onFinishInflate();
- mSystemButtonContainer = findViewById(R.id.system_button_layout);
+ mSystemButtonContainer = findViewById(R.id.nav_button_layout);
mHotseatIconsContainer = findViewById(R.id.hotseat_icons_layout);
+ mContextualButtonContainer = findViewById(R.id.contextual_button_layout);
}
protected void init(TaskbarIconController.TaskbarViewCallbacks callbacks,
@@ -132,6 +136,10 @@
int numHotseatIcons = mActivityContext.getDeviceProfile().numShownHotseatIcons;
updateHotseatItems(new ItemInfo[numHotseatIcons]);
+
+ if (mActivityContext.canShowNavButtons()) {
+ createContextualRegion();
+ }
}
/**
@@ -378,6 +386,16 @@
}
}
+ private void createContextualRegion() {
+ mContextualRotationButton = mButtonProvider.getContextualRotation();
+ mContextualRotationButton.setVisibility(GONE);
+ mContextualButtonContainer.addView(mContextualRotationButton);
+ }
+
+ @Nullable
+ public RotationContextButton getContextualRotationButton() {
+ return mContextualRotationButton;
+ }
// FolderIconParent implemented methods.
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButton.java b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButton.java
new file mode 100644
index 0000000..d421077
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButton.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.contextual;
+
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.view.View;
+
+/**
+ * Interface of a rotation button that interacts {@link RotationButtonController}.
+ * This interface exists because of the two different styles of rotation button in Sysui,
+ * one in contextual for 3 button nav and a floating rotation button for gestural.
+ * Keeping the interface for eventual migration of floating button, so some methods are
+ * pass through to "super" while others are trivially implemented.
+ *
+ * Changes:
+ * * Directly use AnimatedVectorDrawable instead of KeyButtonDrawable
+ */
+public interface RotationButton {
+ void setRotationButtonController(RotationButtonController rotationButtonController);
+ View getCurrentView();
+ boolean show();
+ boolean hide();
+ boolean isVisible();
+ void updateIcon(int lightIconColor, int darkIconColor);
+ void setOnClickListener(View.OnClickListener onClickListener);
+ void setOnHoverListener(View.OnHoverListener onHoverListener);
+ AnimatedVectorDrawable getImageDrawable();
+ void setDarkIntensity(float darkIntensity);
+ default boolean acceptRotationProposal() {
+ return getCurrentView() != null;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButtonController.java b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButtonController.java
new file mode 100644
index 0000000..6f6abc2
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButtonController.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.contextual;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.SuppressLint;
+import android.app.StatusBarManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.IRotationWatcher;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowInsetsController;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.internal.view.RotationPolicy;
+import com.android.launcher3.R;
+import com.android.launcher3.util.DisplayController;
+import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.recents.utilities.ViewRippler;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import java.util.Optional;
+
+/**
+ * Copied over from the SysUI equivalent class. Known issues/things not ported over
+ * * When rotation button visible and in auto-hide mode, we ask auto-hide controller to
+ * keep the navbar around longer. Will need to implement if we use auto-hide on taskbar
+ *
+ * Contains logic that deals with showing a rotate suggestion button with animation.
+ */
+public class RotationButtonController {
+
+ private static final String TAG = "StatusBar/RotationButtonController";
+ private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
+ private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
+
+ private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
+
+ private final Context mContext;
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
+ private final ViewRippler mViewRippler = new ViewRippler();
+ private final DisplayController mDisplayController;
+ private RotationButton mRotationButton;
+
+ private int mLastRotationSuggestion;
+ private boolean mPendingRotationSuggestion;
+ private boolean mHoveringRotationSuggestion;
+ private final AccessibilityManager mAccessibilityManager;
+ private final TaskStackListenerImpl mTaskStackListener;
+ private boolean mListenersRegistered = false;
+ private boolean mIsTaskbarShowing;
+ @SuppressLint("InlinedApi")
+ private @WindowInsetsController.Behavior
+ int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT;
+ private boolean mSkipOverrideUserLockPrefsOnce;
+ private final int mLightIconColor;
+ private final int mDarkIconColor;
+ private int mIconResId = R.drawable.ic_sysbar_rotate_button_ccw_start_90;
+
+ private final Runnable mRemoveRotationProposal =
+ () -> setRotateSuggestionButtonState(false /* visible */);
+ private final Runnable mCancelPendingRotationProposal =
+ () -> mPendingRotationSuggestion = false;
+ private Animator mRotateHideAnimator;
+
+
+ private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() {
+ @Override
+ public void onRotationChanged(final int rotation) throws RemoteException {
+ // We need this to be scheduled as early as possible to beat the redrawing of
+ // window in response to the orientation change.
+ mMainThreadHandler.postAtFrontOfQueue(() -> {
+ // If the screen rotation changes while locked, potentially update lock to flow with
+ // new screen rotation and hide any showing suggestions.
+ if (isRotationLocked()) {
+ if (shouldOverrideUserLockPrefs(rotation)) {
+ setRotationLockedAtAngle(rotation);
+ }
+ setRotateSuggestionButtonState(false /* visible */, true /* forced */);
+ }
+ });
+ }
+ };
+
+ /**
+ * Determines if rotation suggestions disabled2 flag exists in flag
+ * @param disable2Flags see if rotation suggestion flag exists in this flag
+ * @return whether flag exists
+ */
+ static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) {
+ return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0;
+ }
+
+ public RotationButtonController(Context context, @ColorInt int lightIconColor,
+ @ColorInt int darkIconColor) {
+ mContext = context;
+ mLightIconColor = lightIconColor;
+ mDarkIconColor = darkIconColor;
+
+ mAccessibilityManager = AccessibilityManager.getInstance(context);
+ mTaskStackListener = new TaskStackListenerImpl();
+ mDisplayController = DisplayController.INSTANCE.getNoCreate();
+ }
+
+ public void setRotationButton(RotationButton rotationButton) {
+ mRotationButton = rotationButton;
+ mRotationButton.setRotationButtonController(this);
+ mRotationButton.setOnClickListener(this::onRotateSuggestionClick);
+ mRotationButton.setOnHoverListener(this::onRotateSuggestionHover);
+ }
+
+ public void init() {
+ registerListeners();
+ if (mDisplayController.getInfo().id != DEFAULT_DISPLAY) {
+ // Currently there is no accelerometer sensor on non-default display, disable fixed
+ // rotation for non-default display
+ onDisable2FlagChanged(StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS);
+ }
+ }
+
+ public void cleanup() {
+ unregisterListeners();
+ }
+
+ private void registerListeners() {
+ if (mListenersRegistered) {
+ return;
+ }
+
+ mListenersRegistered = true;
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .watchRotation(mRotationWatcher, mDisplayController.getInfo().id);
+ } catch (IllegalArgumentException e) {
+ mListenersRegistered = false;
+ Log.w(TAG, "RegisterListeners for the display failed");
+ } catch (RemoteException e) {
+ Log.e(TAG, "RegisterListeners caught a RemoteException", e);
+ return;
+ }
+
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
+ }
+
+ void unregisterListeners() {
+ if (!mListenersRegistered) {
+ return;
+ }
+
+ mListenersRegistered = false;
+ try {
+ WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher);
+ } catch (RemoteException e) {
+ Log.e(TAG, "UnregisterListeners caught a RemoteException", e);
+ return;
+ }
+
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
+ }
+
+ void setRotationLockedAtAngle(int rotationSuggestion) {
+ RotationPolicy.setRotationLockAtAngle(mContext, true, rotationSuggestion);
+ }
+
+ public boolean isRotationLocked() {
+ return RotationPolicy.isRotationLocked(mContext);
+ }
+
+ public void setRotateSuggestionButtonState(boolean visible) {
+ setRotateSuggestionButtonState(visible, false /* force */);
+ }
+
+ void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
+ // At any point the button can become invisible because an a11y service became active.
+ // Similarly, a call to make the button visible may be rejected because an a11y service is
+ // active. Must account for this.
+ // Rerun a show animation to indicate change but don't rerun a hide animation
+ if (!visible && !mRotationButton.isVisible()) return;
+
+ final View view = mRotationButton.getCurrentView();
+ if (view == null) return;
+
+ final AnimatedVectorDrawable currentDrawable = mRotationButton.getImageDrawable();
+ if (currentDrawable == null) return;
+
+ // Clear any pending suggestion flag as it has either been nullified or is being shown
+ mPendingRotationSuggestion = false;
+ mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
+
+ // Handle the visibility change and animation
+ if (visible) { // Appear and change (cannot force)
+ // Stop and clear any currently running hide animations
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
+ mRotateHideAnimator.cancel();
+ }
+ mRotateHideAnimator = null;
+
+ // Reset the alpha if any has changed due to hide animation
+ view.setAlpha(1f);
+
+ // Run the rotate icon's animation if it has one
+ currentDrawable.reset();
+ currentDrawable.start();
+
+ // TODO(b/187754252): No idea why this doesn't work. If we remove the "false"
+ // we see the animation show the pressed state... but it only shows the first time.
+ if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
+
+ // Set visibility unless a11y service is active.
+ mRotationButton.show();
+ } else { // Hide
+ mViewRippler.stop(); // Prevent any pending ripples, force hide or not
+
+ if (force) {
+ // If a hide animator is running stop it and make invisible
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
+ mRotateHideAnimator.pause();
+ }
+ mRotationButton.hide();
+ return;
+ }
+
+ // Don't start any new hide animations if one is running
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
+
+ ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f);
+ fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
+ fadeOut.setInterpolator(LINEAR);
+ fadeOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mRotationButton.hide();
+ }
+ });
+
+ mRotateHideAnimator = fadeOut;
+ fadeOut.start();
+ }
+ }
+
+ void setDarkIntensity(float darkIntensity) {
+ mRotationButton.setDarkIntensity(darkIntensity);
+ }
+
+ public void onRotationProposal(int rotation, boolean isValid) {
+ int windowRotation = mDisplayController.getInfo().rotation;
+
+ if (!mRotationButton.acceptRotationProposal()) {
+ return;
+ }
+
+ // This method will be called on rotation suggestion changes even if the proposed rotation
+ // is not valid for the top app. Use invalid rotation choices as a signal to remove the
+ // rotate button if shown.
+ if (!isValid) {
+ setRotateSuggestionButtonState(false /* visible */);
+ return;
+ }
+
+ // If window rotation matches suggested rotation, remove any current suggestions
+ if (rotation == windowRotation) {
+ mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
+ setRotateSuggestionButtonState(false /* visible */);
+ return;
+ }
+
+ // Prepare to show the navbar icon by updating the icon style to change anim params
+ mLastRotationSuggestion = rotation; // Remember rotation for click
+ final boolean rotationCCW = Utilities.isRotationAnimationCCW(windowRotation, rotation);
+ if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
+ mIconResId = rotationCCW
+ ? R.drawable.ic_sysbar_rotate_button_ccw_start_90
+ : R.drawable.ic_sysbar_rotate_button_cw_start_90;
+ } else { // 90 or 270
+ mIconResId = rotationCCW
+ ? R.drawable.ic_sysbar_rotate_button_ccw_start_0
+ : R.drawable.ic_sysbar_rotate_button_ccw_start_0;
+ }
+ mRotationButton.updateIcon(mLightIconColor, mDarkIconColor);
+
+ if (canShowRotationButton()) {
+ // The navbar is visible / it's in visual immersive mode, so show the icon right away
+ showAndLogRotationSuggestion();
+ } else {
+ // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
+ // visible given some time limit.
+ mPendingRotationSuggestion = true;
+ mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
+ mMainThreadHandler.postDelayed(mCancelPendingRotationProposal,
+ NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
+ }
+ }
+
+ public void onDisable2FlagChanged(int state2) {
+ final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
+ if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
+ }
+
+ public void onBehaviorChanged(int displayId, @WindowInsetsController.Behavior int behavior) {
+ if (mDisplayController.getInfo().id != displayId) {
+ return;
+ }
+
+ if (mBehavior != behavior) {
+ mBehavior = behavior;
+ showPendingRotationButtonIfNeeded();
+ }
+ }
+
+ public void onTaskBarVisibilityChange(boolean showing) {
+ if (mIsTaskbarShowing != showing) {
+ mIsTaskbarShowing = showing;
+ showPendingRotationButtonIfNeeded();
+ }
+ }
+
+ private void showPendingRotationButtonIfNeeded() {
+ if (canShowRotationButton() && mPendingRotationSuggestion) {
+ showAndLogRotationSuggestion();
+ }
+ }
+
+ /** Return true when either the task bar is visible or it's in visual immersive mode. */
+ @SuppressLint("InlinedApi")
+ private boolean canShowRotationButton() {
+ return mIsTaskbarShowing || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT;
+ }
+
+ public @DrawableRes
+ int getIconResId() {
+ return mIconResId;
+ }
+
+ public @ColorInt int getLightIconColor() {
+ return mLightIconColor;
+ }
+
+ public @ColorInt int getDarkIconColor() {
+ return mDarkIconColor;
+ }
+
+ private void onRotateSuggestionClick(View v) {
+ mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED);
+ incrementNumAcceptedRotationSuggestionsIfNeeded();
+ setRotationLockedAtAngle(mLastRotationSuggestion);
+ }
+
+ private boolean onRotateSuggestionHover(View v, MotionEvent event) {
+ final int action = event.getActionMasked();
+ mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
+ || (action == MotionEvent.ACTION_HOVER_MOVE);
+ rescheduleRotationTimeout(true /* reasonHover */);
+ return false; // Must return false so a11y hover events are dispatched correctly.
+ }
+
+ private void onRotationSuggestionsDisabled() {
+ // Immediately hide the rotate button and clear any planned removal
+ setRotateSuggestionButtonState(false /* visible */, true /* force */);
+ mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
+ }
+
+ private void showAndLogRotationSuggestion() {
+ setRotateSuggestionButtonState(true /* visible */);
+ rescheduleRotationTimeout(false /* reasonHover */);
+ mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_SHOWN);
+ }
+
+ /**
+ * Makes {@link #shouldOverrideUserLockPrefs} always return {@code false} once. It is used to
+ * avoid losing original user rotation when display rotation is changed by entering the fixed
+ * orientation overview.
+ */
+ void setSkipOverrideUserLockPrefsOnce() {
+ mSkipOverrideUserLockPrefsOnce = true;
+ }
+
+ private boolean shouldOverrideUserLockPrefs(final int rotation) {
+ if (mSkipOverrideUserLockPrefsOnce) {
+ mSkipOverrideUserLockPrefsOnce = false;
+ return false;
+ }
+ // Only override user prefs when returning to the natural rotation (normally portrait).
+ // Don't let apps that force landscape or 180 alter user lock.
+ return rotation == NATURAL_ROTATION;
+ }
+
+ private void rescheduleRotationTimeout(final boolean reasonHover) {
+ // May be called due to a new rotation proposal or a change in hover state
+ if (reasonHover) {
+ // Don't reschedule if a hide animator is running
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
+ // Don't reschedule if not visible
+ if (!mRotationButton.isVisible()) return;
+ }
+
+ // Stop any pending removal
+ mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
+ // Schedule timeout
+ mMainThreadHandler.postDelayed(mRemoveRotationProposal,
+ computeRotationProposalTimeout());
+ }
+
+ private int computeRotationProposalTimeout() {
+ return mAccessibilityManager.getRecommendedTimeoutMillis(
+ mHoveringRotationSuggestion ? 16000 : 5000,
+ AccessibilityManager.FLAG_CONTENT_CONTROLS);
+ }
+
+ private boolean isRotateSuggestionIntroduced() {
+ ContentResolver cr = mContext.getContentResolver();
+ return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
+ >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
+ }
+
+ private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
+ // Get the number of accepted suggestions
+ ContentResolver cr = mContext.getContentResolver();
+ final int numSuggestions = Settings.Secure.getInt(cr,
+ Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
+
+ // Increment the number of accepted suggestions only if it would change intro mode
+ if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
+ Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
+ numSuggestions + 1);
+ }
+ }
+
+ private class TaskStackListenerImpl extends TaskStackChangeListener {
+ // Invalidate any rotation suggestion on task change or activity orientation change
+ // Note: all callbacks happen on main thread
+
+ @Override
+ public void onTaskStackChanged() {
+ setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) {
+ setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Override
+ public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
+ // Only hide the icon if the top task changes its requestedOrientation
+ // Launcher can alter its requestedOrientation while it's not on top, don't hide on this
+ Optional.ofNullable(ActivityManagerWrapper.getInstance())
+ .map(ActivityManagerWrapper::getRunningTask)
+ .ifPresent(a -> {
+ if (a.id == taskId) setRotateSuggestionButtonState(false /* visible */);
+ });
+ }
+ }
+
+ enum RotationButtonEvent implements UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "The rotation button was shown")
+ ROTATION_SUGGESTION_SHOWN(206),
+ @UiEvent(doc = "The rotation button was clicked")
+ ROTATION_SUGGESTION_ACCEPTED(207);
+
+ private final int mId;
+ RotationButtonEvent(int id) {
+ mId = id;
+ }
+ @Override public int getId() {
+ return mId;
+ }
+ }
+}
+
diff --git a/quickstep/src/com/android/launcher3/taskbar/contextual/RotationContextButton.java b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationContextButton.java
new file mode 100644
index 0000000..7ad3191
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationContextButton.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.contextual;
+
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.launcher3.R;
+
+/** Containing logic for the rotation button in nav bar. */
+public class RotationContextButton extends ImageView implements RotationButton {
+
+ private AnimatedVectorDrawable mImageDrawable;
+
+ public RotationContextButton(Context context) {
+ super(context);
+ setBackgroundResource(R.drawable.taskbar_icon_click_feedback_roundrect);
+ }
+
+ @Override
+ public void setRotationButtonController(RotationButtonController rotationButtonController) {
+ // TODO(b/187754252) UI polish, different icons based on light/dark context, etc
+ mImageDrawable = (AnimatedVectorDrawable) getContext()
+ .getDrawable(rotationButtonController.getIconResId());
+ setImageDrawable(mImageDrawable);
+ mImageDrawable.setCallback(this);
+ }
+
+ @Override
+ public View getCurrentView() {
+ return this;
+ }
+
+ @Override
+ public boolean show() {
+ setVisibility(VISIBLE);
+ return true;
+ }
+
+ @Override
+ public boolean hide() {
+ setVisibility(GONE);
+ return true;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return getVisibility() == VISIBLE;
+ }
+
+ @Override
+ public void updateIcon(int lightIconColor, int darkIconColor) {
+ // TODO(b/187754252): UI Polish
+ }
+
+ @Override
+ public void setOnClickListener(View.OnClickListener onClickListener) {
+ super.setOnClickListener(onClickListener);
+ }
+
+ @Override
+ public void setOnHoverListener(View.OnHoverListener onHoverListener) {
+ super.setOnHoverListener(onHoverListener);
+ }
+
+ @Override
+ public AnimatedVectorDrawable getImageDrawable() {
+ return mImageDrawable;
+ }
+
+ @Override
+ public void setDarkIntensity(float darkIntensity) {
+ // TODO(b/187754252) UI polish
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ if (visibility != View.VISIBLE && mImageDrawable != null) {
+ mImageDrawable.clearAnimationCallbacks();
+ mImageDrawable.reset();
+ }
+
+ // Start the rotation animation once it becomes visible
+ if (visibility == View.VISIBLE && mImageDrawable != null) {
+ mImageDrawable.reset();
+ mImageDrawable.start();
+ }
+ }
+
+ @Override
+ public boolean acceptRotationProposal() {
+ return isAttachedToWindow();
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index e52405b..afafce7 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -261,7 +261,7 @@
}
@Override
- public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
+ public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
WindowBounds wb = new WindowBounds(bounds, insets);
MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
}
@@ -269,8 +269,34 @@
@Override
public void onImeWindowStatusChanged(int displayId, IBinder token, int vis,
int backDisposition, boolean showImeSwitcher) {
- MAIN_EXECUTOR.execute(() -> mTaskbarManager.updateImeStatus(
- displayId, vis, backDisposition, showImeSwitcher));
+ executeForTaskbarManager(() -> mTaskbarManager
+ .updateImeStatus(displayId, vis, backDisposition, showImeSwitcher));
+ }
+
+ @Override
+ public void onRotationProposal(int rotation, boolean isValid) {
+ executeForTaskbarManager(() -> mTaskbarManager.onRotationProposal(rotation, isValid));
+ }
+
+ @Override
+ public void disable(int displayId, int state1, int state2, boolean animate) {
+ executeForTaskbarManager(() -> mTaskbarManager
+ .disable(displayId, state1, state2, animate));
+ }
+
+ @Override
+ public void onSystemBarAttributesChanged(int displayId, int behavior) {
+ executeForTaskbarManager(() -> mTaskbarManager
+ .onSystemBarAttributesChanged(displayId, behavior));
+ }
+
+ private void executeForTaskbarManager(final Runnable r) {
+ MAIN_EXECUTOR.execute(() -> {
+ if (mTaskbarManager == null) {
+ return;
+ }
+ r.run();
+ });
}
public TaskbarManager getTaskbarManager() {
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java b/quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java
new file mode 100644
index 0000000..af5819a
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.view.View;
+import android.view.WindowInsetsController;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.taskbar.contextual.RotationButton;
+import com.android.launcher3.taskbar.contextual.RotationButtonController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+/** SysUI equivalent */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavigationBarRotationContextTest {
+ private static final int DEFAULT_ROTATE = 0;
+ private static final int DEFAULT_DISPLAY = 0;
+
+
+ private RotationButtonController mRotationButtonController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ Context mTargetContext = InstrumentationRegistry.getTargetContext();
+ final View view = new View(mTargetContext);
+ RotationButton rotationButton = mock(RotationButton.class);
+ mRotationButtonController = new RotationButtonController(mTargetContext, 0, 0);
+ mRotationButtonController.setRotationButton(rotationButton);
+ // Due to a mockito issue, only spy the object after setting the initial state
+ mRotationButtonController = spy(mRotationButtonController);
+ final AnimatedVectorDrawable kbd = mock(AnimatedVectorDrawable.class);
+ doReturn(view).when(rotationButton).getCurrentView();
+ doReturn(true).when(rotationButton).acceptRotationProposal();
+ }
+
+ @Test
+ public void testOnInvalidRotationProposal() {
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE + 1,
+ false /* isValid */);
+ verify(mRotationButtonController, times(1))
+ .setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Test
+ public void testOnSameRotationProposal() {
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE,
+ true /* isValid */);
+ verify(mRotationButtonController, times(1))
+ .setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Test
+ public void testOnRotationProposalShowButtonShowNav() {
+ // No navigation bar should not call to set visibility state
+ mRotationButtonController.onBehaviorChanged(DEFAULT_DISPLAY,
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ mRotationButtonController.onTaskBarVisibilityChange(false /* showing */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ false /* visible */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ true /* visible */);
+
+ // No navigation bar with rotation change should not call to set visibility state
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE + 1,
+ true /* isValid */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ false /* visible */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ true /* visible */);
+
+ // Since rotation has changed rotation should be pending, show mButton when showing nav bar
+ mRotationButtonController.onTaskBarVisibilityChange(true /* showing */);
+ verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
+ true /* visible */);
+ }
+
+ @Test
+ public void testOnRotationProposalShowButton() {
+ // Navigation bar being visible should not call to set visibility state
+ mRotationButtonController.onTaskBarVisibilityChange(true /* showing */);
+ verify(mRotationButtonController, times(0))
+ .setRotateSuggestionButtonState(false /* visible */);
+ verify(mRotationButtonController, times(0))
+ .setRotateSuggestionButtonState(true /* visible */);
+
+ // Navigation bar is visible and rotation requested
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE + 1,
+ true /* isValid */);
+ verify(mRotationButtonController, times(1))
+ .setRotateSuggestionButtonState(true /* visible */);
+ }
+}