1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.car.ui.utils;
17 
18 import android.annotation.TargetApi;
19 import android.content.Context;
20 import android.text.TextUtils;
21 import android.view.View;
22 import android.view.accessibility.AccessibilityEvent;
23 import android.view.accessibility.AccessibilityManager;
24 import android.view.accessibility.AccessibilityNodeInfo;
25 
26 import androidx.annotation.NonNull;
27 
28 import java.lang.reflect.Method;
29 
30 /** Helper class to toggle direct manipulation mode. */
31 public final class DirectManipulationHelper {
32 
33     /**
34      * StateDescription for a {@link View} to support direct manipulation mode. It's also used as
35      * class name of {@link AccessibilityEvent} to indicate that the AccessibilityEvent represents
36      * a request to toggle direct manipulation mode.
37      */
38     private static final String DIRECT_MANIPULATION =
39             "com.android.car.ui.utils.DIRECT_MANIPULATION";
40 
41     /** This is a utility class. */
DirectManipulationHelper()42     private DirectManipulationHelper() {
43     }
44 
45     /**
46      * Enables or disables direct manipulation mode. This method sends an {@link AccessibilityEvent}
47      * to tell {@link com.android.car.rotary.RotaryService} to enter or exit direct manipulation
48      * mode. Typically pressing the center button of the rotary controller with a direct
49      * manipulation view focused will enter direct manipulation mode, while pressing the Back button
50      * will exit direct manipulation mode.
51      *
52      * @param view   the direct manipulation view
53      * @param enable true to enter direct manipulation mode, false to exit direct manipulation mode
54      * @return whether the AccessibilityEvent was sent
55      */
enableDirectManipulationMode(@onNull View view, boolean enable)56     public static boolean enableDirectManipulationMode(@NonNull View view, boolean enable) {
57         AccessibilityManager accessibilityManager = (AccessibilityManager)
58                 view.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
59         if (accessibilityManager == null || !accessibilityManager.isEnabled()) {
60             return false;
61         }
62         AccessibilityEvent event = AccessibilityEvent.obtain();
63         event.setClassName(DIRECT_MANIPULATION);
64         event.setSource(view);
65         event.setEventType(enable
66                 ? AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
67                 : AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
68         accessibilityManager.sendAccessibilityEvent(event);
69         return true;
70     }
71 
72     /** Returns whether the given {@code event} is for direct manipulation. */
isDirectManipulation(@onNull AccessibilityEvent event)73     public static boolean isDirectManipulation(@NonNull AccessibilityEvent event) {
74         return TextUtils.equals(DIRECT_MANIPULATION, event.getClassName());
75     }
76 
77     /** Returns whether the given {@code node} supports direct manipulation. */
78     @TargetApi(30)
supportDirectManipulation(@onNull AccessibilityNodeInfo node)79     public static boolean supportDirectManipulation(@NonNull AccessibilityNodeInfo node) {
80         try {
81             // TODO(b/156115044): remove the reflection once Android R sdk is publicly released.
82             Method getStateDescription =
83                     AccessibilityNodeInfo.class.getMethod("getStateDescription");
84             CharSequence stateDescription = (CharSequence) getStateDescription.invoke(node);
85             return TextUtils.equals(DIRECT_MANIPULATION, stateDescription);
86         } catch (ReflectiveOperationException e) {
87             throw new RuntimeException(e);
88         }
89     }
90 
91     /** Sets whether the given {@code view} supports direct manipulation. */
92     @TargetApi(30)
setSupportsDirectManipulation(@onNull View view, boolean enable)93     public static void setSupportsDirectManipulation(@NonNull View view, boolean enable) {
94         try {
95             // TODO(b/156115044): remove the reflection once Android R sdk is publicly released.
96             Method setStateDescription =
97                     View.class.getMethod("setStateDescription", CharSequence.class);
98             CharSequence stateDescription = enable ? DIRECT_MANIPULATION : null;
99             setStateDescription.invoke(view, stateDescription);
100         } catch (ReflectiveOperationException e) {
101             throw new RuntimeException(e);
102         }
103     }
104 }
105