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