1 /*
2  * Copyright (C) 2022 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.accessibility.floatingmenu;
18 
19 import static android.view.View.GONE;
20 import static android.view.View.VISIBLE;
21 import static android.view.WindowInsets.Type.displayCutout;
22 import static android.view.WindowInsets.Type.ime;
23 import static android.view.WindowInsets.Type.systemBars;
24 
25 import static com.android.systemui.accessibility.floatingmenu.MenuViewLayer.LayerIndex;
26 
27 import static com.google.common.truth.Truth.assertThat;
28 
29 import static org.mockito.Mockito.doReturn;
30 import static org.mockito.Mockito.spy;
31 import static org.mockito.Mockito.verify;
32 import static org.mockito.Mockito.when;
33 
34 import android.accessibilityservice.AccessibilityServiceInfo;
35 import android.content.ComponentName;
36 import android.content.pm.ApplicationInfo;
37 import android.content.pm.ResolveInfo;
38 import android.content.pm.ServiceInfo;
39 import android.graphics.Insets;
40 import android.graphics.PointF;
41 import android.graphics.Rect;
42 import android.os.Build;
43 import android.os.UserHandle;
44 import android.provider.Settings;
45 import android.testing.AndroidTestingRunner;
46 import android.testing.TestableLooper;
47 import android.view.View;
48 import android.view.WindowInsets;
49 import android.view.WindowManager;
50 import android.view.WindowMetrics;
51 import android.view.accessibility.AccessibilityManager;
52 
53 import androidx.test.filters.SmallTest;
54 
55 import com.android.systemui.SysuiTestCase;
56 import com.android.systemui.util.settings.SecureSettings;
57 
58 import org.junit.After;
59 import org.junit.Before;
60 import org.junit.Rule;
61 import org.junit.Test;
62 import org.junit.runner.RunWith;
63 import org.mockito.Mock;
64 import org.mockito.junit.MockitoJUnit;
65 import org.mockito.junit.MockitoRule;
66 
67 import java.util.ArrayList;
68 import java.util.List;
69 
70 /** Tests for {@link MenuViewLayer}. */
71 @RunWith(AndroidTestingRunner.class)
72 @TestableLooper.RunWithLooper(setAsMainLooper = true)
73 @SmallTest
74 public class MenuViewLayerTest extends SysuiTestCase {
75     private static final String SELECT_TO_SPEAK_PACKAGE_NAME = "com.google.android.marvin.talkback";
76     private static final String SELECT_TO_SPEAK_SERVICE_NAME =
77             "com.google.android.accessibility.selecttospeak.SelectToSpeakService";
78     private static final ComponentName TEST_SELECT_TO_SPEAK_COMPONENT_NAME = new ComponentName(
79             SELECT_TO_SPEAK_PACKAGE_NAME, SELECT_TO_SPEAK_SERVICE_NAME);
80 
81     private static final int DISPLAY_WINDOW_WIDTH = 1080;
82     private static final int DISPLAY_WINDOW_HEIGHT = 2340;
83     private static final int STATUS_BAR_HEIGHT = 75;
84     private static final int NAVIGATION_BAR_HEIGHT = 125;
85     private static final int IME_HEIGHT = 350;
86     private static final int IME_TOP =
87             DISPLAY_WINDOW_HEIGHT - STATUS_BAR_HEIGHT - NAVIGATION_BAR_HEIGHT - IME_HEIGHT;
88 
89     private MenuViewLayer mMenuViewLayer;
90     private String mLastAccessibilityButtonTargets;
91     private String mLastEnabledAccessibilityServices;
92     private WindowMetrics mWindowMetrics;
93     private MenuView mMenuView;
94     private MenuAnimationController mMenuAnimationController;
95 
96     @Rule
97     public MockitoRule mockito = MockitoJUnit.rule();
98 
99     @Mock
100     private IAccessibilityFloatingMenu mFloatingMenu;
101 
102     @Mock
103     private SecureSettings mSecureSettings;
104 
105     @Mock
106     private WindowManager mStubWindowManager;
107 
108     @Mock
109     private AccessibilityManager mStubAccessibilityManager;
110 
111     @Before
setUp()112     public void setUp() throws Exception {
113         final Rect mDisplayBounds = new Rect();
114         mDisplayBounds.set(/* left= */ 0, /* top= */ 0, DISPLAY_WINDOW_WIDTH,
115                 DISPLAY_WINDOW_HEIGHT);
116         mWindowMetrics = spy(new WindowMetrics(mDisplayBounds, fakeDisplayInsets()));
117         doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics();
118 
119         mMenuViewLayer = new MenuViewLayer(mContext, mStubWindowManager, mStubAccessibilityManager,
120                 mFloatingMenu, mSecureSettings);
121         mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
122         mMenuAnimationController = mMenuView.getMenuAnimationController();
123 
124         mLastAccessibilityButtonTargets =
125                 Settings.Secure.getStringForUser(mContext.getContentResolver(),
126                         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, UserHandle.USER_CURRENT);
127         mLastEnabledAccessibilityServices =
128                 Settings.Secure.getStringForUser(mContext.getContentResolver(),
129                         Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, UserHandle.USER_CURRENT);
130 
131         mMenuViewLayer.onAttachedToWindow();
132         Settings.Secure.putStringForUser(mContext.getContentResolver(),
133                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, "", UserHandle.USER_CURRENT);
134         Settings.Secure.putStringForUser(mContext.getContentResolver(),
135                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, "", UserHandle.USER_CURRENT);
136     }
137 
138     @After
tearDown()139     public void tearDown() throws Exception {
140         Settings.Secure.putStringForUser(mContext.getContentResolver(),
141                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, mLastAccessibilityButtonTargets,
142                 UserHandle.USER_CURRENT);
143         Settings.Secure.putStringForUser(mContext.getContentResolver(),
144                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, mLastEnabledAccessibilityServices,
145                 UserHandle.USER_CURRENT);
146 
147         mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false);
148         mMenuViewLayer.onDetachedFromWindow();
149     }
150 
151     @Test
onAttachedToWindow_menuIsVisible()152     public void onAttachedToWindow_menuIsVisible() {
153         mMenuViewLayer.onAttachedToWindow();
154         final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
155 
156         assertThat(menuView.getVisibility()).isEqualTo(VISIBLE);
157     }
158 
159     @Test
onAttachedToWindow_menuIsGone()160     public void onAttachedToWindow_menuIsGone() {
161         mMenuViewLayer.onDetachedFromWindow();
162         final View menuView = mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW);
163 
164         assertThat(menuView.getVisibility()).isEqualTo(GONE);
165     }
166 
167     @Test
triggerDismissMenuAction_hideFloatingMenu()168     public void triggerDismissMenuAction_hideFloatingMenu() {
169         mMenuViewLayer.mDismissMenuAction.run();
170 
171         verify(mFloatingMenu).hide();
172     }
173 
174     @Test
triggerDismissMenuAction_matchA11yButtonTargetsResult()175     public void triggerDismissMenuAction_matchA11yButtonTargetsResult() {
176         mMenuViewLayer.mDismissMenuAction.run();
177         verify(mSecureSettings).putStringForUser(
178                 Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, /* value= */ "",
179                 UserHandle.USER_CURRENT);
180     }
181 
182     @Test
triggerDismissMenuAction_matchEnabledA11yServicesResult()183     public void triggerDismissMenuAction_matchEnabledA11yServicesResult() {
184         setupEnabledAccessibilityServiceList();
185 
186         mMenuViewLayer.mDismissMenuAction.run();
187         final String value = Settings.Secure.getString(mContext.getContentResolver(),
188                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
189 
190         assertThat(value).isEqualTo("");
191     }
192 
193     @Test
triggerDismissMenuAction_hasHardwareKeyShortcut_keepEnabledStatus()194     public void triggerDismissMenuAction_hasHardwareKeyShortcut_keepEnabledStatus() {
195         setupEnabledAccessibilityServiceList();
196         final List<String> stubShortcutTargets = new ArrayList<>();
197         stubShortcutTargets.add(TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
198         when(mStubAccessibilityManager.getAccessibilityShortcutTargets(
199                 AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY)).thenReturn(stubShortcutTargets);
200 
201         mMenuViewLayer.mDismissMenuAction.run();
202         final String value = Settings.Secure.getString(mContext.getContentResolver(),
203                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
204 
205         assertThat(value).isEqualTo(TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
206     }
207 
208     @Test
showingImeInsetsChange_notOverlapOnIme_menuKeepOriginalPosition()209     public void showingImeInsetsChange_notOverlapOnIme_menuKeepOriginalPosition() {
210         final float menuTop = STATUS_BAR_HEIGHT + 100;
211         mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
212 
213         dispatchShowingImeInsets();
214 
215         assertThat(mMenuView.getTranslationX()).isEqualTo(0);
216         assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop);
217     }
218 
219     @Test
showingImeInsetsChange_overlapOnIme_menuShownAboveIme()220     public void showingImeInsetsChange_overlapOnIme_menuShownAboveIme() {
221         final float menuTop = IME_TOP + 100;
222         mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
223 
224         dispatchShowingImeInsets();
225 
226         final float menuBottom = mMenuView.getTranslationY() + mMenuView.getMenuHeight();
227         assertThat(mMenuView.getTranslationX()).isEqualTo(0);
228         assertThat(menuBottom).isLessThan(IME_TOP);
229     }
230 
231     @Test
hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition()232     public void hidingImeInsetsChange_overlapOnIme_menuBackToOriginalPosition() {
233         final float menuTop = IME_TOP + 200;
234         mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop));
235         dispatchShowingImeInsets();
236 
237         dispatchHidingImeInsets();
238 
239         assertThat(mMenuView.getTranslationX()).isEqualTo(0);
240         assertThat(mMenuView.getTranslationY()).isEqualTo(menuTop);
241     }
242 
setupEnabledAccessibilityServiceList()243     private void setupEnabledAccessibilityServiceList() {
244         Settings.Secure.putString(mContext.getContentResolver(),
245                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES,
246                 TEST_SELECT_TO_SPEAK_COMPONENT_NAME.flattenToString());
247 
248         final ResolveInfo resolveInfo = new ResolveInfo();
249         final ServiceInfo serviceInfo = new ServiceInfo();
250         final ApplicationInfo applicationInfo = new ApplicationInfo();
251         resolveInfo.serviceInfo = serviceInfo;
252         serviceInfo.applicationInfo = applicationInfo;
253         applicationInfo.targetSdkVersion = Build.VERSION_CODES.R;
254         final AccessibilityServiceInfo accessibilityServiceInfo = new AccessibilityServiceInfo();
255         accessibilityServiceInfo.setResolveInfo(resolveInfo);
256         accessibilityServiceInfo.flags = AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON;
257         final List<AccessibilityServiceInfo> serviceInfoList = new ArrayList<>();
258         accessibilityServiceInfo.setComponentName(TEST_SELECT_TO_SPEAK_COMPONENT_NAME);
259         serviceInfoList.add(accessibilityServiceInfo);
260         when(mStubAccessibilityManager.getEnabledAccessibilityServiceList(
261                 AccessibilityServiceInfo.FEEDBACK_ALL_MASK)).thenReturn(serviceInfoList);
262     }
263 
dispatchShowingImeInsets()264     private void dispatchShowingImeInsets() {
265         final WindowInsets fakeShowingImeInsets = fakeImeInsets(/* isImeVisible= */ true);
266         doReturn(fakeShowingImeInsets).when(mWindowMetrics).getWindowInsets();
267         mMenuViewLayer.dispatchApplyWindowInsets(fakeShowingImeInsets);
268     }
269 
dispatchHidingImeInsets()270     private void dispatchHidingImeInsets() {
271         final WindowInsets fakeHidingImeInsets = fakeImeInsets(/* isImeVisible= */ false);
272         doReturn(fakeHidingImeInsets).when(mWindowMetrics).getWindowInsets();
273         mMenuViewLayer.dispatchApplyWindowInsets(fakeHidingImeInsets);
274     }
275 
fakeDisplayInsets()276     private WindowInsets fakeDisplayInsets() {
277         return new WindowInsets.Builder()
278                 .setVisible(systemBars() | displayCutout(), /* visible= */ true)
279                 .setInsets(systemBars() | displayCutout(),
280                         Insets.of(/* left= */ 0, STATUS_BAR_HEIGHT, /* right= */ 0,
281                                 NAVIGATION_BAR_HEIGHT))
282                 .build();
283     }
284 
fakeImeInsets(boolean isImeVisible)285     private WindowInsets fakeImeInsets(boolean isImeVisible) {
286         final int bottom = isImeVisible ? (IME_HEIGHT + NAVIGATION_BAR_HEIGHT) : 0;
287         return new WindowInsets.Builder()
288                 .setVisible(ime(), isImeVisible)
289                 .setInsets(ime(),
290                         Insets.of(/* left= */ 0, /* top= */ 0, /* right= */ 0, bottom))
291                 .build();
292     }
293 }
294