summaryrefslogtreecommitdiffstats
path: root/services/core/java/com/android/server/InputMethodManagerService.java
diff options
context:
space:
mode:
Diffstat (limited to 'services/core/java/com/android/server/InputMethodManagerService.java')
-rw-r--r--services/core/java/com/android/server/InputMethodManagerService.java3574
1 files changed, 3574 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java
new file mode 100644
index 0000000..26424a5
--- /dev/null
+++ b/services/core/java/com/android/server/InputMethodManagerService.java
@@ -0,0 +1,3574 @@
+/*
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.server;
+
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.inputmethod.InputMethodUtils;
+import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings;
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.view.IInputContext;
+import com.android.internal.view.IInputMethod;
+import com.android.internal.view.IInputSessionCallback;
+import com.android.internal.view.IInputMethodClient;
+import com.android.internal.view.IInputMethodManager;
+import com.android.internal.view.IInputMethodSession;
+import com.android.internal.view.InputBindResult;
+import com.android.server.statusbar.StatusBarManagerService;
+import com.android.server.wm.WindowManagerService;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import android.app.ActivityManagerNative;
+import android.app.AppGlobals;
+import android.app.AlertDialog;
+import android.app.IUserSwitchObserver;
+import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.inputmethodservice.InputMethodService;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.IRemoteCallback;
+import android.os.Message;
+import android.os.Process;
+import android.os.Parcel;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.text.style.SuggestionSpan;
+import android.util.AtomicFile;
+import android.util.EventLog;
+import android.util.LruCache;
+import android.util.Pair;
+import android.util.PrintWriterPrinter;
+import android.util.Printer;
+import android.util.Slog;
+import android.util.Xml;
+import android.view.IWindowManager;
+import android.view.InputChannel;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputBinding;
+import android.view.inputmethod.InputMethod;
+import android.view.inputmethod.InputMethodInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.view.inputmethod.InputMethodSubtype;
+import android.widget.ArrayAdapter;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.RadioButton;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.TreeMap;
+
+/**
+ * This class provides a system service that manages input methods.
+ */
+public class InputMethodManagerService extends IInputMethodManager.Stub
+ implements ServiceConnection, Handler.Callback {
+ static final boolean DEBUG = false;
+ static final String TAG = "InputMethodManagerService";
+
+ static final int MSG_SHOW_IM_PICKER = 1;
+ static final int MSG_SHOW_IM_SUBTYPE_PICKER = 2;
+ static final int MSG_SHOW_IM_SUBTYPE_ENABLER = 3;
+ static final int MSG_SHOW_IM_CONFIG = 4;
+
+ static final int MSG_UNBIND_INPUT = 1000;
+ static final int MSG_BIND_INPUT = 1010;
+ static final int MSG_SHOW_SOFT_INPUT = 1020;
+ static final int MSG_HIDE_SOFT_INPUT = 1030;
+ static final int MSG_ATTACH_TOKEN = 1040;
+ static final int MSG_CREATE_SESSION = 1050;
+
+ static final int MSG_START_INPUT = 2000;
+ static final int MSG_RESTART_INPUT = 2010;
+
+ static final int MSG_UNBIND_METHOD = 3000;
+ static final int MSG_BIND_METHOD = 3010;
+ static final int MSG_SET_ACTIVE = 3020;
+
+ static final int MSG_HARD_KEYBOARD_SWITCH_CHANGED = 4000;
+
+ static final long TIME_TO_RECONNECT = 10*1000;
+
+ static final int SECURE_SUGGESTION_SPANS_MAX_SIZE = 20;
+
+ private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID;
+ private static final String TAG_TRY_SUPPRESSING_IME_SWITCHER = "TrySuppressingImeSwitcher";
+
+
+ final Context mContext;
+ final Resources mRes;
+ final Handler mHandler;
+ final InputMethodSettings mSettings;
+ final SettingsObserver mSettingsObserver;
+ final IWindowManager mIWindowManager;
+ final HandlerCaller mCaller;
+ final boolean mHasFeature;
+ private InputMethodFileManager mFileManager;
+ private InputMethodAndSubtypeListManager mImListManager;
+ private final HardKeyboardListener mHardKeyboardListener;
+ private final WindowManagerService mWindowManagerService;
+
+ final InputBindResult mNoBinding = new InputBindResult(null, null, null, -1);
+
+ // All known input methods. mMethodMap also serves as the global
+ // lock for this class.
+ final ArrayList<InputMethodInfo> mMethodList = new ArrayList<InputMethodInfo>();
+ final HashMap<String, InputMethodInfo> mMethodMap = new HashMap<String, InputMethodInfo>();
+ private final LruCache<SuggestionSpan, InputMethodInfo> mSecureSuggestionSpans =
+ new LruCache<SuggestionSpan, InputMethodInfo>(SECURE_SUGGESTION_SPANS_MAX_SIZE);
+
+ // Used to bring IME service up to visible adjustment while it is being shown.
+ final ServiceConnection mVisibleConnection = new ServiceConnection() {
+ @Override public void onServiceConnected(ComponentName name, IBinder service) {
+ }
+
+ @Override public void onServiceDisconnected(ComponentName name) {
+ }
+ };
+ boolean mVisibleBound = false;
+
+ // Ongoing notification
+ private NotificationManager mNotificationManager;
+ private KeyguardManager mKeyguardManager;
+ private StatusBarManagerService mStatusBar;
+ private Notification mImeSwitcherNotification;
+ private PendingIntent mImeSwitchPendingIntent;
+ private boolean mShowOngoingImeSwitcherForPhones;
+ private boolean mNotificationShown;
+ private final boolean mImeSelectedOnBoot;
+
+ class SessionState {
+ final ClientState client;
+ final IInputMethod method;
+
+ IInputMethodSession session;
+ InputChannel channel;
+
+ @Override
+ public String toString() {
+ return "SessionState{uid " + client.uid + " pid " + client.pid
+ + " method " + Integer.toHexString(
+ System.identityHashCode(method))
+ + " session " + Integer.toHexString(
+ System.identityHashCode(session))
+ + " channel " + channel
+ + "}";
+ }
+
+ SessionState(ClientState _client, IInputMethod _method,
+ IInputMethodSession _session, InputChannel _channel) {
+ client = _client;
+ method = _method;
+ session = _session;
+ channel = _channel;
+ }
+ }
+
+ static final class ClientState {
+ final IInputMethodClient client;
+ final IInputContext inputContext;
+ final int uid;
+ final int pid;
+ final InputBinding binding;
+
+ boolean sessionRequested;
+ SessionState curSession;
+
+ @Override
+ public String toString() {
+ return "ClientState{" + Integer.toHexString(
+ System.identityHashCode(this)) + " uid " + uid
+ + " pid " + pid + "}";
+ }
+
+ ClientState(IInputMethodClient _client, IInputContext _inputContext,
+ int _uid, int _pid) {
+ client = _client;
+ inputContext = _inputContext;
+ uid = _uid;
+ pid = _pid;
+ binding = new InputBinding(null, inputContext.asBinder(), uid, pid);
+ }
+ }
+
+ final HashMap<IBinder, ClientState> mClients
+ = new HashMap<IBinder, ClientState>();
+
+ /**
+ * Set once the system is ready to run third party code.
+ */
+ boolean mSystemReady;
+
+ /**
+ * Id of the currently selected input method.
+ */
+ String mCurMethodId;
+
+ /**
+ * The current binding sequence number, incremented every time there is
+ * a new bind performed.
+ */
+ int mCurSeq;
+
+ /**
+ * The client that is currently bound to an input method.
+ */
+ ClientState mCurClient;
+
+ /**
+ * The last window token that gained focus.
+ */
+ IBinder mCurFocusedWindow;
+
+ /**
+ * The input context last provided by the current client.
+ */
+ IInputContext mCurInputContext;
+
+ /**
+ * The attributes last provided by the current client.
+ */
+ EditorInfo mCurAttribute;
+
+ /**
+ * The input method ID of the input method service that we are currently
+ * connected to or in the process of connecting to.
+ */
+ String mCurId;
+
+ /**
+ * The current subtype of the current input method.
+ */
+ private InputMethodSubtype mCurrentSubtype;
+
+ // This list contains the pairs of InputMethodInfo and InputMethodSubtype.
+ private final HashMap<InputMethodInfo, ArrayList<InputMethodSubtype>>
+ mShortcutInputMethodsAndSubtypes =
+ new HashMap<InputMethodInfo, ArrayList<InputMethodSubtype>>();
+
+ // Was the keyguard locked when this client became current?
+ private boolean mCurClientInKeyguard;
+
+ /**
+ * Set to true if our ServiceConnection is currently actively bound to
+ * a service (whether or not we have gotten its IBinder back yet).
+ */
+ boolean mHaveConnection;
+
+ /**
+ * Set if the client has asked for the input method to be shown.
+ */
+ boolean mShowRequested;
+
+ /**
+ * Set if we were explicitly told to show the input method.
+ */
+ boolean mShowExplicitlyRequested;
+
+ /**
+ * Set if we were forced to be shown.
+ */
+ boolean mShowForced;
+
+ /**
+ * Set if we last told the input method to show itself.
+ */
+ boolean mInputShown;
+
+ /**
+ * The Intent used to connect to the current input method.
+ */
+ Intent mCurIntent;
+
+ /**
+ * The token we have made for the currently active input method, to
+ * identify it in the future.
+ */
+ IBinder mCurToken;
+
+ /**
+ * If non-null, this is the input method service we are currently connected
+ * to.
+ */
+ IInputMethod mCurMethod;
+
+ /**
+ * Time that we last initiated a bind to the input method, to determine
+ * if we should try to disconnect and reconnect to it.
+ */
+ long mLastBindTime;
+
+ /**
+ * Have we called mCurMethod.bindInput()?
+ */
+ boolean mBoundToMethod;
+
+ /**
+ * Currently enabled session. Only touched by service thread, not
+ * protected by a lock.
+ */
+ SessionState mEnabledSession;
+
+ /**
+ * True if the screen is on. The value is true initially.
+ */
+ boolean mScreenOn = true;
+
+ int mBackDisposition = InputMethodService.BACK_DISPOSITION_DEFAULT;
+ int mImeWindowVis;
+
+ private AlertDialog.Builder mDialogBuilder;
+ private AlertDialog mSwitchingDialog;
+ private View mSwitchingDialogTitleView;
+ private InputMethodInfo[] mIms;
+ private int[] mSubtypeIds;
+ private Locale mLastSystemLocale;
+ private final MyPackageMonitor mMyPackageMonitor = new MyPackageMonitor();
+ private final IPackageManager mIPackageManager;
+
+ class SettingsObserver extends ContentObserver {
+ String mLastEnabled = "";
+
+ SettingsObserver(Handler handler) {
+ super(handler);
+ ContentResolver resolver = mContext.getContentResolver();
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.DEFAULT_INPUT_METHOD), false, this);
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.ENABLED_INPUT_METHODS), false, this);
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE), false, this);
+ }
+
+ @Override public void onChange(boolean selfChange) {
+ synchronized (mMethodMap) {
+ boolean enabledChanged = false;
+ String newEnabled = mSettings.getEnabledInputMethodsStr();
+ if (!mLastEnabled.equals(newEnabled)) {
+ mLastEnabled = newEnabled;
+ enabledChanged = true;
+ }
+ updateFromSettingsLocked(enabledChanged);
+ }
+ }
+ }
+
+ class ImmsBroadcastReceiver extends android.content.BroadcastReceiver {
+ private void updateActive() {
+ // Inform the current client of the change in active status
+ if (mCurClient != null && mCurClient.client != null) {
+ executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO(
+ MSG_SET_ACTIVE, mScreenOn ? 1 : 0, mCurClient));
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ if (Intent.ACTION_SCREEN_ON.equals(action)) {
+ mScreenOn = true;
+ refreshImeWindowVisibilityLocked();
+ updateActive();
+ return;
+ } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+ mScreenOn = false;
+ setImeWindowVisibilityStatusHiddenLocked();
+ updateActive();
+ return;
+ } else if (Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(action)) {
+ hideInputMethodMenu();
+ // No need to updateActive
+ return;
+ } else {
+ Slog.w(TAG, "Unexpected intent " + intent);
+ }
+ }
+ }
+
+ class MyPackageMonitor extends PackageMonitor {
+ private boolean isChangingPackagesOfCurrentUser() {
+ final int userId = getChangingUserId();
+ final boolean retval = userId == mSettings.getCurrentUserId();
+ if (DEBUG) {
+ if (!retval) {
+ Slog.d(TAG, "--- ignore this call back from a background user: " + userId);
+ }
+ }
+ return retval;
+ }
+
+ @Override
+ public boolean onHandleForceStop(Intent intent, String[] packages, int uid, boolean doit) {
+ if (!isChangingPackagesOfCurrentUser()) {
+ return false;
+ }
+ synchronized (mMethodMap) {
+ String curInputMethodId = mSettings.getSelectedInputMethod();
+ final int N = mMethodList.size();
+ if (curInputMethodId != null) {
+ for (int i=0; i<N; i++) {
+ InputMethodInfo imi = mMethodList.get(i);
+ if (imi.getId().equals(curInputMethodId)) {
+ for (String pkg : packages) {
+ if (imi.getPackageName().equals(pkg)) {
+ if (!doit) {
+ return true;
+ }
+ resetSelectedInputMethodAndSubtypeLocked("");
+ chooseNewDefaultIMELocked();
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onSomePackagesChanged() {
+ if (!isChangingPackagesOfCurrentUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ InputMethodInfo curIm = null;
+ String curInputMethodId = mSettings.getSelectedInputMethod();
+ final int N = mMethodList.size();
+ if (curInputMethodId != null) {
+ for (int i=0; i<N; i++) {
+ InputMethodInfo imi = mMethodList.get(i);
+ final String imiId = imi.getId();
+ if (imiId.equals(curInputMethodId)) {
+ curIm = imi;
+ }
+
+ int change = isPackageDisappearing(imi.getPackageName());
+ if (isPackageModified(imi.getPackageName())) {
+ mFileManager.deleteAllInputMethodSubtypes(imiId);
+ }
+ if (change == PACKAGE_TEMPORARY_CHANGE
+ || change == PACKAGE_PERMANENT_CHANGE) {
+ Slog.i(TAG, "Input method uninstalled, disabling: "
+ + imi.getComponent());
+ setInputMethodEnabledLocked(imi.getId(), false);
+ }
+ }
+ }
+
+ buildInputMethodListLocked(
+ mMethodList, mMethodMap, false /* resetDefaultEnabledIme */);
+
+ boolean changed = false;
+
+ if (curIm != null) {
+ int change = isPackageDisappearing(curIm.getPackageName());
+ if (change == PACKAGE_TEMPORARY_CHANGE
+ || change == PACKAGE_PERMANENT_CHANGE) {
+ ServiceInfo si = null;
+ try {
+ si = mIPackageManager.getServiceInfo(
+ curIm.getComponent(), 0, mSettings.getCurrentUserId());
+ } catch (RemoteException ex) {
+ }
+ if (si == null) {
+ // Uh oh, current input method is no longer around!
+ // Pick another one...
+ Slog.i(TAG, "Current input method removed: " + curInputMethodId);
+ setImeWindowVisibilityStatusHiddenLocked();
+ if (!chooseNewDefaultIMELocked()) {
+ changed = true;
+ curIm = null;
+ Slog.i(TAG, "Unsetting current input method");
+ resetSelectedInputMethodAndSubtypeLocked("");
+ }
+ }
+ }
+ }
+
+ if (curIm == null) {
+ // We currently don't have a default input method... is
+ // one now available?
+ changed = chooseNewDefaultIMELocked();
+ }
+
+ if (changed) {
+ updateFromSettingsLocked(false);
+ }
+ }
+ }
+ }
+
+ private static final class MethodCallback extends IInputSessionCallback.Stub {
+ private final InputMethodManagerService mParentIMMS;
+ private final IInputMethod mMethod;
+ private final InputChannel mChannel;
+
+ MethodCallback(InputMethodManagerService imms, IInputMethod method,
+ InputChannel channel) {
+ mParentIMMS = imms;
+ mMethod = method;
+ mChannel = channel;
+ }
+
+ @Override
+ public void sessionCreated(IInputMethodSession session) {
+ mParentIMMS.onSessionCreated(mMethod, session, mChannel);
+ }
+ }
+
+ private class HardKeyboardListener
+ implements WindowManagerService.OnHardKeyboardStatusChangeListener {
+ @Override
+ public void onHardKeyboardStatusChange(boolean available, boolean enabled) {
+ mHandler.sendMessage(mHandler.obtainMessage(
+ MSG_HARD_KEYBOARD_SWITCH_CHANGED, available ? 1 : 0, enabled ? 1 : 0));
+ }
+
+ public void handleHardKeyboardStatusChange(boolean available, boolean enabled) {
+ if (DEBUG) {
+ Slog.w(TAG, "HardKeyboardStatusChanged: available = " + available + ", enabled = "
+ + enabled);
+ }
+ synchronized(mMethodMap) {
+ if (mSwitchingDialog != null && mSwitchingDialogTitleView != null
+ && mSwitchingDialog.isShowing()) {
+ mSwitchingDialogTitleView.findViewById(
+ com.android.internal.R.id.hard_keyboard_section).setVisibility(
+ available ? View.VISIBLE : View.GONE);
+ }
+ }
+ }
+ }
+
+ public InputMethodManagerService(Context context, WindowManagerService windowManager) {
+ mIPackageManager = AppGlobals.getPackageManager();
+ mContext = context;
+ mRes = context.getResources();
+ mHandler = new Handler(this);
+ mIWindowManager = IWindowManager.Stub.asInterface(
+ ServiceManager.getService(Context.WINDOW_SERVICE));
+ mCaller = new HandlerCaller(context, null, new HandlerCaller.Callback() {
+ @Override
+ public void executeMessage(Message msg) {
+ handleMessage(msg);
+ }
+ }, true /*asyncHandler*/);
+ mWindowManagerService = windowManager;
+ mHardKeyboardListener = new HardKeyboardListener();
+ mHasFeature = context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_INPUT_METHODS);
+
+ mImeSwitcherNotification = new Notification();
+ mImeSwitcherNotification.icon = com.android.internal.R.drawable.ic_notification_ime_default;
+ mImeSwitcherNotification.when = 0;
+ mImeSwitcherNotification.flags = Notification.FLAG_ONGOING_EVENT;
+ mImeSwitcherNotification.tickerText = null;
+ mImeSwitcherNotification.defaults = 0; // please be quiet
+ mImeSwitcherNotification.sound = null;
+ mImeSwitcherNotification.vibrate = null;
+
+ // Tag this notification specially so SystemUI knows it's important
+ mImeSwitcherNotification.kind = new String[] { "android.system.imeswitcher" };
+
+ Intent intent = new Intent(Settings.ACTION_SHOW_INPUT_METHOD_PICKER);
+ mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
+
+ mShowOngoingImeSwitcherForPhones = false;
+
+ final IntentFilter broadcastFilter = new IntentFilter();
+ broadcastFilter.addAction(Intent.ACTION_SCREEN_ON);
+ broadcastFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ broadcastFilter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ mContext.registerReceiver(new ImmsBroadcastReceiver(), broadcastFilter);
+
+ mNotificationShown = false;
+ int userId = 0;
+ try {
+ ActivityManagerNative.getDefault().registerUserSwitchObserver(
+ new IUserSwitchObserver.Stub() {
+ @Override
+ public void onUserSwitching(int newUserId, IRemoteCallback reply) {
+ synchronized(mMethodMap) {
+ switchUserLocked(newUserId);
+ }
+ if (reply != null) {
+ try {
+ reply.sendResult(null);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ @Override
+ public void onUserSwitchComplete(int newUserId) throws RemoteException {
+ }
+ });
+ userId = ActivityManagerNative.getDefault().getCurrentUser().id;
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e);
+ }
+ mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true);
+
+ // mSettings should be created before buildInputMethodListLocked
+ mSettings = new InputMethodSettings(
+ mRes, context.getContentResolver(), mMethodMap, mMethodList, userId);
+ mFileManager = new InputMethodFileManager(mMethodMap, userId);
+ mImListManager = new InputMethodAndSubtypeListManager(context, this);
+
+ // Just checking if defaultImiId is empty or not
+ final String defaultImiId = mSettings.getSelectedInputMethod();
+ if (DEBUG) {
+ Slog.d(TAG, "Initial default ime = " + defaultImiId);
+ }
+ mImeSelectedOnBoot = !TextUtils.isEmpty(defaultImiId);
+
+ buildInputMethodListLocked(mMethodList, mMethodMap,
+ !mImeSelectedOnBoot /* resetDefaultEnabledIme */);
+ mSettings.enableAllIMEsIfThereIsNoEnabledIME();
+
+ if (!mImeSelectedOnBoot) {
+ Slog.w(TAG, "No IME selected. Choose the most applicable IME.");
+ resetDefaultImeLocked(context);
+ }
+
+ mSettingsObserver = new SettingsObserver(mHandler);
+ updateFromSettingsLocked(true);
+
+ // IMMS wants to receive Intent.ACTION_LOCALE_CHANGED in order to update the current IME
+ // according to the new system locale.
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_LOCALE_CHANGED);
+ mContext.registerReceiver(
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized(mMethodMap) {
+ resetStateIfCurrentLocaleChangedLocked();
+ }
+ }
+ }, filter);
+ }
+
+ private void resetDefaultImeLocked(Context context) {
+ // Do not reset the default (current) IME when it is a 3rd-party IME
+ if (mCurMethodId != null
+ && !InputMethodUtils.isSystemIme(mMethodMap.get(mCurMethodId))) {
+ return;
+ }
+
+ InputMethodInfo defIm = null;
+ for (InputMethodInfo imi : mMethodList) {
+ if (defIm == null) {
+ if (InputMethodUtils.isValidSystemDefaultIme(
+ mSystemReady, imi, context)) {
+ defIm = imi;
+ Slog.i(TAG, "Selected default: " + imi.getId());
+ }
+ }
+ }
+ if (defIm == null && mMethodList.size() > 0) {
+ defIm = InputMethodUtils.getMostApplicableDefaultIME(
+ mSettings.getEnabledInputMethodListLocked());
+ Slog.i(TAG, "No default found, using " + defIm.getId());
+ }
+ if (defIm != null) {
+ setSelectedInputMethodAndSubtypeLocked(defIm, NOT_A_SUBTYPE_ID, false);
+ }
+ }
+
+ private void resetAllInternalStateLocked(final boolean updateOnlyWhenLocaleChanged,
+ final boolean resetDefaultEnabledIme) {
+ if (!mSystemReady) {
+ // not system ready
+ return;
+ }
+ final Locale newLocale = mRes.getConfiguration().locale;
+ if (!updateOnlyWhenLocaleChanged
+ || (newLocale != null && !newLocale.equals(mLastSystemLocale))) {
+ if (!updateOnlyWhenLocaleChanged) {
+ hideCurrentInputLocked(0, null);
+ mCurMethodId = null;
+ unbindCurrentMethodLocked(true, false);
+ }
+ if (DEBUG) {
+ Slog.i(TAG, "Locale has been changed to " + newLocale);
+ }
+ // InputMethodAndSubtypeListManager should be reset when the locale is changed.
+ mImListManager = new InputMethodAndSubtypeListManager(mContext, this);
+ buildInputMethodListLocked(mMethodList, mMethodMap, resetDefaultEnabledIme);
+ if (!updateOnlyWhenLocaleChanged) {
+ final String selectedImiId = mSettings.getSelectedInputMethod();
+ if (TextUtils.isEmpty(selectedImiId)) {
+ // This is the first time of the user switch and
+ // set the current ime to the proper one.
+ resetDefaultImeLocked(mContext);
+ }
+ } else {
+ // If the locale is changed, needs to reset the default ime
+ resetDefaultImeLocked(mContext);
+ }
+ updateFromSettingsLocked(true);
+ mLastSystemLocale = newLocale;
+ if (!updateOnlyWhenLocaleChanged) {
+ try {
+ startInputInnerLocked();
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Unexpected exception", e);
+ }
+ }
+ }
+ }
+
+ private void resetStateIfCurrentLocaleChangedLocked() {
+ resetAllInternalStateLocked(true /* updateOnlyWhenLocaleChanged */,
+ true /* resetDefaultImeLocked */);
+ }
+
+ private void switchUserLocked(int newUserId) {
+ mSettings.setCurrentUserId(newUserId);
+ // InputMethodFileManager should be reset when the user is changed
+ mFileManager = new InputMethodFileManager(mMethodMap, newUserId);
+ final String defaultImiId = mSettings.getSelectedInputMethod();
+ // For secondary users, the list of enabled IMEs may not have been updated since the
+ // callbacks to PackageMonitor are ignored for the secondary user. Here, defaultImiId may
+ // not be empty even if the IME has been uninstalled by the primary user.
+ // Even in such cases, IMMS works fine because it will find the most applicable
+ // IME for that user.
+ final boolean initialUserSwitch = TextUtils.isEmpty(defaultImiId);
+ if (DEBUG) {
+ Slog.d(TAG, "Switch user: " + newUserId + " current ime = " + defaultImiId);
+ }
+ resetAllInternalStateLocked(false /* updateOnlyWhenLocaleChanged */,
+ initialUserSwitch /* needsToResetDefaultIme */);
+ if (initialUserSwitch) {
+ InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(mContext.getPackageManager(),
+ mSettings.getEnabledInputMethodListLocked());
+ }
+ }
+
+ @Override
+ public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
+ throws RemoteException {
+ try {
+ return super.onTransact(code, data, reply, flags);
+ } catch (RuntimeException e) {
+ // The input method manager only throws security exceptions, so let's
+ // log all others.
+ if (!(e instanceof SecurityException)) {
+ Slog.wtf(TAG, "Input Method Manager Crash", e);
+ }
+ throw e;
+ }
+ }
+
+ public void systemRunning(StatusBarManagerService statusBar) {
+ synchronized (mMethodMap) {
+ if (DEBUG) {
+ Slog.d(TAG, "--- systemReady");
+ }
+ if (!mSystemReady) {
+ mSystemReady = true;
+ mKeyguardManager =
+ (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+ mNotificationManager = (NotificationManager)
+ mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ mStatusBar = statusBar;
+ statusBar.setIconVisibility("ime", false);
+ updateImeWindowStatusLocked();
+ mShowOngoingImeSwitcherForPhones = mRes.getBoolean(
+ com.android.internal.R.bool.show_ongoing_ime_switcher);
+ if (mShowOngoingImeSwitcherForPhones) {
+ mWindowManagerService.setOnHardKeyboardStatusChangeListener(
+ mHardKeyboardListener);
+ }
+ buildInputMethodListLocked(mMethodList, mMethodMap,
+ !mImeSelectedOnBoot /* resetDefaultEnabledIme */);
+ if (!mImeSelectedOnBoot) {
+ Slog.w(TAG, "Reset the default IME as \"Resource\" is ready here.");
+ resetStateIfCurrentLocaleChangedLocked();
+ InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
+ mContext.getPackageManager(),
+ mSettings.getEnabledInputMethodListLocked());
+ }
+ mLastSystemLocale = mRes.getConfiguration().locale;
+ try {
+ startInputInnerLocked();
+ } catch (RuntimeException e) {
+ Slog.w(TAG, "Unexpected exception", e);
+ }
+ }
+ }
+ }
+
+ private void setImeWindowVisibilityStatusHiddenLocked() {
+ mImeWindowVis = 0;
+ updateImeWindowStatusLocked();
+ }
+
+ private void refreshImeWindowVisibilityLocked() {
+ final Configuration conf = mRes.getConfiguration();
+ final boolean haveHardKeyboard = conf.keyboard
+ != Configuration.KEYBOARD_NOKEYS;
+ final boolean hardKeyShown = haveHardKeyboard
+ && conf.hardKeyboardHidden
+ != Configuration.HARDKEYBOARDHIDDEN_YES;
+
+ final boolean isScreenLocked = isKeyguardLocked();
+ final boolean inputActive = !isScreenLocked && (mInputShown || hardKeyShown);
+ // We assume the softkeyboard is shown when the input is active as long as the
+ // hard keyboard is not shown.
+ final boolean inputVisible = inputActive && !hardKeyShown;
+ mImeWindowVis = (inputActive ? InputMethodService.IME_ACTIVE : 0)
+ | (inputVisible ? InputMethodService.IME_VISIBLE : 0);
+ updateImeWindowStatusLocked();
+ }
+
+ private void updateImeWindowStatusLocked() {
+ setImeWindowStatus(mCurToken, mImeWindowVis, mBackDisposition);
+ }
+
+ // ---------------------------------------------------------------------------------------
+ // Check whether or not this is a valid IPC. Assumes an IPC is valid when either
+ // 1) it comes from the system process
+ // 2) the calling process' user id is identical to the current user id IMMS thinks.
+ private boolean calledFromValidUser() {
+ final int uid = Binder.getCallingUid();
+ final int userId = UserHandle.getUserId(uid);
+ if (DEBUG) {
+ Slog.d(TAG, "--- calledFromForegroundUserOrSystemProcess ? "
+ + "calling uid = " + uid + " system uid = " + Process.SYSTEM_UID
+ + " calling userId = " + userId + ", foreground user id = "
+ + mSettings.getCurrentUserId() + ", calling pid = " + Binder.getCallingPid()
+ + InputMethodUtils.getApiCallStack());
+ }
+ if (uid == Process.SYSTEM_UID || userId == mSettings.getCurrentUserId()) {
+ return true;
+ }
+
+ // Caveat: A process which has INTERACT_ACROSS_USERS_FULL gets results for the
+ // foreground user, not for the user of that process. Accordingly InputMethodManagerService
+ // must not manage background users' states in any functions.
+ // Note that privacy-sensitive IPCs, such as setInputMethod, are still securely guarded
+ // by a token.
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS_FULL)
+ == PackageManager.PERMISSION_GRANTED) {
+ if (DEBUG) {
+ Slog.d(TAG, "--- Access granted because the calling process has "
+ + "the INTERACT_ACROSS_USERS_FULL permission");
+ }
+ return true;
+ }
+ Slog.w(TAG, "--- IPC called from background users. Ignore. \n"
+ + InputMethodUtils.getStackTrace());
+ return false;
+ }
+
+ private boolean bindCurrentInputMethodService(
+ Intent service, ServiceConnection conn, int flags) {
+ if (service == null || conn == null) {
+ Slog.e(TAG, "--- bind failed: service = " + service + ", conn = " + conn);
+ return false;
+ }
+ return mContext.bindServiceAsUser(service, conn, flags,
+ new UserHandle(mSettings.getCurrentUserId()));
+ }
+
+ @Override
+ public List<InputMethodInfo> getInputMethodList() {
+ // TODO: Make this work even for non-current users?
+ if (!calledFromValidUser()) {
+ return Collections.emptyList();
+ }
+ synchronized (mMethodMap) {
+ return new ArrayList<InputMethodInfo>(mMethodList);
+ }
+ }
+
+ @Override
+ public List<InputMethodInfo> getEnabledInputMethodList() {
+ // TODO: Make this work even for non-current users?
+ if (!calledFromValidUser()) {
+ return Collections.emptyList();
+ }
+ synchronized (mMethodMap) {
+ return mSettings.getEnabledInputMethodListLocked();
+ }
+ }
+
+ private HashMap<InputMethodInfo, List<InputMethodSubtype>>
+ getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked() {
+ HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
+ new HashMap<InputMethodInfo, List<InputMethodSubtype>>();
+ for (InputMethodInfo imi: mSettings.getEnabledInputMethodListLocked()) {
+ enabledInputMethodAndSubtypes.put(
+ imi, mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true));
+ }
+ return enabledInputMethodAndSubtypes;
+ }
+
+ /**
+ * @param imiId if null, returns enabled subtypes for the current imi
+ * @return enabled subtypes of the specified imi
+ */
+ @Override
+ public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId,
+ boolean allowsImplicitlySelectedSubtypes) {
+ // TODO: Make this work even for non-current users?
+ if (!calledFromValidUser()) {
+ return Collections.<InputMethodSubtype>emptyList();
+ }
+ synchronized (mMethodMap) {
+ final InputMethodInfo imi;
+ if (imiId == null && mCurMethodId != null) {
+ imi = mMethodMap.get(mCurMethodId);
+ } else {
+ imi = mMethodMap.get(imiId);
+ }
+ if (imi == null) {
+ return Collections.<InputMethodSubtype>emptyList();
+ }
+ return mSettings.getEnabledInputMethodSubtypeListLocked(
+ mContext, imi, allowsImplicitlySelectedSubtypes);
+ }
+ }
+
+ @Override
+ public void addClient(IInputMethodClient client,
+ IInputContext inputContext, int uid, int pid) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ mClients.put(client.asBinder(), new ClientState(client,
+ inputContext, uid, pid));
+ }
+ }
+
+ @Override
+ public void removeClient(IInputMethodClient client) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ ClientState cs = mClients.remove(client.asBinder());
+ if (cs != null) {
+ clearClientSessionLocked(cs);
+ }
+ }
+ }
+
+ void executeOrSendMessage(IInterface target, Message msg) {
+ if (target.asBinder() instanceof Binder) {
+ mCaller.sendMessage(msg);
+ } else {
+ handleMessage(msg);
+ msg.recycle();
+ }
+ }
+
+ void unbindCurrentClientLocked() {
+ if (mCurClient != null) {
+ if (DEBUG) Slog.v(TAG, "unbindCurrentInputLocked: client = "
+ + mCurClient.client.asBinder());
+ if (mBoundToMethod) {
+ mBoundToMethod = false;
+ if (mCurMethod != null) {
+ executeOrSendMessage(mCurMethod, mCaller.obtainMessageO(
+ MSG_UNBIND_INPUT, mCurMethod));
+ }
+ }
+
+ executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO(
+ MSG_SET_ACTIVE, 0, mCurClient));
+ executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO(
+ MSG_UNBIND_METHOD, mCurSeq, mCurClient.client));
+ mCurClient.sessionRequested = false;
+ mCurClient = null;
+
+ hideInputMethodMenuLocked();
+ }
+ }
+
+ private int getImeShowFlags() {
+ int flags = 0;
+ if (mShowForced) {
+ flags |= InputMethod.SHOW_FORCED
+ | InputMethod.SHOW_EXPLICIT;
+ } else if (mShowExplicitlyRequested) {
+ flags |= InputMethod.SHOW_EXPLICIT;
+ }
+ return flags;
+ }
+
+ private int getAppShowFlags() {
+ int flags = 0;
+ if (mShowForced) {
+ flags |= InputMethodManager.SHOW_FORCED;
+ } else if (!mShowExplicitlyRequested) {
+ flags |= InputMethodManager.SHOW_IMPLICIT;
+ }
+ return flags;
+ }
+
+ InputBindResult attachNewInputLocked(boolean initial) {
+ if (!mBoundToMethod) {
+ executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
+ MSG_BIND_INPUT, mCurMethod, mCurClient.binding));
+ mBoundToMethod = true;
+ }
+ final SessionState session = mCurClient.curSession;
+ if (initial) {
+ executeOrSendMessage(session.method, mCaller.obtainMessageOOO(
+ MSG_START_INPUT, session, mCurInputContext, mCurAttribute));
+ } else {
+ executeOrSendMessage(session.method, mCaller.obtainMessageOOO(
+ MSG_RESTART_INPUT, session, mCurInputContext, mCurAttribute));
+ }
+ if (mShowRequested) {
+ if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
+ showCurrentInputLocked(getAppShowFlags(), null);
+ }
+ return new InputBindResult(session.session,
+ session.channel != null ? session.channel.dup() : null, mCurId, mCurSeq);
+ }
+
+ InputBindResult startInputLocked(IInputMethodClient client,
+ IInputContext inputContext, EditorInfo attribute, int controlFlags) {
+ // If no method is currently selected, do nothing.
+ if (mCurMethodId == null) {
+ return mNoBinding;
+ }
+
+ ClientState cs = mClients.get(client.asBinder());
+ if (cs == null) {
+ throw new IllegalArgumentException("unknown client "
+ + client.asBinder());
+ }
+
+ try {
+ if (!mIWindowManager.inputMethodClientHasFocus(cs.client)) {
+ // Check with the window manager to make sure this client actually
+ // has a window with focus. If not, reject. This is thread safe
+ // because if the focus changes some time before or after, the
+ // next client receiving focus that has any interest in input will
+ // be calling through here after that change happens.
+ Slog.w(TAG, "Starting input on non-focused client " + cs.client
+ + " (uid=" + cs.uid + " pid=" + cs.pid + ")");
+ return null;
+ }
+ } catch (RemoteException e) {
+ }
+
+ return startInputUncheckedLocked(cs, inputContext, attribute, controlFlags);
+ }
+
+ InputBindResult startInputUncheckedLocked(ClientState cs,
+ IInputContext inputContext, EditorInfo attribute, int controlFlags) {
+ // If no method is currently selected, do nothing.
+ if (mCurMethodId == null) {
+ return mNoBinding;
+ }
+
+ if (mCurClient != cs) {
+ // Was the keyguard locked when switching over to the new client?
+ mCurClientInKeyguard = isKeyguardLocked();
+ // If the client is changing, we need to switch over to the new
+ // one.
+ unbindCurrentClientLocked();
+ if (DEBUG) Slog.v(TAG, "switching to client: client = "
+ + cs.client.asBinder() + " keyguard=" + mCurClientInKeyguard);
+
+ // If the screen is on, inform the new client it is active
+ if (mScreenOn) {
+ executeOrSendMessage(cs.client, mCaller.obtainMessageIO(
+ MSG_SET_ACTIVE, mScreenOn ? 1 : 0, cs));
+ }
+ }
+
+ // Bump up the sequence for this client and attach it.
+ mCurSeq++;
+ if (mCurSeq <= 0) mCurSeq = 1;
+ mCurClient = cs;
+ mCurInputContext = inputContext;
+ mCurAttribute = attribute;
+
+ // Check if the input method is changing.
+ if (mCurId != null && mCurId.equals(mCurMethodId)) {
+ if (cs.curSession != null) {
+ // Fast case: if we are already connected to the input method,
+ // then just return it.
+ return attachNewInputLocked(
+ (controlFlags&InputMethodManager.CONTROL_START_INITIAL) != 0);
+ }
+ if (mHaveConnection) {
+ if (mCurMethod != null) {
+ // Return to client, and we will get back with it when
+ // we have had a session made for it.
+ requestClientSessionLocked(cs);
+ return new InputBindResult(null, null, mCurId, mCurSeq);
+ } else if (SystemClock.uptimeMillis()
+ < (mLastBindTime+TIME_TO_RECONNECT)) {
+ // In this case we have connected to the service, but
+ // don't yet have its interface. If it hasn't been too
+ // long since we did the connection, we'll return to
+ // the client and wait to get the service interface so
+ // we can report back. If it has been too long, we want
+ // to fall through so we can try a disconnect/reconnect
+ // to see if we can get back in touch with the service.
+ return new InputBindResult(null, null, mCurId, mCurSeq);
+ } else {
+ EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME,
+ mCurMethodId, SystemClock.uptimeMillis()-mLastBindTime, 0);
+ }
+ }
+ }
+
+ return startInputInnerLocked();
+ }
+
+ InputBindResult startInputInnerLocked() {
+ if (mCurMethodId == null) {
+ return mNoBinding;
+ }
+
+ if (!mSystemReady) {
+ // If the system is not yet ready, we shouldn't be running third
+ // party code.
+ return new InputBindResult(null, null, mCurMethodId, mCurSeq);
+ }
+
+ InputMethodInfo info = mMethodMap.get(mCurMethodId);
+ if (info == null) {
+ throw new IllegalArgumentException("Unknown id: " + mCurMethodId);
+ }
+
+ unbindCurrentMethodLocked(false, true);
+
+ mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
+ mCurIntent.setComponent(info.getComponent());
+ mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
+ com.android.internal.R.string.input_method_binding_label);
+ mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
+ mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS), 0));
+ if (bindCurrentInputMethodService(mCurIntent, this, Context.BIND_AUTO_CREATE
+ | Context.BIND_NOT_VISIBLE | Context.BIND_SHOWING_UI)) {
+ mLastBindTime = SystemClock.uptimeMillis();
+ mHaveConnection = true;
+ mCurId = info.getId();
+ mCurToken = new Binder();
+ try {
+ if (true || DEBUG) Slog.v(TAG, "Adding window token: " + mCurToken);
+ mIWindowManager.addWindowToken(mCurToken,
+ WindowManager.LayoutParams.TYPE_INPUT_METHOD);
+ } catch (RemoteException e) {
+ }
+ return new InputBindResult(null, null, mCurId, mCurSeq);
+ } else {
+ mCurIntent = null;
+ Slog.w(TAG, "Failure connecting to input method service: "
+ + mCurIntent);
+ }
+ return null;
+ }
+
+ @Override
+ public InputBindResult startInput(IInputMethodClient client,
+ IInputContext inputContext, EditorInfo attribute, int controlFlags) {
+ if (!calledFromValidUser()) {
+ return null;
+ }
+ synchronized (mMethodMap) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ return startInputLocked(client, inputContext, attribute, controlFlags);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @Override
+ public void finishInput(IInputMethodClient client) {
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mMethodMap) {
+ if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
+ mCurMethod = IInputMethod.Stub.asInterface(service);
+ if (mCurToken == null) {
+ Slog.w(TAG, "Service connected without a token!");
+ unbindCurrentMethodLocked(false, false);
+ return;
+ }
+ if (DEBUG) Slog.v(TAG, "Initiating attach with token: " + mCurToken);
+ executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
+ MSG_ATTACH_TOKEN, mCurMethod, mCurToken));
+ if (mCurClient != null) {
+ clearClientSessionLocked(mCurClient);
+ requestClientSessionLocked(mCurClient);
+ }
+ }
+ }
+ }
+
+ void onSessionCreated(IInputMethod method, IInputMethodSession session,
+ InputChannel channel) {
+ synchronized (mMethodMap) {
+ if (mCurMethod != null && method != null
+ && mCurMethod.asBinder() == method.asBinder()) {
+ if (mCurClient != null) {
+ clearClientSessionLocked(mCurClient);
+ mCurClient.curSession = new SessionState(mCurClient,
+ method, session, channel);
+ InputBindResult res = attachNewInputLocked(true);
+ if (res.method != null) {
+ executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(
+ MSG_BIND_METHOD, mCurClient.client, res));
+ }
+ return;
+ }
+ }
+ }
+
+ // Session abandoned. Close its associated input channel.
+ channel.dispose();
+ }
+
+ void unbindCurrentMethodLocked(boolean reportToClient, boolean savePosition) {
+ if (mVisibleBound) {
+ mContext.unbindService(mVisibleConnection);
+ mVisibleBound = false;
+ }
+
+ if (mHaveConnection) {
+ mContext.unbindService(this);
+ mHaveConnection = false;
+ }
+
+ if (mCurToken != null) {
+ try {
+ if (DEBUG) Slog.v(TAG, "Removing window token: " + mCurToken);
+ if ((mImeWindowVis & InputMethodService.IME_ACTIVE) != 0 && savePosition) {
+ // The current IME is shown. Hence an IME switch (transition) is happening.
+ mWindowManagerService.saveLastInputMethodWindowForTransition();
+ }
+ mIWindowManager.removeWindowToken(mCurToken);
+ } catch (RemoteException e) {
+ }
+ mCurToken = null;
+ }
+
+ mCurId = null;
+ clearCurMethodLocked();
+
+ if (reportToClient && mCurClient != null) {
+ executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO(
+ MSG_UNBIND_METHOD, mCurSeq, mCurClient.client));
+ }
+ }
+
+ void requestClientSessionLocked(ClientState cs) {
+ if (!cs.sessionRequested) {
+ if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
+ InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());
+ cs.sessionRequested = true;
+ executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO(
+ MSG_CREATE_SESSION, mCurMethod, channels[1],
+ new MethodCallback(this, mCurMethod, channels[0])));
+ }
+ }
+
+ void clearClientSessionLocked(ClientState cs) {
+ finishSessionLocked(cs.curSession);
+ cs.curSession = null;
+ cs.sessionRequested = false;
+ }
+
+ private void finishSessionLocked(SessionState sessionState) {
+ if (sessionState != null) {
+ if (sessionState.session != null) {
+ try {
+ sessionState.session.finishSession();
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Session failed to close due to remote exception", e);
+ setImeWindowVisibilityStatusHiddenLocked();
+ }
+ sessionState.session = null;
+ }
+ if (sessionState.channel != null) {
+ sessionState.channel.dispose();
+ sessionState.channel = null;
+ }
+ }
+ }
+
+ void clearCurMethodLocked() {
+ if (mCurMethod != null) {
+ for (ClientState cs : mClients.values()) {
+ clearClientSessionLocked(cs);
+ }
+
+ finishSessionLocked(mEnabledSession);
+ mEnabledSession = null;
+ mCurMethod = null;
+ }
+ if (mStatusBar != null) {
+ mStatusBar.setIconVisibility("ime", false);
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (mMethodMap) {
+ if (DEBUG) Slog.v(TAG, "Service disconnected: " + name
+ + " mCurIntent=" + mCurIntent);
+ if (mCurMethod != null && mCurIntent != null
+ && name.equals(mCurIntent.getComponent())) {
+ clearCurMethodLocked();
+ // We consider this to be a new bind attempt, since the system
+ // should now try to restart the service for us.
+ mLastBindTime = SystemClock.uptimeMillis();
+ mShowRequested = mInputShown;
+ mInputShown = false;
+ if (mCurClient != null) {
+ executeOrSendMessage(mCurClient.client, mCaller.obtainMessageIO(
+ MSG_UNBIND_METHOD, mCurSeq, mCurClient.client));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void updateStatusIcon(IBinder token, String packageName, int iconId) {
+ int uid = Binder.getCallingUid();
+ long ident = Binder.clearCallingIdentity();
+ try {
+ if (token == null || mCurToken != token) {
+ Slog.w(TAG, "Ignoring setInputMethod of uid " + uid + " token: " + token);
+ return;
+ }
+
+ synchronized (mMethodMap) {
+ if (iconId == 0) {
+ if (DEBUG) Slog.d(TAG, "hide the small icon for the input method");
+ if (mStatusBar != null) {
+ mStatusBar.setIconVisibility("ime", false);
+ }
+ } else if (packageName != null) {
+ if (DEBUG) Slog.d(TAG, "show a small icon for the input method");
+ CharSequence contentDescription = null;
+ try {
+ // Use PackageManager to load label
+ final PackageManager packageManager = mContext.getPackageManager();
+ contentDescription = packageManager.getApplicationLabel(
+ mIPackageManager.getApplicationInfo(packageName, 0,
+ mSettings.getCurrentUserId()));
+ } catch (RemoteException e) {
+ /* ignore */
+ }
+ if (mStatusBar != null) {
+ mStatusBar.setIcon("ime", packageName, iconId, 0,
+ contentDescription != null
+ ? contentDescription.toString() : null);
+ mStatusBar.setIconVisibility("ime", true);
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private boolean needsToShowImeSwitchOngoingNotification() {
+ if (!mShowOngoingImeSwitcherForPhones) return false;
+ if (isScreenLocked()) return false;
+ synchronized (mMethodMap) {
+ List<InputMethodInfo> imis = mSettings.getEnabledInputMethodListLocked();
+ final int N = imis.size();
+ if (N > 2) return true;
+ if (N < 1) return false;
+ int nonAuxCount = 0;
+ int auxCount = 0;
+ InputMethodSubtype nonAuxSubtype = null;
+ InputMethodSubtype auxSubtype = null;
+ for(int i = 0; i < N; ++i) {
+ final InputMethodInfo imi = imis.get(i);
+ final List<InputMethodSubtype> subtypes =
+ mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true);
+ final int subtypeCount = subtypes.size();
+ if (subtypeCount == 0) {
+ ++nonAuxCount;
+ } else {
+ for (int j = 0; j < subtypeCount; ++j) {
+ final InputMethodSubtype subtype = subtypes.get(j);
+ if (!subtype.isAuxiliary()) {
+ ++nonAuxCount;
+ nonAuxSubtype = subtype;
+ } else {
+ ++auxCount;
+ auxSubtype = subtype;
+ }
+ }
+ }
+ }
+ if (nonAuxCount > 1 || auxCount > 1) {
+ return true;
+ } else if (nonAuxCount == 1 && auxCount == 1) {
+ if (nonAuxSubtype != null && auxSubtype != null
+ && (nonAuxSubtype.getLocale().equals(auxSubtype.getLocale())
+ || auxSubtype.overridesImplicitlyEnabledSubtype()
+ || nonAuxSubtype.overridesImplicitlyEnabledSubtype())
+ && nonAuxSubtype.containsExtraValueKey(TAG_TRY_SUPPRESSING_IME_SWITCHER)) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private boolean isKeyguardLocked() {
+ return mKeyguardManager != null && mKeyguardManager.isKeyguardLocked();
+ }
+
+ // Caution! This method is called in this class. Handle multi-user carefully
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setImeWindowStatus(IBinder token, int vis, int backDisposition) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ if (token == null || mCurToken != token) {
+ int uid = Binder.getCallingUid();
+ Slog.w(TAG, "Ignoring setImeWindowStatus of uid " + uid + " token: " + token);
+ return;
+ }
+ synchronized (mMethodMap) {
+ // apply policy for binder calls
+ if (vis != 0 && isKeyguardLocked() && !mCurClientInKeyguard) {
+ vis = 0;
+ }
+ mImeWindowVis = vis;
+ mBackDisposition = backDisposition;
+ if (mStatusBar != null) {
+ mStatusBar.setImeWindowStatus(token, vis, backDisposition);
+ }
+ final boolean iconVisibility = ((vis & (InputMethodService.IME_ACTIVE)) != 0)
+ && (mWindowManagerService.isHardKeyboardAvailable()
+ || (vis & (InputMethodService.IME_VISIBLE)) != 0);
+ final InputMethodInfo imi = mMethodMap.get(mCurMethodId);
+ if (imi != null && iconVisibility && needsToShowImeSwitchOngoingNotification()) {
+ // Used to load label
+ final CharSequence title = mRes.getText(
+ com.android.internal.R.string.select_input_method);
+ final CharSequence summary = InputMethodUtils.getImeAndSubtypeDisplayName(
+ mContext, imi, mCurrentSubtype);
+
+ mImeSwitcherNotification.setLatestEventInfo(
+ mContext, title, summary, mImeSwitchPendingIntent);
+ if (mNotificationManager != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "--- show notification: label = " + summary);
+ }
+ mNotificationManager.notifyAsUser(null,
+ com.android.internal.R.string.select_input_method,
+ mImeSwitcherNotification, UserHandle.ALL);
+ mNotificationShown = true;
+ }
+ } else {
+ if (mNotificationShown && mNotificationManager != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "--- hide notification");
+ }
+ mNotificationManager.cancelAsUser(null,
+ com.android.internal.R.string.select_input_method, UserHandle.ALL);
+ mNotificationShown = false;
+ }
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public void registerSuggestionSpansForNotification(SuggestionSpan[] spans) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ final InputMethodInfo currentImi = mMethodMap.get(mCurMethodId);
+ for (int i = 0; i < spans.length; ++i) {
+ SuggestionSpan ss = spans[i];
+ if (!TextUtils.isEmpty(ss.getNotificationTargetClassName())) {
+ mSecureSuggestionSpans.put(ss, currentImi);
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean notifySuggestionPicked(SuggestionSpan span, String originalString, int index) {
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ synchronized (mMethodMap) {
+ final InputMethodInfo targetImi = mSecureSuggestionSpans.get(span);
+ // TODO: Do not send the intent if the process of the targetImi is already dead.
+ if (targetImi != null) {
+ final String[] suggestions = span.getSuggestions();
+ if (index < 0 || index >= suggestions.length) return false;
+ final String className = span.getNotificationTargetClassName();
+ final Intent intent = new Intent();
+ // Ensures that only a class in the original IME package will receive the
+ // notification.
+ intent.setClassName(targetImi.getPackageName(), className);
+ intent.setAction(SuggestionSpan.ACTION_SUGGESTION_PICKED);
+ intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_BEFORE, originalString);
+ intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_AFTER, suggestions[index]);
+ intent.putExtra(SuggestionSpan.SUGGESTION_SPAN_PICKED_HASHCODE, span.hashCode());
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void updateFromSettingsLocked(boolean enabledMayChange) {
+ if (enabledMayChange) {
+ List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+ for (int i=0; i<enabled.size(); i++) {
+ // We allow the user to select "disabled until used" apps, so if they
+ // are enabling one of those here we now need to make it enabled.
+ InputMethodInfo imm = enabled.get(i);
+ try {
+ ApplicationInfo ai = mIPackageManager.getApplicationInfo(imm.getPackageName(),
+ PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS,
+ mSettings.getCurrentUserId());
+ if (ai != null && ai.enabledSetting
+ == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+ if (DEBUG) {
+ Slog.d(TAG, "Update state(" + imm.getId()
+ + "): DISABLED_UNTIL_USED -> DEFAULT");
+ }
+ mIPackageManager.setApplicationEnabledSetting(imm.getPackageName(),
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
+ PackageManager.DONT_KILL_APP, mSettings.getCurrentUserId(),
+ mContext.getBasePackageName());
+ }
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ // We are assuming that whoever is changing DEFAULT_INPUT_METHOD and
+ // ENABLED_INPUT_METHODS is taking care of keeping them correctly in
+ // sync, so we will never have a DEFAULT_INPUT_METHOD that is not
+ // enabled.
+ String id = mSettings.getSelectedInputMethod();
+ // There is no input method selected, try to choose new applicable input method.
+ if (TextUtils.isEmpty(id) && chooseNewDefaultIMELocked()) {
+ id = mSettings.getSelectedInputMethod();
+ }
+ if (!TextUtils.isEmpty(id)) {
+ try {
+ setInputMethodLocked(id, mSettings.getSelectedInputMethodSubtypeId(id));
+ } catch (IllegalArgumentException e) {
+ Slog.w(TAG, "Unknown input method from prefs: " + id, e);
+ mCurMethodId = null;
+ unbindCurrentMethodLocked(true, false);
+ }
+ mShortcutInputMethodsAndSubtypes.clear();
+ } else {
+ // There is no longer an input method set, so stop any current one.
+ mCurMethodId = null;
+ unbindCurrentMethodLocked(true, false);
+ }
+ }
+
+ /* package */ void setInputMethodLocked(String id, int subtypeId) {
+ InputMethodInfo info = mMethodMap.get(id);
+ if (info == null) {
+ throw new IllegalArgumentException("Unknown id: " + id);
+ }
+
+ // See if we need to notify a subtype change within the same IME.
+ if (id.equals(mCurMethodId)) {
+ final int subtypeCount = info.getSubtypeCount();
+ if (subtypeCount <= 0) {
+ return;
+ }
+ final InputMethodSubtype oldSubtype = mCurrentSubtype;
+ final InputMethodSubtype newSubtype;
+ if (subtypeId >= 0 && subtypeId < subtypeCount) {
+ newSubtype = info.getSubtypeAt(subtypeId);
+ } else {
+ // If subtype is null, try to find the most applicable one from
+ // getCurrentInputMethodSubtype.
+ newSubtype = getCurrentInputMethodSubtypeLocked();
+ }
+ if (newSubtype == null || oldSubtype == null) {
+ Slog.w(TAG, "Illegal subtype state: old subtype = " + oldSubtype
+ + ", new subtype = " + newSubtype);
+ return;
+ }
+ if (newSubtype != oldSubtype) {
+ setSelectedInputMethodAndSubtypeLocked(info, subtypeId, true);
+ if (mCurMethod != null) {
+ try {
+ refreshImeWindowVisibilityLocked();
+ mCurMethod.changeInputMethodSubtype(newSubtype);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to call changeInputMethodSubtype");
+ }
+ }
+ }
+ return;
+ }
+
+ // Changing to a different IME.
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ // Set a subtype to this input method.
+ // subtypeId the name of a subtype which will be set.
+ setSelectedInputMethodAndSubtypeLocked(info, subtypeId, false);
+ // mCurMethodId should be updated after setSelectedInputMethodAndSubtypeLocked()
+ // because mCurMethodId is stored as a history in
+ // setSelectedInputMethodAndSubtypeLocked().
+ mCurMethodId = id;
+
+ if (ActivityManagerNative.isSystemReady()) {
+ Intent intent = new Intent(Intent.ACTION_INPUT_METHOD_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+ intent.putExtra("input_method_id", id);
+ mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT);
+ }
+ unbindCurrentClientLocked();
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ @Override
+ public boolean showSoftInput(IInputMethodClient client, int flags,
+ ResultReceiver resultReceiver) {
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ int uid = Binder.getCallingUid();
+ long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mMethodMap) {
+ if (mCurClient == null || client == null
+ || mCurClient.client.asBinder() != client.asBinder()) {
+ try {
+ // We need to check if this is the current client with
+ // focus in the window manager, to allow this call to
+ // be made before input is started in it.
+ if (!mIWindowManager.inputMethodClientHasFocus(client)) {
+ Slog.w(TAG, "Ignoring showSoftInput of uid " + uid + ": " + client);
+ return false;
+ }
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ if (DEBUG) Slog.v(TAG, "Client requesting input be shown");
+ return showCurrentInputLocked(flags, resultReceiver);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ boolean showCurrentInputLocked(int flags, ResultReceiver resultReceiver) {
+ mShowRequested = true;
+ if ((flags&InputMethodManager.SHOW_IMPLICIT) == 0) {
+ mShowExplicitlyRequested = true;
+ }
+ if ((flags&InputMethodManager.SHOW_FORCED) != 0) {
+ mShowExplicitlyRequested = true;
+ mShowForced = true;
+ }
+
+ if (!mSystemReady) {
+ return false;
+ }
+
+ boolean res = false;
+ if (mCurMethod != null) {
+ if (DEBUG) Slog.d(TAG, "showCurrentInputLocked: mCurToken=" + mCurToken);
+ executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
+ MSG_SHOW_SOFT_INPUT, getImeShowFlags(), mCurMethod,
+ resultReceiver));
+ mInputShown = true;
+ if (mHaveConnection && !mVisibleBound) {
+ bindCurrentInputMethodService(
+ mCurIntent, mVisibleConnection, Context.BIND_AUTO_CREATE);
+ mVisibleBound = true;
+ }
+ res = true;
+ } else if (mHaveConnection && SystemClock.uptimeMillis()
+ >= (mLastBindTime+TIME_TO_RECONNECT)) {
+ // The client has asked to have the input method shown, but
+ // we have been sitting here too long with a connection to the
+ // service and no interface received, so let's disconnect/connect
+ // to try to prod things along.
+ EventLog.writeEvent(EventLogTags.IMF_FORCE_RECONNECT_IME, mCurMethodId,
+ SystemClock.uptimeMillis()-mLastBindTime,1);
+ Slog.w(TAG, "Force disconnect/connect to the IME in showCurrentInputLocked()");
+ mContext.unbindService(this);
+ bindCurrentInputMethodService(mCurIntent, this, Context.BIND_AUTO_CREATE
+ | Context.BIND_NOT_VISIBLE);
+ } else {
+ if (DEBUG) {
+ Slog.d(TAG, "Can't show input: connection = " + mHaveConnection + ", time = "
+ + ((mLastBindTime+TIME_TO_RECONNECT) - SystemClock.uptimeMillis()));
+ }
+ }
+
+ return res;
+ }
+
+ @Override
+ public boolean hideSoftInput(IInputMethodClient client, int flags,
+ ResultReceiver resultReceiver) {
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ int uid = Binder.getCallingUid();
+ long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mMethodMap) {
+ if (mCurClient == null || client == null
+ || mCurClient.client.asBinder() != client.asBinder()) {
+ try {
+ // We need to check if this is the current client with
+ // focus in the window manager, to allow this call to
+ // be made before input is started in it.
+ if (!mIWindowManager.inputMethodClientHasFocus(client)) {
+ if (DEBUG) Slog.w(TAG, "Ignoring hideSoftInput of uid "
+ + uid + ": " + client);
+ setImeWindowVisibilityStatusHiddenLocked();
+ return false;
+ }
+ } catch (RemoteException e) {
+ setImeWindowVisibilityStatusHiddenLocked();
+ return false;
+ }
+ }
+
+ if (DEBUG) Slog.v(TAG, "Client requesting input be hidden");
+ return hideCurrentInputLocked(flags, resultReceiver);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ boolean hideCurrentInputLocked(int flags, ResultReceiver resultReceiver) {
+ if ((flags&InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
+ && (mShowExplicitlyRequested || mShowForced)) {
+ if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
+ return false;
+ }
+ if (mShowForced && (flags&InputMethodManager.HIDE_NOT_ALWAYS) != 0) {
+ if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide");
+ return false;
+ }
+ boolean res;
+ if (mInputShown && mCurMethod != null) {
+ executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
+ MSG_HIDE_SOFT_INPUT, mCurMethod, resultReceiver));
+ res = true;
+ } else {
+ res = false;
+ }
+ if (mHaveConnection && mVisibleBound) {
+ mContext.unbindService(mVisibleConnection);
+ mVisibleBound = false;
+ }
+ mInputShown = false;
+ mShowRequested = false;
+ mShowExplicitlyRequested = false;
+ mShowForced = false;
+ return res;
+ }
+
+ @Override
+ public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken,
+ int controlFlags, int softInputMode, int windowFlags,
+ EditorInfo attribute, IInputContext inputContext) {
+ // Needs to check the validity before clearing calling identity
+ final boolean calledFromValidUser = calledFromValidUser();
+
+ InputBindResult res = null;
+ long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mMethodMap) {
+ if (DEBUG) Slog.v(TAG, "windowGainedFocus: " + client.asBinder()
+ + " controlFlags=#" + Integer.toHexString(controlFlags)
+ + " softInputMode=#" + Integer.toHexString(softInputMode)
+ + " windowFlags=#" + Integer.toHexString(windowFlags));
+
+ ClientState cs = mClients.get(client.asBinder());
+ if (cs == null) {
+ throw new IllegalArgumentException("unknown client "
+ + client.asBinder());
+ }
+
+ try {
+ if (!mIWindowManager.inputMethodClientHasFocus(cs.client)) {
+ // Check with the window manager to make sure this client actually
+ // has a window with focus. If not, reject. This is thread safe
+ // because if the focus changes some time before or after, the
+ // next client receiving focus that has any interest in input will
+ // be calling through here after that change happens.
+ Slog.w(TAG, "Focus gain on non-focused client " + cs.client
+ + " (uid=" + cs.uid + " pid=" + cs.pid + ")");
+ return null;
+ }
+ } catch (RemoteException e) {
+ }
+
+ if (!calledFromValidUser) {
+ Slog.w(TAG, "A background user is requesting window. Hiding IME.");
+ Slog.w(TAG, "If you want to interect with IME, you need "
+ + "android.permission.INTERACT_ACROSS_USERS_FULL");
+ hideCurrentInputLocked(0, null);
+ return null;
+ }
+
+ if (mCurFocusedWindow == windowToken) {
+ Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
+ + " attribute=" + attribute + ", token = " + windowToken);
+ if (attribute != null) {
+ return startInputUncheckedLocked(cs, inputContext, attribute,
+ controlFlags);
+ }
+ return null;
+ }
+ mCurFocusedWindow = windowToken;
+
+ // Should we auto-show the IME even if the caller has not
+ // specified what should be done with it?
+ // We only do this automatically if the window can resize
+ // to accommodate the IME (so what the user sees will give
+ // them good context without input information being obscured
+ // by the IME) or if running on a large screen where there
+ // is more room for the target window + IME.
+ final boolean doAutoShow =
+ (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST)
+ == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+ || mRes.getConfiguration().isLayoutSizeAtLeast(
+ Configuration.SCREENLAYOUT_SIZE_LARGE);
+ final boolean isTextEditor =
+ (controlFlags&InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR) != 0;
+
+ // We want to start input before showing the IME, but after closing
+ // it. We want to do this after closing it to help the IME disappear
+ // more quickly (not get stuck behind it initializing itself for the
+ // new focused input, even if its window wants to hide the IME).
+ boolean didStart = false;
+
+ switch (softInputMode&WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) {
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
+ if (!isTextEditor || !doAutoShow) {
+ if (WindowManager.LayoutParams.mayUseInputMethod(windowFlags)) {
+ // There is no focus view, and this window will
+ // be behind any soft input window, so hide the
+ // soft input window if it is shown.
+ if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
+ hideCurrentInputLocked(InputMethodManager.HIDE_NOT_ALWAYS, null);
+ }
+ } else if (isTextEditor && doAutoShow && (softInputMode &
+ WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
+ // There is a focus view, and we are navigating forward
+ // into the window, so show the input window for the user.
+ // We only do this automatically if the window can resize
+ // to accommodate the IME (so what the user sees will give
+ // them good context without input information being obscured
+ // by the IME) or if running on a large screen where there
+ // is more room for the target window + IME.
+ if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
+ if (attribute != null) {
+ res = startInputUncheckedLocked(cs, inputContext, attribute,
+ controlFlags);
+ didStart = true;
+ }
+ showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
+ // Do nothing.
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN:
+ if ((softInputMode &
+ WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
+ if (DEBUG) Slog.v(TAG, "Window asks to hide input going forward");
+ hideCurrentInputLocked(0, null);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN:
+ if (DEBUG) Slog.v(TAG, "Window asks to hide input");
+ hideCurrentInputLocked(0, null);
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE:
+ if ((softInputMode &
+ WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
+ if (DEBUG) Slog.v(TAG, "Window asks to show input going forward");
+ if (attribute != null) {
+ res = startInputUncheckedLocked(cs, inputContext, attribute,
+ controlFlags);
+ didStart = true;
+ }
+ showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
+ }
+ break;
+ case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE:
+ if (DEBUG) Slog.v(TAG, "Window asks to always show input");
+ if (attribute != null) {
+ res = startInputUncheckedLocked(cs, inputContext, attribute,
+ controlFlags);
+ didStart = true;
+ }
+ showCurrentInputLocked(InputMethodManager.SHOW_IMPLICIT, null);
+ break;
+ }
+
+ if (!didStart && attribute != null) {
+ res = startInputUncheckedLocked(cs, inputContext, attribute,
+ controlFlags);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+
+ return res;
+ }
+
+ @Override
+ public void showInputMethodPickerFromClient(IInputMethodClient client) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ if (mCurClient == null || client == null
+ || mCurClient.client.asBinder() != client.asBinder()) {
+ Slog.w(TAG, "Ignoring showInputMethodPickerFromClient of uid "
+ + Binder.getCallingUid() + ": " + client);
+ }
+
+ // Always call subtype picker, because subtype picker is a superset of input method
+ // picker.
+ mHandler.sendEmptyMessage(MSG_SHOW_IM_SUBTYPE_PICKER);
+ }
+ }
+
+ @Override
+ public void setInputMethod(IBinder token, String id) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ setInputMethodWithSubtypeId(token, id, NOT_A_SUBTYPE_ID);
+ }
+
+ @Override
+ public void setInputMethodAndSubtype(IBinder token, String id, InputMethodSubtype subtype) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ if (subtype != null) {
+ setInputMethodWithSubtypeId(token, id, InputMethodUtils.getSubtypeIdFromHashCode(
+ mMethodMap.get(id), subtype.hashCode()));
+ } else {
+ setInputMethod(token, id);
+ }
+ }
+ }
+
+ @Override
+ public void showInputMethodAndSubtypeEnablerFromClient(
+ IInputMethodClient client, String inputMethodId) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ if (mCurClient == null || client == null
+ || mCurClient.client.asBinder() != client.asBinder()) {
+ Slog.w(TAG, "Ignoring showInputMethodAndSubtypeEnablerFromClient of: " + client);
+ }
+ executeOrSendMessage(mCurMethod, mCaller.obtainMessageO(
+ MSG_SHOW_IM_SUBTYPE_ENABLER, inputMethodId));
+ }
+ }
+
+ @Override
+ public boolean switchToLastInputMethod(IBinder token) {
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ synchronized (mMethodMap) {
+ final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked();
+ final InputMethodInfo lastImi;
+ if (lastIme != null) {
+ lastImi = mMethodMap.get(lastIme.first);
+ } else {
+ lastImi = null;
+ }
+ String targetLastImiId = null;
+ int subtypeId = NOT_A_SUBTYPE_ID;
+ if (lastIme != null && lastImi != null) {
+ final boolean imiIdIsSame = lastImi.getId().equals(mCurMethodId);
+ final int lastSubtypeHash = Integer.valueOf(lastIme.second);
+ final int currentSubtypeHash = mCurrentSubtype == null ? NOT_A_SUBTYPE_ID
+ : mCurrentSubtype.hashCode();
+ // If the last IME is the same as the current IME and the last subtype is not
+ // defined, there is no need to switch to the last IME.
+ if (!imiIdIsSame || lastSubtypeHash != currentSubtypeHash) {
+ targetLastImiId = lastIme.first;
+ subtypeId = InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash);
+ }
+ }
+
+ if (TextUtils.isEmpty(targetLastImiId)
+ && !InputMethodUtils.canAddToLastInputMethod(mCurrentSubtype)) {
+ // This is a safety net. If the currentSubtype can't be added to the history
+ // and the framework couldn't find the last ime, we will make the last ime be
+ // the most applicable enabled keyboard subtype of the system imes.
+ final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+ if (enabled != null) {
+ final int N = enabled.size();
+ final String locale = mCurrentSubtype == null
+ ? mRes.getConfiguration().locale.toString()
+ : mCurrentSubtype.getLocale();
+ for (int i = 0; i < N; ++i) {
+ final InputMethodInfo imi = enabled.get(i);
+ if (imi.getSubtypeCount() > 0 && InputMethodUtils.isSystemIme(imi)) {
+ InputMethodSubtype keyboardSubtype =
+ InputMethodUtils.findLastResortApplicableSubtypeLocked(mRes,
+ InputMethodUtils.getSubtypes(imi),
+ InputMethodUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
+ if (keyboardSubtype != null) {
+ targetLastImiId = imi.getId();
+ subtypeId = InputMethodUtils.getSubtypeIdFromHashCode(
+ imi, keyboardSubtype.hashCode());
+ if(keyboardSubtype.getLocale().equals(locale)) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!TextUtils.isEmpty(targetLastImiId)) {
+ if (DEBUG) {
+ Slog.d(TAG, "Switch to: " + lastImi.getId() + ", " + lastIme.second
+ + ", from: " + mCurMethodId + ", " + subtypeId);
+ }
+ setInputMethodWithSubtypeId(token, targetLastImiId, subtypeId);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public boolean switchToNextInputMethod(IBinder token, boolean onlyCurrentIme) {
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ synchronized (mMethodMap) {
+ final ImeSubtypeListItem nextSubtype = mImListManager.getNextInputMethod(
+ onlyCurrentIme, mMethodMap.get(mCurMethodId), mCurrentSubtype);
+ if (nextSubtype == null) {
+ return false;
+ }
+ setInputMethodWithSubtypeId(token, nextSubtype.mImi.getId(), nextSubtype.mSubtypeId);
+ return true;
+ }
+ }
+
+ @Override
+ public boolean shouldOfferSwitchingToNextInputMethod(IBinder token) {
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ synchronized (mMethodMap) {
+ final ImeSubtypeListItem nextSubtype = mImListManager.getNextInputMethod(
+ false /* onlyCurrentIme */, mMethodMap.get(mCurMethodId), mCurrentSubtype);
+ if (nextSubtype == null) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ @Override
+ public InputMethodSubtype getLastInputMethodSubtype() {
+ if (!calledFromValidUser()) {
+ return null;
+ }
+ synchronized (mMethodMap) {
+ final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked();
+ // TODO: Handle the case of the last IME with no subtypes
+ if (lastIme == null || TextUtils.isEmpty(lastIme.first)
+ || TextUtils.isEmpty(lastIme.second)) return null;
+ final InputMethodInfo lastImi = mMethodMap.get(lastIme.first);
+ if (lastImi == null) return null;
+ try {
+ final int lastSubtypeHash = Integer.valueOf(lastIme.second);
+ final int lastSubtypeId =
+ InputMethodUtils.getSubtypeIdFromHashCode(lastImi, lastSubtypeHash);
+ if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) {
+ return null;
+ }
+ return lastImi.getSubtypeAt(lastSubtypeId);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ // By this IPC call, only a process which shares the same uid with the IME can add
+ // additional input method subtypes to the IME.
+ if (TextUtils.isEmpty(imiId) || subtypes == null || subtypes.length == 0) return;
+ synchronized (mMethodMap) {
+ final InputMethodInfo imi = mMethodMap.get(imiId);
+ if (imi == null) return;
+ final String[] packageInfos;
+ try {
+ packageInfos = mIPackageManager.getPackagesForUid(Binder.getCallingUid());
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get package infos");
+ return;
+ }
+ if (packageInfos != null) {
+ final int packageNum = packageInfos.length;
+ for (int i = 0; i < packageNum; ++i) {
+ if (packageInfos[i].equals(imi.getPackageName())) {
+ mFileManager.addInputMethodSubtypes(imi, subtypes);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ buildInputMethodListLocked(mMethodList, mMethodMap,
+ false /* resetDefaultEnabledIme */);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ return;
+ }
+ }
+ }
+ }
+ return;
+ }
+
+ private void setInputMethodWithSubtypeId(IBinder token, String id, int subtypeId) {
+ synchronized (mMethodMap) {
+ if (token == null) {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Using null token requires permission "
+ + android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ }
+ } else if (mCurToken != token) {
+ Slog.w(TAG, "Ignoring setInputMethod of uid " + Binder.getCallingUid()
+ + " token: " + token);
+ return;
+ }
+
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ setInputMethodLocked(id, subtypeId);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @Override
+ public void hideMySoftInput(IBinder token, int flags) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ if (token == null || mCurToken != token) {
+ if (DEBUG) Slog.w(TAG, "Ignoring hideInputMethod of uid "
+ + Binder.getCallingUid() + " token: " + token);
+ return;
+ }
+ long ident = Binder.clearCallingIdentity();
+ try {
+ hideCurrentInputLocked(flags, null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ @Override
+ public void showMySoftInput(IBinder token, int flags) {
+ if (!calledFromValidUser()) {
+ return;
+ }
+ synchronized (mMethodMap) {
+ if (token == null || mCurToken != token) {
+ Slog.w(TAG, "Ignoring showMySoftInput of uid "
+ + Binder.getCallingUid() + " token: " + token);
+ return;
+ }
+ long ident = Binder.clearCallingIdentity();
+ try {
+ showCurrentInputLocked(flags, null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ void setEnabledSessionInMainThread(SessionState session) {
+ if (mEnabledSession != session) {
+ if (mEnabledSession != null) {
+ try {
+ if (DEBUG) Slog.v(TAG, "Disabling: " + mEnabledSession);
+ mEnabledSession.method.setSessionEnabled(
+ mEnabledSession.session, false);
+ } catch (RemoteException e) {
+ }
+ }
+ mEnabledSession = session;
+ try {
+ if (DEBUG) Slog.v(TAG, "Enabling: " + mEnabledSession);
+ session.method.setSessionEnabled(
+ session.session, true);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ SomeArgs args;
+ switch (msg.what) {
+ case MSG_SHOW_IM_PICKER:
+ showInputMethodMenu();
+ return true;
+
+ case MSG_SHOW_IM_SUBTYPE_PICKER:
+ showInputMethodSubtypeMenu();
+ return true;
+
+ case MSG_SHOW_IM_SUBTYPE_ENABLER:
+ args = (SomeArgs)msg.obj;
+ showInputMethodAndSubtypeEnabler((String)args.arg1);
+ args.recycle();
+ return true;
+
+ case MSG_SHOW_IM_CONFIG:
+ showConfigureInputMethods();
+ return true;
+
+ // ---------------------------------------------------------
+
+ case MSG_UNBIND_INPUT:
+ try {
+ ((IInputMethod)msg.obj).unbindInput();
+ } catch (RemoteException e) {
+ // There is nothing interesting about the method dying.
+ }
+ return true;
+ case MSG_BIND_INPUT:
+ args = (SomeArgs)msg.obj;
+ try {
+ ((IInputMethod)args.arg1).bindInput((InputBinding)args.arg2);
+ } catch (RemoteException e) {
+ }
+ args.recycle();
+ return true;
+ case MSG_SHOW_SOFT_INPUT:
+ args = (SomeArgs)msg.obj;
+ try {
+ if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".showSoftInput("
+ + msg.arg1 + ", " + args.arg2 + ")");
+ ((IInputMethod)args.arg1).showSoftInput(msg.arg1, (ResultReceiver)args.arg2);
+ } catch (RemoteException e) {
+ }
+ args.recycle();
+ return true;
+ case MSG_HIDE_SOFT_INPUT:
+ args = (SomeArgs)msg.obj;
+ try {
+ if (DEBUG) Slog.v(TAG, "Calling " + args.arg1 + ".hideSoftInput(0, "
+ + args.arg2 + ")");
+ ((IInputMethod)args.arg1).hideSoftInput(0, (ResultReceiver)args.arg2);
+ } catch (RemoteException e) {
+ }
+ args.recycle();
+ return true;
+ case MSG_ATTACH_TOKEN:
+ args = (SomeArgs)msg.obj;
+ try {
+ if (DEBUG) Slog.v(TAG, "Sending attach of token: " + args.arg2);
+ ((IInputMethod)args.arg1).attachToken((IBinder)args.arg2);
+ } catch (RemoteException e) {
+ }
+ args.recycle();
+ return true;
+ case MSG_CREATE_SESSION: {
+ args = (SomeArgs)msg.obj;
+ IInputMethod method = (IInputMethod)args.arg1;
+ InputChannel channel = (InputChannel)args.arg2;
+ try {
+ method.createSession(channel, (IInputSessionCallback)args.arg3);
+ } catch (RemoteException e) {
+ } finally {
+ // Dispose the channel if the input method is not local to this process
+ // because the remote proxy will get its own copy when unparceled.
+ if (channel != null && Binder.isProxy(method)) {
+ channel.dispose();
+ }
+ }
+ args.recycle();
+ return true;
+ }
+ // ---------------------------------------------------------
+
+ case MSG_START_INPUT:
+ args = (SomeArgs)msg.obj;
+ try {
+ SessionState session = (SessionState)args.arg1;
+ setEnabledSessionInMainThread(session);
+ session.method.startInput((IInputContext)args.arg2,
+ (EditorInfo)args.arg3);
+ } catch (RemoteException e) {
+ }
+ args.recycle();
+ return true;
+ case MSG_RESTART_INPUT:
+ args = (SomeArgs)msg.obj;
+ try {
+ SessionState session = (SessionState)args.arg1;
+ setEnabledSessionInMainThread(session);
+ session.method.restartInput((IInputContext)args.arg2,
+ (EditorInfo)args.arg3);
+ } catch (RemoteException e) {
+ }
+ args.recycle();
+ return true;
+
+ // ---------------------------------------------------------
+
+ case MSG_UNBIND_METHOD:
+ try {
+ ((IInputMethodClient)msg.obj).onUnbindMethod(msg.arg1);
+ } catch (RemoteException e) {
+ // There is nothing interesting about the last client dying.
+ }
+ return true;
+ case MSG_BIND_METHOD: {
+ args = (SomeArgs)msg.obj;
+ IInputMethodClient client = (IInputMethodClient)args.arg1;
+ InputBindResult res = (InputBindResult)args.arg2;
+ try {
+ client.onBindMethod(res);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Client died receiving input method " + args.arg2);
+ } finally {
+ // Dispose the channel if the input method is not local to this process
+ // because the remote proxy will get its own copy when unparceled.
+ if (res.channel != null && Binder.isProxy(client)) {
+ res.channel.dispose();
+ }
+ }
+ args.recycle();
+ return true;
+ }
+ case MSG_SET_ACTIVE:
+ try {
+ ((ClientState)msg.obj).client.setActive(msg.arg1 != 0);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Got RemoteException sending setActive(false) notification to pid "
+ + ((ClientState)msg.obj).pid + " uid "
+ + ((ClientState)msg.obj).uid);
+ }
+ return true;
+
+ // --------------------------------------------------------------
+ case MSG_HARD_KEYBOARD_SWITCH_CHANGED:
+ mHardKeyboardListener.handleHardKeyboardStatusChange(
+ msg.arg1 == 1, msg.arg2 == 1);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean chooseNewDefaultIMELocked() {
+ final InputMethodInfo imi = InputMethodUtils.getMostApplicableDefaultIME(
+ mSettings.getEnabledInputMethodListLocked());
+ if (imi != null) {
+ if (DEBUG) {
+ Slog.d(TAG, "New default IME was selected: " + imi.getId());
+ }
+ resetSelectedInputMethodAndSubtypeLocked(imi.getId());
+ return true;
+ }
+
+ return false;
+ }
+
+ void buildInputMethodListLocked(ArrayList<InputMethodInfo> list,
+ HashMap<String, InputMethodInfo> map, boolean resetDefaultEnabledIme) {
+ if (DEBUG) {
+ Slog.d(TAG, "--- re-buildInputMethodList reset = " + resetDefaultEnabledIme
+ + " \n ------ \n" + InputMethodUtils.getStackTrace());
+ }
+ list.clear();
+ map.clear();
+
+ // Use for queryIntentServicesAsUser
+ final PackageManager pm = mContext.getPackageManager();
+ String disabledSysImes = mSettings.getDisabledSystemInputMethods();
+ if (disabledSysImes == null) disabledSysImes = "";
+
+ final List<ResolveInfo> services = pm.queryIntentServicesAsUser(
+ new Intent(InputMethod.SERVICE_INTERFACE),
+ PackageManager.GET_META_DATA | PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS,
+ mSettings.getCurrentUserId());
+
+ final HashMap<String, List<InputMethodSubtype>> additionalSubtypes =
+ mFileManager.getAllAdditionalInputMethodSubtypes();
+ for (int i = 0; i < services.size(); ++i) {
+ ResolveInfo ri = services.get(i);
+ ServiceInfo si = ri.serviceInfo;
+ ComponentName compName = new ComponentName(si.packageName, si.name);
+ if (!android.Manifest.permission.BIND_INPUT_METHOD.equals(
+ si.permission)) {
+ Slog.w(TAG, "Skipping input method " + compName
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_INPUT_METHOD);
+ continue;
+ }
+
+ if (DEBUG) Slog.d(TAG, "Checking " + compName);
+
+ try {
+ InputMethodInfo p = new InputMethodInfo(mContext, ri, additionalSubtypes);
+ list.add(p);
+ final String id = p.getId();
+ map.put(id, p);
+
+ if (DEBUG) {
+ Slog.d(TAG, "Found an input method " + p);
+ }
+
+ } catch (XmlPullParserException e) {
+ Slog.w(TAG, "Unable to load input method " + compName, e);
+ } catch (IOException e) {
+ Slog.w(TAG, "Unable to load input method " + compName, e);
+ }
+ }
+
+ if (resetDefaultEnabledIme) {
+ final ArrayList<InputMethodInfo> defaultEnabledIme =
+ InputMethodUtils.getDefaultEnabledImes(mContext, mSystemReady, list);
+ for (int i = 0; i < defaultEnabledIme.size(); ++i) {
+ final InputMethodInfo imi = defaultEnabledIme.get(i);
+ if (DEBUG) {
+ Slog.d(TAG, "--- enable ime = " + imi);
+ }
+ setInputMethodEnabledLocked(imi.getId(), true);
+ }
+ }
+
+ final String defaultImiId = mSettings.getSelectedInputMethod();
+ if (!TextUtils.isEmpty(defaultImiId)) {
+ if (!map.containsKey(defaultImiId)) {
+ Slog.w(TAG, "Default IME is uninstalled. Choose new default IME.");
+ if (chooseNewDefaultIMELocked()) {
+ updateFromSettingsLocked(true);
+ }
+ } else {
+ // Double check that the default IME is certainly enabled.
+ setInputMethodEnabledLocked(defaultImiId, true);
+ }
+ }
+ }
+
+ // ----------------------------------------------------------------------
+
+ private void showInputMethodMenu() {
+ showInputMethodMenuInternal(false);
+ }
+
+ private void showInputMethodSubtypeMenu() {
+ showInputMethodMenuInternal(true);
+ }
+
+ private void showInputMethodAndSubtypeEnabler(String inputMethodId) {
+ Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ if (!TextUtils.isEmpty(inputMethodId)) {
+ intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, inputMethodId);
+ }
+ mContext.startActivityAsUser(intent, null, UserHandle.CURRENT);
+ }
+
+ private void showConfigureInputMethods() {
+ Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ mContext.startActivityAsUser(intent, null, UserHandle.CURRENT);
+ }
+
+ private boolean isScreenLocked() {
+ return mKeyguardManager != null
+ && mKeyguardManager.isKeyguardLocked() && mKeyguardManager.isKeyguardSecure();
+ }
+ private void showInputMethodMenuInternal(boolean showSubtypes) {
+ if (DEBUG) Slog.v(TAG, "Show switching menu");
+
+ final Context context = mContext;
+ final boolean isScreenLocked = isScreenLocked();
+
+ final String lastInputMethodId = mSettings.getSelectedInputMethod();
+ int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
+ if (DEBUG) Slog.v(TAG, "Current IME: " + lastInputMethodId);
+
+ synchronized (mMethodMap) {
+ final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
+ getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked();
+ if (immis == null || immis.size() == 0) {
+ return;
+ }
+
+ hideInputMethodMenuLocked();
+
+ final List<ImeSubtypeListItem> imList =
+ mImListManager.getSortedInputMethodAndSubtypeList(
+ showSubtypes, mInputShown, isScreenLocked);
+
+ if (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID) {
+ final InputMethodSubtype currentSubtype = getCurrentInputMethodSubtypeLocked();
+ if (currentSubtype != null) {
+ final InputMethodInfo currentImi = mMethodMap.get(mCurMethodId);
+ lastInputMethodSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode(
+ currentImi, currentSubtype.hashCode());
+ }
+ }
+
+ final int N = imList.size();
+ mIms = new InputMethodInfo[N];
+ mSubtypeIds = new int[N];
+ int checkedItem = 0;
+ for (int i = 0; i < N; ++i) {
+ final ImeSubtypeListItem item = imList.get(i);
+ mIms[i] = item.mImi;
+ mSubtypeIds[i] = item.mSubtypeId;
+ if (mIms[i].getId().equals(lastInputMethodId)) {
+ int subtypeId = mSubtypeIds[i];
+ if ((subtypeId == NOT_A_SUBTYPE_ID)
+ || (lastInputMethodSubtypeId == NOT_A_SUBTYPE_ID && subtypeId == 0)
+ || (subtypeId == lastInputMethodSubtypeId)) {
+ checkedItem = i;
+ }
+ }
+ }
+ final TypedArray a = context.obtainStyledAttributes(null,
+ com.android.internal.R.styleable.DialogPreference,
+ com.android.internal.R.attr.alertDialogStyle, 0);
+ mDialogBuilder = new AlertDialog.Builder(context)
+ .setOnCancelListener(new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ hideInputMethodMenu();
+ }
+ })
+ .setIcon(a.getDrawable(
+ com.android.internal.R.styleable.DialogPreference_dialogTitle));
+ a.recycle();
+ final LayoutInflater inflater =
+ (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ final View tv = inflater.inflate(
+ com.android.internal.R.layout.input_method_switch_dialog_title, null);
+ mDialogBuilder.setCustomTitle(tv);
+
+ // Setup layout for a toggle switch of the hardware keyboard
+ mSwitchingDialogTitleView = tv;
+ mSwitchingDialogTitleView.findViewById(
+ com.android.internal.R.id.hard_keyboard_section).setVisibility(
+ mWindowManagerService.isHardKeyboardAvailable() ?
+ View.VISIBLE : View.GONE);
+ final Switch hardKeySwitch = ((Switch)mSwitchingDialogTitleView.findViewById(
+ com.android.internal.R.id.hard_keyboard_switch));
+ hardKeySwitch.setChecked(mWindowManagerService.isHardKeyboardEnabled());
+ hardKeySwitch.setOnCheckedChangeListener(
+ new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(
+ CompoundButton buttonView, boolean isChecked) {
+ mWindowManagerService.setHardKeyboardEnabled(isChecked);
+ // Ensure that the input method dialog is dismissed when changing
+ // the hardware keyboard state.
+ hideInputMethodMenu();
+ }
+ });
+
+ final ImeSubtypeListAdapter adapter = new ImeSubtypeListAdapter(context,
+ com.android.internal.R.layout.simple_list_item_2_single_choice, imList,
+ checkedItem);
+
+ mDialogBuilder.setSingleChoiceItems(adapter, checkedItem,
+ new AlertDialog.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ synchronized (mMethodMap) {
+ if (mIms == null || mIms.length <= which
+ || mSubtypeIds == null || mSubtypeIds.length <= which) {
+ return;
+ }
+ InputMethodInfo im = mIms[which];
+ int subtypeId = mSubtypeIds[which];
+ adapter.mCheckedItem = which;
+ adapter.notifyDataSetChanged();
+ hideInputMethodMenu();
+ if (im != null) {
+ if ((subtypeId < 0)
+ || (subtypeId >= im.getSubtypeCount())) {
+ subtypeId = NOT_A_SUBTYPE_ID;
+ }
+ setInputMethodLocked(im.getId(), subtypeId);
+ }
+ }
+ }
+ });
+
+ if (showSubtypes && !isScreenLocked) {
+ mDialogBuilder.setPositiveButton(
+ com.android.internal.R.string.configure_input_methods,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ showConfigureInputMethods();
+ }
+ });
+ }
+ mSwitchingDialog = mDialogBuilder.create();
+ mSwitchingDialog.setCanceledOnTouchOutside(true);
+ mSwitchingDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
+ mSwitchingDialog.getWindow().getAttributes().privateFlags |=
+ WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ mSwitchingDialog.getWindow().getAttributes().setTitle("Select input method");
+ mSwitchingDialog.show();
+ }
+ }
+
+ private static class ImeSubtypeListItem implements Comparable<ImeSubtypeListItem> {
+ public final CharSequence mImeName;
+ public final CharSequence mSubtypeName;
+ public final InputMethodInfo mImi;
+ public final int mSubtypeId;
+ private final boolean mIsSystemLocale;
+ private final boolean mIsSystemLanguage;
+
+ public ImeSubtypeListItem(CharSequence imeName, CharSequence subtypeName,
+ InputMethodInfo imi, int subtypeId, String subtypeLocale, String systemLocale) {
+ mImeName = imeName;
+ mSubtypeName = subtypeName;
+ mImi = imi;
+ mSubtypeId = subtypeId;
+ if (TextUtils.isEmpty(subtypeLocale)) {
+ mIsSystemLocale = false;
+ mIsSystemLanguage = false;
+ } else {
+ mIsSystemLocale = subtypeLocale.equals(systemLocale);
+ mIsSystemLanguage = mIsSystemLocale
+ || subtypeLocale.startsWith(systemLocale.substring(0, 2));
+ }
+ }
+
+ @Override
+ public int compareTo(ImeSubtypeListItem other) {
+ if (TextUtils.isEmpty(mImeName)) {
+ return 1;
+ }
+ if (TextUtils.isEmpty(other.mImeName)) {
+ return -1;
+ }
+ if (!TextUtils.equals(mImeName, other.mImeName)) {
+ return mImeName.toString().compareTo(other.mImeName.toString());
+ }
+ if (TextUtils.equals(mSubtypeName, other.mSubtypeName)) {
+ return 0;
+ }
+ if (mIsSystemLocale) {
+ return -1;
+ }
+ if (other.mIsSystemLocale) {
+ return 1;
+ }
+ if (mIsSystemLanguage) {
+ return -1;
+ }
+ if (other.mIsSystemLanguage) {
+ return 1;
+ }
+ if (TextUtils.isEmpty(mSubtypeName)) {
+ return 1;
+ }
+ if (TextUtils.isEmpty(other.mSubtypeName)) {
+ return -1;
+ }
+ return mSubtypeName.toString().compareTo(other.mSubtypeName.toString());
+ }
+ }
+
+ private static class ImeSubtypeListAdapter extends ArrayAdapter<ImeSubtypeListItem> {
+ private final LayoutInflater mInflater;
+ private final int mTextViewResourceId;
+ private final List<ImeSubtypeListItem> mItemsList;
+ public int mCheckedItem;
+ public ImeSubtypeListAdapter(Context context, int textViewResourceId,
+ List<ImeSubtypeListItem> itemsList, int checkedItem) {
+ super(context, textViewResourceId, itemsList);
+ mTextViewResourceId = textViewResourceId;
+ mItemsList = itemsList;
+ mCheckedItem = checkedItem;
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view = convertView != null ? convertView
+ : mInflater.inflate(mTextViewResourceId, null);
+ if (position < 0 || position >= mItemsList.size()) return view;
+ final ImeSubtypeListItem item = mItemsList.get(position);
+ final CharSequence imeName = item.mImeName;
+ final CharSequence subtypeName = item.mSubtypeName;
+ final TextView firstTextView = (TextView)view.findViewById(android.R.id.text1);
+ final TextView secondTextView = (TextView)view.findViewById(android.R.id.text2);
+ if (TextUtils.isEmpty(subtypeName)) {
+ firstTextView.setText(imeName);
+ secondTextView.setVisibility(View.GONE);
+ } else {
+ firstTextView.setText(subtypeName);
+ secondTextView.setText(imeName);
+ secondTextView.setVisibility(View.VISIBLE);
+ }
+ final RadioButton radioButton =
+ (RadioButton)view.findViewById(com.android.internal.R.id.radio);
+ radioButton.setChecked(position == mCheckedItem);
+ return view;
+ }
+ }
+
+ void hideInputMethodMenu() {
+ synchronized (mMethodMap) {
+ hideInputMethodMenuLocked();
+ }
+ }
+
+ void hideInputMethodMenuLocked() {
+ if (DEBUG) Slog.v(TAG, "Hide switching menu");
+
+ if (mSwitchingDialog != null) {
+ mSwitchingDialog.dismiss();
+ mSwitchingDialog = null;
+ }
+
+ mDialogBuilder = null;
+ mIms = null;
+ }
+
+ // ----------------------------------------------------------------------
+
+ @Override
+ public boolean setInputMethodEnabled(String id, boolean enabled) {
+ // TODO: Make this work even for non-current users?
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ synchronized (mMethodMap) {
+ if (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Requires permission "
+ + android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ }
+
+ long ident = Binder.clearCallingIdentity();
+ try {
+ return setInputMethodEnabledLocked(id, enabled);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
+ boolean setInputMethodEnabledLocked(String id, boolean enabled) {
+ // Make sure this is a valid input method.
+ InputMethodInfo imm = mMethodMap.get(id);
+ if (imm == null) {
+ throw new IllegalArgumentException("Unknown id: " + mCurMethodId);
+ }
+
+ List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings
+ .getEnabledInputMethodsAndSubtypeListLocked();
+
+ if (enabled) {
+ for (Pair<String, ArrayList<String>> pair: enabledInputMethodsList) {
+ if (pair.first.equals(id)) {
+ // We are enabling this input method, but it is already enabled.
+ // Nothing to do. The previous state was enabled.
+ return true;
+ }
+ }
+ mSettings.appendAndPutEnabledInputMethodLocked(id, false);
+ // Previous state was disabled.
+ return false;
+ } else {
+ StringBuilder builder = new StringBuilder();
+ if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ builder, enabledInputMethodsList, id)) {
+ // Disabled input method is currently selected, switch to another one.
+ final String selId = mSettings.getSelectedInputMethod();
+ if (id.equals(selId) && !chooseNewDefaultIMELocked()) {
+ Slog.i(TAG, "Can't find new IME, unsetting the current input method.");
+ resetSelectedInputMethodAndSubtypeLocked("");
+ }
+ // Previous state was enabled.
+ return true;
+ } else {
+ // We are disabling the input method but it is already disabled.
+ // Nothing to do. The previous state was disabled.
+ return false;
+ }
+ }
+ }
+
+ private void setSelectedInputMethodAndSubtypeLocked(InputMethodInfo imi, int subtypeId,
+ boolean setSubtypeOnly) {
+ // Update the history of InputMethod and Subtype
+ mSettings.saveCurrentInputMethodAndSubtypeToHistory(mCurMethodId, mCurrentSubtype);
+
+ // Set Subtype here
+ if (imi == null || subtypeId < 0) {
+ mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
+ mCurrentSubtype = null;
+ } else {
+ if (subtypeId < imi.getSubtypeCount()) {
+ InputMethodSubtype subtype = imi.getSubtypeAt(subtypeId);
+ mSettings.putSelectedSubtype(subtype.hashCode());
+ mCurrentSubtype = subtype;
+ } else {
+ mSettings.putSelectedSubtype(NOT_A_SUBTYPE_ID);
+ // If the subtype is not specified, choose the most applicable one
+ mCurrentSubtype = getCurrentInputMethodSubtypeLocked();
+ }
+ }
+
+ // Workaround.
+ // ASEC is not ready in the IMMS constructor. Accordingly, forward-locked
+ // IMEs are not recognized and considered uninstalled.
+ // Actually, we can't move everything after SystemReady because
+ // IMMS needs to run in the encryption lock screen. So, we just skip changing
+ // the default IME here and try cheking the default IME again in systemReady().
+ // TODO: Do nothing before system ready and implement a separated logic for
+ // the encryption lock screen.
+ // TODO: ASEC should be ready before IMMS is instantiated.
+ if (mSystemReady && !setSubtypeOnly) {
+ // Set InputMethod here
+ mSettings.putSelectedInputMethod(imi != null ? imi.getId() : "");
+ }
+ }
+
+ private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) {
+ InputMethodInfo imi = mMethodMap.get(newDefaultIme);
+ int lastSubtypeId = NOT_A_SUBTYPE_ID;
+ // newDefaultIme is empty when there is no candidate for the selected IME.
+ if (imi != null && !TextUtils.isEmpty(newDefaultIme)) {
+ String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme);
+ if (subtypeHashCode != null) {
+ try {
+ lastSubtypeId = InputMethodUtils.getSubtypeIdFromHashCode(
+ imi, Integer.valueOf(subtypeHashCode));
+ } catch (NumberFormatException e) {
+ Slog.w(TAG, "HashCode for subtype looks broken: " + subtypeHashCode, e);
+ }
+ }
+ }
+ setSelectedInputMethodAndSubtypeLocked(imi, lastSubtypeId, false);
+ }
+
+ // If there are no selected shortcuts, tries finding the most applicable ones.
+ private Pair<InputMethodInfo, InputMethodSubtype>
+ findLastResortApplicableShortcutInputMethodAndSubtypeLocked(String mode) {
+ List<InputMethodInfo> imis = mSettings.getEnabledInputMethodListLocked();
+ InputMethodInfo mostApplicableIMI = null;
+ InputMethodSubtype mostApplicableSubtype = null;
+ boolean foundInSystemIME = false;
+
+ // Search applicable subtype for each InputMethodInfo
+ for (InputMethodInfo imi: imis) {
+ final String imiId = imi.getId();
+ if (foundInSystemIME && !imiId.equals(mCurMethodId)) {
+ continue;
+ }
+ InputMethodSubtype subtype = null;
+ final List<InputMethodSubtype> enabledSubtypes =
+ mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true);
+ // 1. Search by the current subtype's locale from enabledSubtypes.
+ if (mCurrentSubtype != null) {
+ subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked(
+ mRes, enabledSubtypes, mode, mCurrentSubtype.getLocale(), false);
+ }
+ // 2. Search by the system locale from enabledSubtypes.
+ // 3. Search the first enabled subtype matched with mode from enabledSubtypes.
+ if (subtype == null) {
+ subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked(
+ mRes, enabledSubtypes, mode, null, true);
+ }
+ final ArrayList<InputMethodSubtype> overridingImplicitlyEnabledSubtypes =
+ InputMethodUtils.getOverridingImplicitlyEnabledSubtypes(imi, mode);
+ final ArrayList<InputMethodSubtype> subtypesForSearch =
+ overridingImplicitlyEnabledSubtypes.isEmpty()
+ ? InputMethodUtils.getSubtypes(imi)
+ : overridingImplicitlyEnabledSubtypes;
+ // 4. Search by the current subtype's locale from all subtypes.
+ if (subtype == null && mCurrentSubtype != null) {
+ subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked(
+ mRes, subtypesForSearch, mode, mCurrentSubtype.getLocale(), false);
+ }
+ // 5. Search by the system locale from all subtypes.
+ // 6. Search the first enabled subtype matched with mode from all subtypes.
+ if (subtype == null) {
+ subtype = InputMethodUtils.findLastResortApplicableSubtypeLocked(
+ mRes, subtypesForSearch, mode, null, true);
+ }
+ if (subtype != null) {
+ if (imiId.equals(mCurMethodId)) {
+ // The current input method is the most applicable IME.
+ mostApplicableIMI = imi;
+ mostApplicableSubtype = subtype;
+ break;
+ } else if (!foundInSystemIME) {
+ // The system input method is 2nd applicable IME.
+ mostApplicableIMI = imi;
+ mostApplicableSubtype = subtype;
+ if ((imi.getServiceInfo().applicationInfo.flags
+ & ApplicationInfo.FLAG_SYSTEM) != 0) {
+ foundInSystemIME = true;
+ }
+ }
+ }
+ }
+ if (DEBUG) {
+ if (mostApplicableIMI != null) {
+ Slog.w(TAG, "Most applicable shortcut input method was:"
+ + mostApplicableIMI.getId());
+ if (mostApplicableSubtype != null) {
+ Slog.w(TAG, "Most applicable shortcut input method subtype was:"
+ + "," + mostApplicableSubtype.getMode() + ","
+ + mostApplicableSubtype.getLocale());
+ }
+ }
+ }
+ if (mostApplicableIMI != null) {
+ return new Pair<InputMethodInfo, InputMethodSubtype> (mostApplicableIMI,
+ mostApplicableSubtype);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @return Return the current subtype of this input method.
+ */
+ @Override
+ public InputMethodSubtype getCurrentInputMethodSubtype() {
+ // TODO: Make this work even for non-current users?
+ if (!calledFromValidUser()) {
+ return null;
+ }
+ synchronized (mMethodMap) {
+ return getCurrentInputMethodSubtypeLocked();
+ }
+ }
+
+ private InputMethodSubtype getCurrentInputMethodSubtypeLocked() {
+ if (mCurMethodId == null) {
+ return null;
+ }
+ final boolean subtypeIsSelected = mSettings.isSubtypeSelected();
+ final InputMethodInfo imi = mMethodMap.get(mCurMethodId);
+ if (imi == null || imi.getSubtypeCount() == 0) {
+ return null;
+ }
+ if (!subtypeIsSelected || mCurrentSubtype == null
+ || !InputMethodUtils.isValidSubtypeId(imi, mCurrentSubtype.hashCode())) {
+ int subtypeId = mSettings.getSelectedInputMethodSubtypeId(mCurMethodId);
+ if (subtypeId == NOT_A_SUBTYPE_ID) {
+ // If there are no selected subtypes, the framework will try to find
+ // the most applicable subtype from explicitly or implicitly enabled
+ // subtypes.
+ List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
+ mSettings.getEnabledInputMethodSubtypeListLocked(mContext, imi, true);
+ // If there is only one explicitly or implicitly enabled subtype,
+ // just returns it.
+ if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
+ mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0);
+ } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) {
+ mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked(
+ mRes, explicitlyOrImplicitlyEnabledSubtypes,
+ InputMethodUtils.SUBTYPE_MODE_KEYBOARD, null, true);
+ if (mCurrentSubtype == null) {
+ mCurrentSubtype = InputMethodUtils.findLastResortApplicableSubtypeLocked(
+ mRes, explicitlyOrImplicitlyEnabledSubtypes, null, null,
+ true);
+ }
+ }
+ } else {
+ mCurrentSubtype = InputMethodUtils.getSubtypes(imi).get(subtypeId);
+ }
+ }
+ return mCurrentSubtype;
+ }
+
+ private void addShortcutInputMethodAndSubtypes(InputMethodInfo imi,
+ InputMethodSubtype subtype) {
+ if (mShortcutInputMethodsAndSubtypes.containsKey(imi)) {
+ mShortcutInputMethodsAndSubtypes.get(imi).add(subtype);
+ } else {
+ ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+ subtypes.add(subtype);
+ mShortcutInputMethodsAndSubtypes.put(imi, subtypes);
+ }
+ }
+
+ // TODO: We should change the return type from List to List<Parcelable>
+ @SuppressWarnings("rawtypes")
+ @Override
+ public List getShortcutInputMethodsAndSubtypes() {
+ synchronized (mMethodMap) {
+ ArrayList<Object> ret = new ArrayList<Object>();
+ if (mShortcutInputMethodsAndSubtypes.size() == 0) {
+ // If there are no selected shortcut subtypes, the framework will try to find
+ // the most applicable subtype from all subtypes whose mode is
+ // SUBTYPE_MODE_VOICE. This is an exceptional case, so we will hardcode the mode.
+ Pair<InputMethodInfo, InputMethodSubtype> info =
+ findLastResortApplicableShortcutInputMethodAndSubtypeLocked(
+ InputMethodUtils.SUBTYPE_MODE_VOICE);
+ if (info != null) {
+ ret.add(info.first);
+ ret.add(info.second);
+ }
+ return ret;
+ }
+ for (InputMethodInfo imi: mShortcutInputMethodsAndSubtypes.keySet()) {
+ ret.add(imi);
+ for (InputMethodSubtype subtype: mShortcutInputMethodsAndSubtypes.get(imi)) {
+ ret.add(subtype);
+ }
+ }
+ return ret;
+ }
+ }
+
+ @Override
+ public boolean setCurrentInputMethodSubtype(InputMethodSubtype subtype) {
+ // TODO: Make this work even for non-current users?
+ if (!calledFromValidUser()) {
+ return false;
+ }
+ synchronized (mMethodMap) {
+ if (subtype != null && mCurMethodId != null) {
+ InputMethodInfo imi = mMethodMap.get(mCurMethodId);
+ int subtypeId = InputMethodUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode());
+ if (subtypeId != NOT_A_SUBTYPE_ID) {
+ setInputMethodLocked(mCurMethodId, subtypeId);
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private static class InputMethodAndSubtypeListManager {
+ private final Context mContext;
+ // Used to load label
+ private final PackageManager mPm;
+ private final InputMethodManagerService mImms;
+ private final String mSystemLocaleStr;
+ public InputMethodAndSubtypeListManager(Context context, InputMethodManagerService imms) {
+ mContext = context;
+ mPm = context.getPackageManager();
+ mImms = imms;
+ final Locale locale = context.getResources().getConfiguration().locale;
+ mSystemLocaleStr = locale != null ? locale.toString() : "";
+ }
+
+ private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> mSortedImmis =
+ new TreeMap<InputMethodInfo, List<InputMethodSubtype>>(
+ new Comparator<InputMethodInfo>() {
+ @Override
+ public int compare(InputMethodInfo imi1, InputMethodInfo imi2) {
+ if (imi2 == null) return 0;
+ if (imi1 == null) return 1;
+ if (mPm == null) {
+ return imi1.getId().compareTo(imi2.getId());
+ }
+ CharSequence imiId1 = imi1.loadLabel(mPm) + "/" + imi1.getId();
+ CharSequence imiId2 = imi2.loadLabel(mPm) + "/" + imi2.getId();
+ return imiId1.toString().compareTo(imiId2.toString());
+ }
+ });
+
+ public ImeSubtypeListItem getNextInputMethod(
+ boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) {
+ if (imi == null) {
+ return null;
+ }
+ final List<ImeSubtypeListItem> imList = getSortedInputMethodAndSubtypeList();
+ if (imList.size() <= 1) {
+ return null;
+ }
+ final int N = imList.size();
+ final int currentSubtypeId = subtype != null
+ ? InputMethodUtils.getSubtypeIdFromHashCode(imi, subtype.hashCode())
+ : NOT_A_SUBTYPE_ID;
+ for (int i = 0; i < N; ++i) {
+ final ImeSubtypeListItem isli = imList.get(i);
+ if (isli.mImi.equals(imi) && isli.mSubtypeId == currentSubtypeId) {
+ if (!onlyCurrentIme) {
+ return imList.get((i + 1) % N);
+ }
+ for (int j = 0; j < N - 1; ++j) {
+ final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N);
+ if (candidate.mImi.equals(imi)) {
+ return candidate;
+ }
+ }
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList() {
+ return getSortedInputMethodAndSubtypeList(true, false, false);
+ }
+
+ public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes,
+ boolean inputShown, boolean isScreenLocked) {
+ final ArrayList<ImeSubtypeListItem> imList = new ArrayList<ImeSubtypeListItem>();
+ final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis =
+ mImms.getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked();
+ if (immis == null || immis.size() == 0) {
+ return Collections.emptyList();
+ }
+ mSortedImmis.clear();
+ mSortedImmis.putAll(immis);
+ for (InputMethodInfo imi : mSortedImmis.keySet()) {
+ if (imi == null) continue;
+ List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = immis.get(imi);
+ HashSet<String> enabledSubtypeSet = new HashSet<String>();
+ for (InputMethodSubtype subtype: explicitlyOrImplicitlyEnabledSubtypeList) {
+ enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
+ }
+ final CharSequence imeLabel = imi.loadLabel(mPm);
+ if (showSubtypes && enabledSubtypeSet.size() > 0) {
+ final int subtypeCount = imi.getSubtypeCount();
+ if (DEBUG) {
+ Slog.v(TAG, "Add subtypes: " + subtypeCount + ", " + imi.getId());
+ }
+ for (int j = 0; j < subtypeCount; ++j) {
+ final InputMethodSubtype subtype = imi.getSubtypeAt(j);
+ final String subtypeHashCode = String.valueOf(subtype.hashCode());
+ // We show all enabled IMEs and subtypes when an IME is shown.
+ if (enabledSubtypeSet.contains(subtypeHashCode)
+ && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) {
+ final CharSequence subtypeLabel =
+ subtype.overridesImplicitlyEnabledSubtype() ? null
+ : subtype.getDisplayName(mContext, imi.getPackageName(),
+ imi.getServiceInfo().applicationInfo);
+ imList.add(new ImeSubtypeListItem(imeLabel, subtypeLabel, imi, j,
+ subtype.getLocale(), mSystemLocaleStr));
+
+ // Removing this subtype from enabledSubtypeSet because we no longer
+ // need to add an entry of this subtype to imList to avoid duplicated
+ // entries.
+ enabledSubtypeSet.remove(subtypeHashCode);
+ }
+ }
+ } else {
+ imList.add(new ImeSubtypeListItem(imeLabel, null, imi, NOT_A_SUBTYPE_ID,
+ null, mSystemLocaleStr));
+ }
+ }
+ Collections.sort(imList);
+ return imList;
+ }
+ }
+
+ // TODO: Cache the state for each user and reset when the cached user is removed.
+ private static class InputMethodFileManager {
+ private static final String SYSTEM_PATH = "system";
+ private static final String INPUT_METHOD_PATH = "inputmethod";
+ private static final String ADDITIONAL_SUBTYPES_FILE_NAME = "subtypes.xml";
+ private static final String NODE_SUBTYPES = "subtypes";
+ private static final String NODE_SUBTYPE = "subtype";
+ private static final String NODE_IMI = "imi";
+ private static final String ATTR_ID = "id";
+ private static final String ATTR_LABEL = "label";
+ private static final String ATTR_ICON = "icon";
+ private static final String ATTR_IME_SUBTYPE_LOCALE = "imeSubtypeLocale";
+ private static final String ATTR_IME_SUBTYPE_MODE = "imeSubtypeMode";
+ private static final String ATTR_IME_SUBTYPE_EXTRA_VALUE = "imeSubtypeExtraValue";
+ private static final String ATTR_IS_AUXILIARY = "isAuxiliary";
+ private final AtomicFile mAdditionalInputMethodSubtypeFile;
+ private final HashMap<String, InputMethodInfo> mMethodMap;
+ private final HashMap<String, List<InputMethodSubtype>> mAdditionalSubtypesMap =
+ new HashMap<String, List<InputMethodSubtype>>();
+ public InputMethodFileManager(HashMap<String, InputMethodInfo> methodMap, int userId) {
+ if (methodMap == null) {
+ throw new NullPointerException("methodMap is null");
+ }
+ mMethodMap = methodMap;
+ final File systemDir = userId == UserHandle.USER_OWNER
+ ? new File(Environment.getDataDirectory(), SYSTEM_PATH)
+ : Environment.getUserSystemDirectory(userId);
+ final File inputMethodDir = new File(systemDir, INPUT_METHOD_PATH);
+ if (!inputMethodDir.mkdirs()) {
+ Slog.w(TAG, "Couldn't create dir.: " + inputMethodDir.getAbsolutePath());
+ }
+ final File subtypeFile = new File(inputMethodDir, ADDITIONAL_SUBTYPES_FILE_NAME);
+ mAdditionalInputMethodSubtypeFile = new AtomicFile(subtypeFile);
+ if (!subtypeFile.exists()) {
+ // If "subtypes.xml" doesn't exist, create a blank file.
+ writeAdditionalInputMethodSubtypes(
+ mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, methodMap);
+ } else {
+ readAdditionalInputMethodSubtypes(
+ mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile);
+ }
+ }
+
+ private void deleteAllInputMethodSubtypes(String imiId) {
+ synchronized (mMethodMap) {
+ mAdditionalSubtypesMap.remove(imiId);
+ writeAdditionalInputMethodSubtypes(
+ mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, mMethodMap);
+ }
+ }
+
+ public void addInputMethodSubtypes(
+ InputMethodInfo imi, InputMethodSubtype[] additionalSubtypes) {
+ synchronized (mMethodMap) {
+ final ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
+ final int N = additionalSubtypes.length;
+ for (int i = 0; i < N; ++i) {
+ final InputMethodSubtype subtype = additionalSubtypes[i];
+ if (!subtypes.contains(subtype)) {
+ subtypes.add(subtype);
+ } else {
+ Slog.w(TAG, "Duplicated subtype definition found: "
+ + subtype.getLocale() + ", " + subtype.getMode());
+ }
+ }
+ mAdditionalSubtypesMap.put(imi.getId(), subtypes);
+ writeAdditionalInputMethodSubtypes(
+ mAdditionalSubtypesMap, mAdditionalInputMethodSubtypeFile, mMethodMap);
+ }
+ }
+
+ public HashMap<String, List<InputMethodSubtype>> getAllAdditionalInputMethodSubtypes() {
+ synchronized (mMethodMap) {
+ return mAdditionalSubtypesMap;
+ }
+ }
+
+ private static void writeAdditionalInputMethodSubtypes(
+ HashMap<String, List<InputMethodSubtype>> allSubtypes, AtomicFile subtypesFile,
+ HashMap<String, InputMethodInfo> methodMap) {
+ // Safety net for the case that this function is called before methodMap is set.
+ final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
+ FileOutputStream fos = null;
+ try {
+ fos = subtypesFile.startWrite();
+ final XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, "utf-8");
+ out.startDocument(null, true);
+ out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ out.startTag(null, NODE_SUBTYPES);
+ for (String imiId : allSubtypes.keySet()) {
+ if (isSetMethodMap && !methodMap.containsKey(imiId)) {
+ Slog.w(TAG, "IME uninstalled or not valid.: " + imiId);
+ continue;
+ }
+ out.startTag(null, NODE_IMI);
+ out.attribute(null, ATTR_ID, imiId);
+ final List<InputMethodSubtype> subtypesList = allSubtypes.get(imiId);
+ final int N = subtypesList.size();
+ for (int i = 0; i < N; ++i) {
+ final InputMethodSubtype subtype = subtypesList.get(i);
+ out.startTag(null, NODE_SUBTYPE);
+ out.attribute(null, ATTR_ICON, String.valueOf(subtype.getIconResId()));
+ out.attribute(null, ATTR_LABEL, String.valueOf(subtype.getNameResId()));
+ out.attribute(null, ATTR_IME_SUBTYPE_LOCALE, subtype.getLocale());
+ out.attribute(null, ATTR_IME_SUBTYPE_MODE, subtype.getMode());
+ out.attribute(null, ATTR_IME_SUBTYPE_EXTRA_VALUE, subtype.getExtraValue());
+ out.attribute(null, ATTR_IS_AUXILIARY,
+ String.valueOf(subtype.isAuxiliary() ? 1 : 0));
+ out.endTag(null, NODE_SUBTYPE);
+ }
+ out.endTag(null, NODE_IMI);
+ }
+ out.endTag(null, NODE_SUBTYPES);
+ out.endDocument();
+ subtypesFile.finishWrite(fos);
+ } catch (java.io.IOException e) {
+ Slog.w(TAG, "Error writing subtypes", e);
+ if (fos != null) {
+ subtypesFile.failWrite(fos);
+ }
+ }
+ }
+
+ private static void readAdditionalInputMethodSubtypes(
+ HashMap<String, List<InputMethodSubtype>> allSubtypes, AtomicFile subtypesFile) {
+ if (allSubtypes == null || subtypesFile == null) return;
+ allSubtypes.clear();
+ FileInputStream fis = null;
+ try {
+ fis = subtypesFile.openRead();
+ final XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, null);
+ int type = parser.getEventType();
+ // Skip parsing until START_TAG
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {}
+ String firstNodeName = parser.getName();
+ if (!NODE_SUBTYPES.equals(firstNodeName)) {
+ throw new XmlPullParserException("Xml doesn't start with subtypes");
+ }
+ final int depth =parser.getDepth();
+ String currentImiId = null;
+ ArrayList<InputMethodSubtype> tempSubtypesArray = null;
+ while (((type = parser.next()) != XmlPullParser.END_TAG
+ || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
+ if (type != XmlPullParser.START_TAG)
+ continue;
+ final String nodeName = parser.getName();
+ if (NODE_IMI.equals(nodeName)) {
+ currentImiId = parser.getAttributeValue(null, ATTR_ID);
+ if (TextUtils.isEmpty(currentImiId)) {
+ Slog.w(TAG, "Invalid imi id found in subtypes.xml");
+ continue;
+ }
+ tempSubtypesArray = new ArrayList<InputMethodSubtype>();
+ allSubtypes.put(currentImiId, tempSubtypesArray);
+ } else if (NODE_SUBTYPE.equals(nodeName)) {
+ if (TextUtils.isEmpty(currentImiId) || tempSubtypesArray == null) {
+ Slog.w(TAG, "IME uninstalled or not valid.: " + currentImiId);
+ continue;
+ }
+ final int icon = Integer.valueOf(
+ parser.getAttributeValue(null, ATTR_ICON));
+ final int label = Integer.valueOf(
+ parser.getAttributeValue(null, ATTR_LABEL));
+ final String imeSubtypeLocale =
+ parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LOCALE);
+ final String imeSubtypeMode =
+ parser.getAttributeValue(null, ATTR_IME_SUBTYPE_MODE);
+ final String imeSubtypeExtraValue =
+ parser.getAttributeValue(null, ATTR_IME_SUBTYPE_EXTRA_VALUE);
+ final boolean isAuxiliary = "1".equals(String.valueOf(
+ parser.getAttributeValue(null, ATTR_IS_AUXILIARY)));
+ final InputMethodSubtype subtype =
+ new InputMethodSubtype(label, icon, imeSubtypeLocale,
+ imeSubtypeMode, imeSubtypeExtraValue, isAuxiliary);
+ tempSubtypesArray.add(subtype);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ Slog.w(TAG, "Error reading subtypes: " + e);
+ return;
+ } catch (java.io.IOException e) {
+ Slog.w(TAG, "Error reading subtypes: " + e);
+ return;
+ } catch (NumberFormatException e) {
+ Slog.w(TAG, "Error reading subtypes: " + e);
+ return;
+ } finally {
+ if (fis != null) {
+ try {
+ fis.close();
+ } catch (java.io.IOException e1) {
+ Slog.w(TAG, "Failed to close.");
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PackageManager.PERMISSION_GRANTED) {
+
+ pw.println("Permission Denial: can't dump InputMethodManager from from pid="
+ + Binder.getCallingPid()
+ + ", uid=" + Binder.getCallingUid());
+ return;
+ }
+
+ IInputMethod method;
+ ClientState client;
+
+ final Printer p = new PrintWriterPrinter(pw);
+
+ synchronized (mMethodMap) {
+ p.println("Current Input Method Manager state:");
+ int N = mMethodList.size();
+ p.println(" Input Methods:");
+ for (int i=0; i<N; i++) {
+ InputMethodInfo info = mMethodList.get(i);
+ p.println(" InputMethod #" + i + ":");
+ info.dump(p, " ");
+ }
+ p.println(" Clients:");
+ for (ClientState ci : mClients.values()) {
+ p.println(" Client " + ci + ":");
+ p.println(" client=" + ci.client);
+ p.println(" inputContext=" + ci.inputContext);
+ p.println(" sessionRequested=" + ci.sessionRequested);
+ p.println(" curSession=" + ci.curSession);
+ }
+ p.println(" mCurMethodId=" + mCurMethodId);
+ client = mCurClient;
+ p.println(" mCurClient=" + client + " mCurSeq=" + mCurSeq);
+ p.println(" mCurFocusedWindow=" + mCurFocusedWindow);
+ p.println(" mCurId=" + mCurId + " mHaveConnect=" + mHaveConnection
+ + " mBoundToMethod=" + mBoundToMethod);
+ p.println(" mCurToken=" + mCurToken);
+ p.println(" mCurIntent=" + mCurIntent);
+ method = mCurMethod;
+ p.println(" mCurMethod=" + mCurMethod);
+ p.println(" mEnabledSession=" + mEnabledSession);
+ p.println(" mShowRequested=" + mShowRequested
+ + " mShowExplicitlyRequested=" + mShowExplicitlyRequested
+ + " mShowForced=" + mShowForced
+ + " mInputShown=" + mInputShown);
+ p.println(" mSystemReady=" + mSystemReady + " mScreenOn=" + mScreenOn);
+ }
+
+ p.println(" ");
+ if (client != null) {
+ pw.flush();
+ try {
+ client.client.asBinder().dump(fd, args);
+ } catch (RemoteException e) {
+ p.println("Input method client dead: " + e);
+ }
+ } else {
+ p.println("No input method client.");
+ }
+
+ p.println(" ");
+ if (method != null) {
+ pw.flush();
+ try {
+ method.asBinder().dump(fd, args);
+ } catch (RemoteException e) {
+ p.println("Input method service dead: " + e);
+ }
+ } else {
+ p.println("No input method service.");
+ }
+ }
+}