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