1 /*
2  * Copyright (C) 2019 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.car.settings.inputmethod;
18 
19 import android.app.admin.DevicePolicyManager;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.pm.PackageManager;
23 import android.content.pm.PackageManager.NameNotFoundException;
24 import android.graphics.Color;
25 import android.graphics.drawable.ColorDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.provider.Settings;
28 import android.text.TextUtils;
29 import android.view.inputmethod.InputMethodInfo;
30 import android.view.inputmethod.InputMethodManager;
31 import android.view.inputmethod.InputMethodSubtype;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.VisibleForTesting;
35 
36 import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil;
37 
38 import java.util.ArrayList;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.stream.Collectors;
42 
43 /** Keyboard utility class. */
44 public final class InputMethodUtil {
45     /**
46      * Delimiter for Enabled Input Methods' concatenated string.
47      */
48     public static final char INPUT_METHOD_DELIMITER = ':';
49     /**
50      * A list of past and present Google Voice Typing package names
51      */
52     public static final List<String> GVT_PACKAGE_NAMES =
53             Collections.unmodifiableList(
54                     new ArrayList<String>(){{
55                         add("com.google.android.tts");
56                         add("com.google.android.carassistant");
57                         add("com.google.android.googlequicksearchbox");
58                     }});
59     /**
60      * Splitter for Enabled Input Methods' concatenated string.
61      */
62     public static final TextUtils.SimpleStringSplitter sInputMethodSplitter =
63             new TextUtils.SimpleStringSplitter(INPUT_METHOD_DELIMITER);
64     @VisibleForTesting
65     static final Drawable NO_ICON = new ColorDrawable(Color.TRANSPARENT);
66 
InputMethodUtil()67     private InputMethodUtil() {
68     }
69 
70     /** Returns permitted list of enabled input methods. */
getPermittedAndEnabledInputMethodList( InputMethodManager imm, DevicePolicyManager dpm)71     public static List<InputMethodInfo> getPermittedAndEnabledInputMethodList(
72             InputMethodManager imm, DevicePolicyManager dpm) {
73         List<InputMethodInfo> inputMethodInfos = imm.getEnabledInputMethodList();
74         if (inputMethodInfos != null) {
75             // permittedList == null means all input methods are allowed.
76             List<String> permittedList = dpm.getPermittedInputMethodsForCurrentUser();
77 
78             inputMethodInfos = inputMethodInfos.stream().filter(info -> {
79                 boolean isAllowedByOrganization = permittedList == null
80                         || permittedList.contains(info.getPackageName());
81                 // Hide "Google voice typing" IME.
82                 boolean isGoogleVoiceTyping =
83                         InputMethodUtil.GVT_PACKAGE_NAMES.contains(info.getPackageName());
84                 return isAllowedByOrganization && !isGoogleVoiceTyping;
85             }).collect(Collectors.toList());
86         }
87         return inputMethodInfos;
88     }
89 
90     /** Returns package icon. */
getPackageIcon(@onNull PackageManager packageManager, @NonNull InputMethodInfo inputMethodInfo)91     public static Drawable getPackageIcon(@NonNull PackageManager packageManager,
92             @NonNull InputMethodInfo inputMethodInfo) {
93         Drawable icon;
94         try {
95             icon = packageManager.getApplicationIcon(inputMethodInfo.getPackageName());
96         } catch (NameNotFoundException e) {
97             icon = NO_ICON;
98         }
99 
100         return icon;
101     }
102 
103     /** Returns package label. */
getPackageLabel(@onNull PackageManager packageManager, @NonNull InputMethodInfo inputMethodInfo)104     public static String getPackageLabel(@NonNull PackageManager packageManager,
105             @NonNull InputMethodInfo inputMethodInfo) {
106         return inputMethodInfo.loadLabel(packageManager).toString();
107     }
108 
109     /** Returns input method summary. */
getSummaryString(@onNull Context context, @NonNull InputMethodManager inputMethodManager, @NonNull InputMethodInfo inputMethodInfo)110     public static String getSummaryString(@NonNull Context context,
111             @NonNull InputMethodManager inputMethodManager,
112             @NonNull InputMethodInfo inputMethodInfo) {
113         List<InputMethodSubtype> subtypes =
114                 inputMethodManager.getEnabledInputMethodSubtypeList(
115                         inputMethodInfo, /* allowsImplicitlySelectedSubtypes= */ true);
116         return InputMethodAndSubtypeUtil.getSubtypeLocaleNameListAsSentence(
117                 subtypes, context, inputMethodInfo);
118     }
119 
120     /**
121      * Check if input method is enabled.
122      *
123      * @return {@code true} if the input method is enabled.
124      */
isInputMethodEnabled(ContentResolver resolver, InputMethodInfo inputMethodInfo)125     public static boolean isInputMethodEnabled(ContentResolver resolver,
126             InputMethodInfo inputMethodInfo) {
127         String enabledImes = getEnabledInputMethodsConcatenatedIds(resolver);
128         if (TextUtils.isEmpty(enabledImes)) {
129             return false;
130         }
131         sInputMethodSplitter.setString(enabledImes);
132         while (sInputMethodSplitter.hasNext()) {
133             String inputMethodId = sInputMethodSplitter.next();
134             if (inputMethodId.equals(inputMethodInfo.getId())) {
135                 return true;
136             }
137         }
138         return false;
139     }
140 
141     /**
142      * Enable an input method using its InputMethodInfo.
143      */
enableInputMethod(ContentResolver resolver, InputMethodInfo inputMethodInfo)144     public static void enableInputMethod(ContentResolver resolver,
145             InputMethodInfo inputMethodInfo) {
146         if (isInputMethodEnabled(resolver, inputMethodInfo)) {
147             return;
148         }
149 
150         StringBuilder builder = new StringBuilder();
151         builder.append(getEnabledInputMethodsConcatenatedIds(resolver));
152 
153         if (!builder.toString().isEmpty()) {
154             builder.append(INPUT_METHOD_DELIMITER);
155         }
156 
157         builder.append(inputMethodInfo.getId());
158 
159         setEnabledInputMethodsConcatenatedIds(resolver, builder.toString());
160     }
161 
162     /**
163      * Disable an input method if its not the default system input method or if there exists another
164      * enabled input method that can also be set as the default system input method.
165      */
disableInputMethod(Context context, InputMethodManager inputMethodManager, InputMethodInfo inputMethodInfo)166     public static void disableInputMethod(Context context, InputMethodManager inputMethodManager,
167             InputMethodInfo inputMethodInfo) {
168         List<InputMethodInfo> enabledInputMethodInfos = inputMethodManager
169                 .getEnabledInputMethodList();
170         StringBuilder builder = new StringBuilder();
171 
172         boolean foundAnotherEnabledDefaultInputMethod = false;
173         boolean isSystemDefault = isDefaultInputMethod(context.getContentResolver(),
174                 inputMethodInfo);
175         for (InputMethodInfo enabledInputMethodInfo : enabledInputMethodInfos) {
176             if (enabledInputMethodInfo.getId().equals(inputMethodInfo.getId())) {
177                 continue;
178             }
179 
180             if (builder.length() > 0) {
181                 builder.append(INPUT_METHOD_DELIMITER);
182             }
183 
184             builder.append(enabledInputMethodInfo.getId());
185 
186             if (isSystemDefault && enabledInputMethodInfo.isDefault(context)) {
187                 foundAnotherEnabledDefaultInputMethod = true;
188                 setDefaultInputMethodId(context.getContentResolver(),
189                         enabledInputMethodInfo.getId());
190             }
191         }
192 
193         if (isSystemDefault && !foundAnotherEnabledDefaultInputMethod) {
194             return;
195         }
196 
197         setEnabledInputMethodsConcatenatedIds(context.getContentResolver(), builder.toString());
198     }
199 
getEnabledInputMethodsConcatenatedIds(ContentResolver resolver)200     private static String getEnabledInputMethodsConcatenatedIds(ContentResolver resolver) {
201         return Settings.Secure.getString(resolver, Settings.Secure.ENABLED_INPUT_METHODS);
202     }
203 
getDefaultInputMethodId(ContentResolver resolver)204     private static String getDefaultInputMethodId(ContentResolver resolver) {
205         return Settings.Secure.getString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD);
206     }
207 
isDefaultInputMethod(ContentResolver resolver, InputMethodInfo inputMethodInfo)208     private static boolean isDefaultInputMethod(ContentResolver resolver,
209             InputMethodInfo inputMethodInfo) {
210         return inputMethodInfo.getId().equals(getDefaultInputMethodId(resolver));
211     }
212 
setEnabledInputMethodsConcatenatedIds(ContentResolver resolver, String enabledInputMethodIds)213     private static void setEnabledInputMethodsConcatenatedIds(ContentResolver resolver,
214             String enabledInputMethodIds) {
215         Settings.Secure.putString(resolver, Settings.Secure.ENABLED_INPUT_METHODS,
216                 enabledInputMethodIds);
217     }
218 
setDefaultInputMethodId(ContentResolver resolver, String defaultInputMethodId)219     private static void setDefaultInputMethodId(ContentResolver resolver,
220             String defaultInputMethodId) {
221         Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD,
222                 defaultInputMethodId);
223     }
224 }
225