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.tv.settings.inputmethod;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.hardware.input.InputDeviceIdentifier;
23 import android.hardware.input.InputManager;
24 import android.hardware.input.KeyboardLayout;
25 import android.text.TextUtils;
26 import android.view.InputDevice;
27 
28 import com.android.settingslib.R;
29 
30 import java.text.Collator;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Objects;
34 
35 /**
36  * Helper methods to retrieve information about physical keyboard devices.
37  */
38 public class PhysicalKeyboardHelper {
39 
40     /**
41      * Queries the input manager for a list of physical keyboards.
42      */
43     @NonNull
getPhysicalKeyboards( @onNull Context context)44     public static List<DeviceInfo> getPhysicalKeyboards(
45             @NonNull Context context) {
46         final List<DeviceInfo> keyboards = new ArrayList<>();
47         final InputManager im = context.getSystemService(InputManager.class);
48         if (im == null) {
49             return new ArrayList<>();
50         }
51         for (int deviceId : InputDevice.getDeviceIds()) {
52             final InputDevice device = InputDevice.getDevice(deviceId);
53             if (device == null || device.isVirtual() || !device.isFullKeyboard()) {
54                 continue;
55             }
56             final String currentLayoutDesc =
57                     im.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
58             keyboards.add(new DeviceInfo(device.getName(), device.getId(),
59                     device.getIdentifier(), currentLayoutDesc,
60                     getLayoutLabel(currentLayoutDesc, context, im)));
61         }
62 
63         // We intentionally don't reuse Comparator because Collator may not be thread-safe.
64         final Collator collator = Collator.getInstance();
65         keyboards.sort((a, b) -> {
66             int result = collator.compare(a.mDeviceName, b.mDeviceName);
67             if (result != 0) {
68                 return result;
69             }
70             result = a.mDeviceIdentifier.getDescriptor().compareTo(
71                     b.mDeviceIdentifier.getDescriptor());
72             if (result != 0) {
73                 return result;
74             }
75             return collator.compare(a.mCurrentLayoutLabel, b.mCurrentLayoutLabel);
76         });
77         return keyboards;
78     }
79 
getLayoutLabel(@ullable String currentLayoutDescriptor, @NonNull Context context, @NonNull InputManager im)80     private static String getLayoutLabel(@Nullable String currentLayoutDescriptor,
81             @NonNull Context context, @NonNull InputManager im) {
82         if (currentLayoutDescriptor == null) {
83             return context.getString(R.string.keyboard_layout_default_label);
84         }
85         final KeyboardLayout currentLayout = im.getKeyboardLayout(currentLayoutDescriptor);
86         if (currentLayout == null) {
87             return context.getString(R.string.keyboard_layout_default_label);
88         }
89         // If current layout is specified but the layout is null, just return an empty string
90         // instead of falling back to R.string.keyboard_layout_default_label.
91         return TextUtils.emptyIfNull(currentLayout.getLabel());
92     }
93 
94     /**
95      * Contains information about a physical keyboard.
96      */
97     public static final class DeviceInfo {
98         @NonNull
99         public final String mDeviceName;
100         public final int mDeviceId;
101         @NonNull
102         public final InputDeviceIdentifier mDeviceIdentifier;
103         @NonNull
104         public final String mCurrentLayoutLabel;
105         @Nullable
106         public final String mCurrentLayoutDescriptor;
107 
DeviceInfo( @ullable String deviceName, int deviceId, @NonNull InputDeviceIdentifier deviceIdentifier, @Nullable String layoutDescriptor, @NonNull String layoutLabel)108         public DeviceInfo(
109                 @Nullable String deviceName,
110                 int deviceId,
111                 @NonNull InputDeviceIdentifier deviceIdentifier,
112                 @Nullable String layoutDescriptor,
113                 @NonNull String layoutLabel) {
114             mDeviceName = TextUtils.emptyIfNull(deviceName);
115             mDeviceId = deviceId;
116             mDeviceIdentifier = deviceIdentifier;
117             mCurrentLayoutDescriptor = layoutDescriptor;
118             mCurrentLayoutLabel = layoutLabel;
119         }
120 
getSummary()121         public String getSummary() {
122             return mDeviceName + ": " + mCurrentLayoutLabel;
123         }
124 
125         @Override
equals(Object o)126         public boolean equals(Object o) {
127             if (o == this) return true;
128             if (o == null) return false;
129 
130             if (!(o instanceof DeviceInfo)) return false;
131 
132             final DeviceInfo that = (DeviceInfo) o;
133             if (!TextUtils.equals(mDeviceName, that.mDeviceName)) {
134                 return false;
135             }
136             if (!(mDeviceId == that.mDeviceId)) {
137                 return false;
138             }
139             if (!Objects.equals(mDeviceIdentifier, that.mDeviceIdentifier)) {
140                 return false;
141             }
142             if (!TextUtils.equals(mCurrentLayoutDescriptor, that.mCurrentLayoutDescriptor)) {
143                 return false;
144             }
145             if (!TextUtils.equals(mCurrentLayoutLabel, that.mCurrentLayoutLabel)) {
146                 return false;
147             }
148 
149             return true;
150         }
151 
152         @Override
hashCode()153         public int hashCode() {
154             return Objects.hash(mDeviceName, mDeviceId, mDeviceIdentifier, mCurrentLayoutLabel,
155                     mCurrentLayoutDescriptor);
156         }
157 
158         @Override
toString()159         public String toString() {
160             return "DeviceInfo: name=" + mDeviceName + ", id=" + mDeviceId
161                     + ", descriptor=" + mDeviceIdentifier.getDescriptor()
162                     + ", currentLayoutDescriptor=" + mCurrentLayoutDescriptor
163                     + ", currentLayoutLabel=" + mCurrentLayoutLabel;
164         }
165     }
166 }
167