1 /*
2  * Copyright (C) 2017 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.settings.intelligence.search.query;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ServiceInfo;
24 import android.icu.text.ListFormatter;
25 import androidx.annotation.NonNull;
26 import androidx.annotation.VisibleForTesting;
27 import android.util.Log;
28 import android.view.InputDevice;
29 import android.view.inputmethod.InputMethodInfo;
30 import android.view.inputmethod.InputMethodManager;
31 import android.view.inputmethod.InputMethodSubtype;
32 
33 import com.android.settings.intelligence.R;
34 import com.android.settings.intelligence.nano.SettingsIntelligenceLogProto;
35 import com.android.settings.intelligence.search.ResultPayload;
36 import com.android.settings.intelligence.search.SearchFeatureProvider;
37 import com.android.settings.intelligence.search.SearchResult;
38 import com.android.settings.intelligence.search.indexing.DatabaseIndexingUtils;
39 import com.android.settings.intelligence.search.sitemap.SiteMapManager;
40 
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Set;
47 
48 public class InputDeviceResultTask extends SearchQueryTask.QueryWorker {
49 
50     private static final String TAG = "InputResultFutureTask";
51 
52     public static final int QUERY_WORKER_ID =
53             SettingsIntelligenceLogProto.SettingsIntelligenceEvent.SEARCH_QUERY_INPUT_DEVICES;
54 
55     @VisibleForTesting
56     static final String PHYSICAL_KEYBOARD_FRAGMENT =
57             "com.android.settings.inputmethod.PhysicalKeyboardFragment";
58     @VisibleForTesting
59     static final String VIRTUAL_KEYBOARD_FRAGMENT =
60             "com.android.settings.inputmethod.AvailableVirtualKeyboardFragment";
61 
newTask(Context context, SiteMapManager manager, String query)62     public static SearchQueryTask newTask(Context context, SiteMapManager manager,
63             String query) {
64         return new SearchQueryTask(new InputDeviceResultTask(context, manager, query));
65     }
66 
67 
68     private static final int NAME_NO_MATCH = -1;
69 
70     private final InputMethodManager mImm;
71     private final PackageManager mPackageManager;
72 
73     private List<String> mPhysicalKeyboardBreadcrumb;
74     private List<String> mVirtualKeyboardBreadcrumb;
75 
InputDeviceResultTask(Context context, SiteMapManager manager, String query)76     public InputDeviceResultTask(Context context, SiteMapManager manager, String query) {
77         super(context, manager, query);
78 
79         mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
80         mPackageManager = context.getPackageManager();
81     }
82 
83     @Override
getQueryWorkerId()84     protected int getQueryWorkerId() {
85         return QUERY_WORKER_ID;
86     }
87 
88     @Override
query()89     protected List<? extends SearchResult> query() {
90         long startTime = System.currentTimeMillis();
91         final List<SearchResult> results = new ArrayList<>();
92         results.addAll(buildPhysicalKeyboardSearchResults());
93         results.addAll(buildVirtualKeyboardSearchResults());
94         Collections.sort(results);
95         if (SearchFeatureProvider.DEBUG) {
96             Log.d(TAG, "Input search loading took:" + (System.currentTimeMillis() - startTime));
97         }
98         return results;
99     }
100 
buildPhysicalKeyboardSearchResults()101     private Set<SearchResult> buildPhysicalKeyboardSearchResults() {
102         final Set<SearchResult> results = new HashSet<>();
103         final String screenTitle = mContext.getString(R.string.physical_keyboard_title);
104 
105         for (final InputDevice device : getPhysicalFullKeyboards()) {
106             final String deviceName = device.getName();
107             final int wordDiff = SearchQueryUtils.getWordDifference(deviceName, mQuery);
108             if (wordDiff == NAME_NO_MATCH) {
109                 continue;
110             }
111             final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
112                     PHYSICAL_KEYBOARD_FRAGMENT, deviceName, screenTitle);
113             results.add(new SearchResult.Builder()
114                     .setTitle(deviceName)
115                     .setPayload(new ResultPayload(intent))
116                     .setDataKey(deviceName)
117                     .setRank(wordDiff)
118                     .addBreadcrumbs(getPhysicalKeyboardBreadCrumb())
119                     .build());
120         }
121         return results;
122     }
123 
buildVirtualKeyboardSearchResults()124     private Set<SearchResult> buildVirtualKeyboardSearchResults() {
125         final Set<SearchResult> results = new HashSet<>();
126         final String screenTitle = mContext.getString(R.string.add_virtual_keyboard);
127         final List<InputMethodInfo> inputMethods = mImm.getInputMethodList();
128         for (InputMethodInfo info : inputMethods) {
129             final String title = info.loadLabel(mPackageManager).toString();
130             final String summary = getSubtypeLocaleNameListAsSentence(
131                     getAllSubtypesOf(info), mContext, info);
132             int wordDiff = SearchQueryUtils.getWordDifference(title, mQuery);
133             if (wordDiff == NAME_NO_MATCH) {
134                 wordDiff = SearchQueryUtils.getWordDifference(summary, mQuery);
135             }
136             if (wordDiff == NAME_NO_MATCH) {
137                 continue;
138             }
139             final ServiceInfo serviceInfo = info.getServiceInfo();
140             final String key = new ComponentName(serviceInfo.packageName, serviceInfo.name)
141                     .flattenToString();
142             final Intent intent = DatabaseIndexingUtils.buildSearchTrampolineIntent(mContext,
143                     VIRTUAL_KEYBOARD_FRAGMENT, key, screenTitle);
144             results.add(new SearchResult.Builder()
145                     .setTitle(title)
146                     .setSummary(summary)
147                     .setRank(wordDiff)
148                     .setDataKey(key)
149                     .addBreadcrumbs(getVirtualKeyboardBreadCrumb())
150                     .setPayload(new ResultPayload(intent))
151                     .build());
152         }
153         return results;
154     }
155 
getPhysicalKeyboardBreadCrumb()156     private List<String> getPhysicalKeyboardBreadCrumb() {
157         if (mPhysicalKeyboardBreadcrumb == null || mPhysicalKeyboardBreadcrumb.isEmpty()) {
158             mPhysicalKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
159                     mContext, PHYSICAL_KEYBOARD_FRAGMENT,
160                     mContext.getString(R.string.physical_keyboard_title));
161         }
162         return mPhysicalKeyboardBreadcrumb;
163     }
164 
165 
getVirtualKeyboardBreadCrumb()166     private List<String> getVirtualKeyboardBreadCrumb() {
167         if (mVirtualKeyboardBreadcrumb == null || mVirtualKeyboardBreadcrumb.isEmpty()) {
168             final Context context = mContext;
169             mVirtualKeyboardBreadcrumb = mSiteMapManager.buildBreadCrumb(
170                     context, VIRTUAL_KEYBOARD_FRAGMENT,
171                     context.getString(R.string.add_virtual_keyboard));
172         }
173         return mVirtualKeyboardBreadcrumb;
174     }
175 
getPhysicalFullKeyboards()176     private List<InputDevice> getPhysicalFullKeyboards() {
177         final List<InputDevice> keyboards = new ArrayList<>();
178         final int[] deviceIds = InputDevice.getDeviceIds();
179         if (deviceIds != null) {
180             for (int deviceId : deviceIds) {
181                 final InputDevice device = InputDevice.getDevice(deviceId);
182                 if (isFullPhysicalKeyboard(device)) {
183                     keyboards.add(device);
184                 }
185             }
186         }
187         return keyboards;
188     }
189 
190     @NonNull
getSubtypeLocaleNameListAsSentence( @onNull final List<InputMethodSubtype> subtypes, @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo)191     private static String getSubtypeLocaleNameListAsSentence(
192             @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context,
193             @NonNull final InputMethodInfo inputMethodInfo) {
194         if (subtypes.isEmpty()) {
195             return "";
196         }
197         final Locale locale = Locale.getDefault();
198         final int subtypeCount = subtypes.size();
199         final CharSequence[] subtypeNames = new CharSequence[subtypeCount];
200         for (int i = 0; i < subtypeCount; i++) {
201             subtypeNames[i] = subtypes.get(i).getDisplayName(context,
202                     inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo()
203                             .applicationInfo);
204         }
205         return toSentenceCase(
206                 ListFormatter.getInstance(locale).format((Object[]) subtypeNames), locale);
207     }
208 
toSentenceCase(String str, Locale locale)209     private static String toSentenceCase(String str, Locale locale) {
210         if (str.isEmpty()) {
211             return str;
212         }
213         final int firstCodePointLen = str.offsetByCodePoints(0, 1);
214         return str.substring(0, firstCodePointLen).toUpperCase(locale)
215                 + str.substring(firstCodePointLen);
216     }
217 
isFullPhysicalKeyboard(InputDevice device)218     private static boolean isFullPhysicalKeyboard(InputDevice device) {
219         return device != null && !device.isVirtual() &&
220                 (device.getSources() & InputDevice.SOURCE_KEYBOARD)
221                         == InputDevice.SOURCE_KEYBOARD
222                 && device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC;
223     }
224 
getAllSubtypesOf(final InputMethodInfo imi)225     private static List<InputMethodSubtype> getAllSubtypesOf(final InputMethodInfo imi) {
226         final int subtypeCount = imi.getSubtypeCount();
227         final List<InputMethodSubtype> allSubtypes = new ArrayList<>(subtypeCount);
228         for (int index = 0; index < subtypeCount; index++) {
229             allSubtypes.add(imi.getSubtypeAt(index));
230         }
231         return allSubtypes;
232     }
233 }
234