1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.statusbar.tablet; 18 19 import com.android.systemui.R; 20 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.pm.PackageManager; 26 import android.graphics.drawable.Drawable; 27 import android.os.IBinder; 28 import android.provider.Settings; 29 import android.text.TextUtils; 30 import android.util.AttributeSet; 31 import android.util.Log; 32 import android.util.Pair; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.inputmethod.InputMethodInfo; 36 import android.view.inputmethod.InputMethodManager; 37 import android.view.inputmethod.InputMethodSubtype; 38 import android.widget.ImageView; 39 import android.widget.LinearLayout; 40 import android.widget.RadioButton; 41 import android.widget.Switch; 42 import android.widget.TextView; 43 44 import java.util.Comparator; 45 import java.util.HashMap; 46 import java.util.List; 47 import java.util.Map; 48 import java.util.Set; 49 import java.util.TreeMap; 50 51 public class InputMethodsPanel extends LinearLayout implements StatusBarPanel, 52 View.OnClickListener { 53 private static final boolean DEBUG = TabletStatusBar.DEBUG; 54 private static final String TAG = "InputMethodsPanel"; 55 56 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 57 @Override 58 public void onReceive(Context context, Intent intent) { 59 onPackageChanged(); 60 } 61 }; 62 63 private final InputMethodManager mImm; 64 private final IntentFilter mIntentFilter = new IntentFilter(); 65 private final HashMap<View, Pair<InputMethodInfo, InputMethodSubtype>> mRadioViewAndImiMap = 66 new HashMap<View, Pair<InputMethodInfo, InputMethodSubtype>>(); 67 private final TreeMap<InputMethodInfo, List<InputMethodSubtype>> 68 mEnabledInputMethodAndSubtypesCache = 69 new TreeMap<InputMethodInfo, List<InputMethodSubtype>>( 70 new InputMethodComparator()); 71 72 private boolean mAttached = false; 73 private boolean mPackageChanged = false; 74 private Context mContext; 75 private IBinder mToken; 76 private InputMethodButton mInputMethodSwitchButton; 77 private LinearLayout mInputMethodMenuList; 78 private boolean mHardKeyboardAvailable; 79 private boolean mHardKeyboardEnabled; 80 private OnHardKeyboardEnabledChangeListener mHardKeyboardEnabledChangeListener; 81 private LinearLayout mHardKeyboardSection; 82 private Switch mHardKeyboardSwitch; 83 private PackageManager mPackageManager; 84 private String mEnabledInputMethodAndSubtypesCacheStr; 85 private String mLastSystemLocaleString; 86 private View mConfigureImeShortcut; 87 88 private class InputMethodComparator implements Comparator<InputMethodInfo> { 89 @Override 90 public int compare(InputMethodInfo imi1, InputMethodInfo imi2) { 91 if (imi2 == null) return 0; 92 if (imi1 == null) return 1; 93 if (mPackageManager == null) { 94 return imi1.getId().compareTo(imi2.getId()); 95 } 96 CharSequence imiId1 = imi1.loadLabel(mPackageManager) + "/" + imi1.getId(); 97 CharSequence imiId2 = imi2.loadLabel(mPackageManager) + "/" + imi2.getId(); 98 return imiId1.toString().compareTo(imiId2.toString()); 99 } 100 } 101 102 public InputMethodsPanel(Context context, AttributeSet attrs) { 103 this(context, attrs, 0); 104 } 105 106 public InputMethodsPanel(Context context, AttributeSet attrs, int defStyle) { 107 super(context, attrs, defStyle); 108 mContext = context; 109 mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 110 mIntentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); 111 mIntentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 112 mIntentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 113 mIntentFilter.addDataScheme("package"); 114 } 115 116 public void setHardKeyboardEnabledChangeListener( 117 OnHardKeyboardEnabledChangeListener listener) { 118 mHardKeyboardEnabledChangeListener = listener; 119 } 120 121 @Override 122 protected void onDetachedFromWindow() { 123 super.onDetachedFromWindow(); 124 if (mAttached) { 125 getContext().unregisterReceiver(mBroadcastReceiver); 126 mAttached = false; 127 } 128 } 129 130 @Override 131 protected void onAttachedToWindow() { 132 super.onAttachedToWindow(); 133 if (!mAttached) { 134 getContext().registerReceiver(mBroadcastReceiver, mIntentFilter); 135 mAttached = true; 136 } 137 } 138 139 @Override 140 public void onFinishInflate() { 141 mInputMethodMenuList = (LinearLayout) findViewById(R.id.input_method_menu_list); 142 mHardKeyboardSection = (LinearLayout) findViewById(R.id.hard_keyboard_section); 143 mHardKeyboardSwitch = (Switch) findViewById(R.id.hard_keyboard_switch); 144 mConfigureImeShortcut = findViewById(R.id.ime_settings_shortcut); 145 mConfigureImeShortcut.setOnClickListener(this); 146 // TODO: If configurations for IME are not changed, do not update 147 // by checking onConfigurationChanged. 148 updateUiElements(); 149 } 150 151 @Override 152 public boolean isInContentArea(int x, int y) { 153 return false; 154 } 155 156 @Override 157 public void onClick(View view) { 158 if (view == mConfigureImeShortcut) { 159 showConfigureInputMethods(); 160 closePanel(true); 161 } 162 } 163 164 @Override 165 public boolean dispatchHoverEvent(MotionEvent event) { 166 // Ignore hover events outside of this panel bounds since such events 167 // generate spurious accessibility events with the panel content when 168 // tapping outside of it, thus confusing the user. 169 final int x = (int) event.getX(); 170 final int y = (int) event.getY(); 171 if (x >= 0 && x < getWidth() && y >= 0 && y < getHeight()) { 172 return super.dispatchHoverEvent(event); 173 } 174 return true; 175 } 176 177 private void updateHardKeyboardEnabled() { 178 if (mHardKeyboardAvailable) { 179 final boolean checked = mHardKeyboardSwitch.isChecked(); 180 if (mHardKeyboardEnabled != checked) { 181 mHardKeyboardEnabled = checked; 182 if (mHardKeyboardEnabledChangeListener != null) 183 mHardKeyboardEnabledChangeListener.onHardKeyboardEnabledChange(checked); 184 } 185 } 186 } 187 188 public void openPanel() { 189 setVisibility(View.VISIBLE); 190 updateUiElements(); 191 if (mInputMethodSwitchButton != null) { 192 mInputMethodSwitchButton.setIconImage(R.drawable.ic_sysbar_ime_pressed); 193 } 194 } 195 196 public void closePanel(boolean closeKeyboard) { 197 setVisibility(View.GONE); 198 if (mInputMethodSwitchButton != null) { 199 mInputMethodSwitchButton.setIconImage(R.drawable.ic_sysbar_ime); 200 } 201 if (closeKeyboard) { 202 mImm.hideSoftInputFromWindow(getWindowToken(), 0); 203 } 204 } 205 206 private void startActivity(Intent intent) { 207 mContext.startActivity(intent); 208 } 209 210 private void showConfigureInputMethods() { 211 Intent intent = new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS); 212 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 213 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 214 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 215 startActivity(intent); 216 } 217 218 private View createInputMethodItem( 219 final InputMethodInfo imi, final InputMethodSubtype subtype) { 220 final CharSequence subtypeName; 221 if (subtype == null || subtype.overridesImplicitlyEnabledSubtype()) { 222 subtypeName = null; 223 } else { 224 subtypeName = getSubtypeName(imi, subtype); 225 } 226 final CharSequence imiName = getIMIName(imi); 227 final Drawable icon = getSubtypeIcon(imi, subtype); 228 final View view = View.inflate(mContext, R.layout.system_bar_input_methods_item, null); 229 final ImageView subtypeIcon = (ImageView)view.findViewById(R.id.item_icon); 230 final TextView itemTitle = (TextView)view.findViewById(R.id.item_title); 231 final TextView itemSubtitle = (TextView)view.findViewById(R.id.item_subtitle); 232 final ImageView settingsIcon = (ImageView)view.findViewById(R.id.item_settings_icon); 233 final View subtypeView = view.findViewById(R.id.item_subtype); 234 if (subtypeName == null) { 235 itemTitle.setText(imiName); 236 itemSubtitle.setVisibility(View.GONE); 237 } else { 238 itemTitle.setText(subtypeName); 239 itemSubtitle.setVisibility(View.VISIBLE); 240 itemSubtitle.setText(imiName); 241 } 242 subtypeIcon.setImageDrawable(icon); 243 subtypeIcon.setContentDescription(itemTitle.getText()); 244 final String settingsActivity = imi.getSettingsActivity(); 245 if (!TextUtils.isEmpty(settingsActivity)) { 246 settingsIcon.setOnClickListener(new View.OnClickListener() { 247 @Override 248 public void onClick(View arg0) { 249 Intent intent = new Intent(Intent.ACTION_MAIN); 250 intent.setClassName(imi.getPackageName(), settingsActivity); 251 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 252 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 253 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 254 startActivity(intent); 255 closePanel(true); 256 } 257 }); 258 } else { 259 // Do not show the settings icon if the IME does not have a settings preference 260 view.findViewById(R.id.item_vertical_separator).setVisibility(View.GONE); 261 settingsIcon.setVisibility(View.GONE); 262 } 263 mRadioViewAndImiMap.put( 264 subtypeView, new Pair<InputMethodInfo, InputMethodSubtype> (imi, subtype)); 265 subtypeView.setOnClickListener(new View.OnClickListener() { 266 @Override 267 public void onClick(View v) { 268 Pair<InputMethodInfo, InputMethodSubtype> imiAndSubtype = 269 updateRadioButtonsByView(v); 270 closePanel(false); 271 setInputMethodAndSubtype(imiAndSubtype.first, imiAndSubtype.second); 272 } 273 }); 274 return view; 275 } 276 277 private void updateUiElements() { 278 updateHardKeyboardSection(); 279 280 // TODO: Reuse subtype views. 281 mInputMethodMenuList.removeAllViews(); 282 mRadioViewAndImiMap.clear(); 283 mPackageManager = mContext.getPackageManager(); 284 285 Map<InputMethodInfo, List<InputMethodSubtype>> enabledIMIs = 286 getEnabledInputMethodAndSubtypeList(); 287 Set<InputMethodInfo> cachedImiSet = enabledIMIs.keySet(); 288 for (InputMethodInfo imi: cachedImiSet) { 289 List<InputMethodSubtype> subtypes = enabledIMIs.get(imi); 290 if (subtypes == null || subtypes.size() == 0) { 291 mInputMethodMenuList.addView( 292 createInputMethodItem(imi, null)); 293 continue; 294 } 295 for (InputMethodSubtype subtype: subtypes) { 296 mInputMethodMenuList.addView(createInputMethodItem(imi, subtype)); 297 } 298 } 299 updateRadioButtons(); 300 } 301 302 public void setImeToken(IBinder token) { 303 mToken = token; 304 } 305 306 public void setImeSwitchButton(InputMethodButton imb) { 307 mInputMethodSwitchButton = imb; 308 } 309 310 private void setInputMethodAndSubtype(InputMethodInfo imi, InputMethodSubtype subtype) { 311 if (mToken != null) { 312 mImm.setInputMethodAndSubtype(mToken, imi.getId(), subtype); 313 } else { 314 Log.w(TAG, "IME Token is not set yet."); 315 } 316 } 317 318 public void setHardKeyboardStatus(boolean available, boolean enabled) { 319 if (mHardKeyboardAvailable != available || mHardKeyboardEnabled != enabled) { 320 mHardKeyboardAvailable = available; 321 mHardKeyboardEnabled = enabled; 322 updateHardKeyboardSection(); 323 } 324 } 325 326 private void updateHardKeyboardSection() { 327 if (mHardKeyboardAvailable) { 328 mHardKeyboardSection.setVisibility(View.VISIBLE); 329 if (mHardKeyboardSwitch.isChecked() != mHardKeyboardEnabled) { 330 mHardKeyboardSwitch.setChecked(mHardKeyboardEnabled); 331 updateHardKeyboardEnabled(); 332 } 333 } else { 334 mHardKeyboardSection.setVisibility(View.GONE); 335 } 336 } 337 338 // Turn on the selected radio button when the user chooses the item 339 private Pair<InputMethodInfo, InputMethodSubtype> updateRadioButtonsByView(View selectedView) { 340 Pair<InputMethodInfo, InputMethodSubtype> selectedImiAndSubtype = null; 341 if (mRadioViewAndImiMap.containsKey(selectedView)) { 342 for (View radioView: mRadioViewAndImiMap.keySet()) { 343 RadioButton subtypeRadioButton = 344 (RadioButton) radioView.findViewById(R.id.item_radio); 345 if (subtypeRadioButton == null) { 346 Log.w(TAG, "RadioButton was not found in the selected subtype view"); 347 return null; 348 } 349 if (radioView == selectedView) { 350 Pair<InputMethodInfo, InputMethodSubtype> imiAndSubtype = 351 mRadioViewAndImiMap.get(radioView); 352 selectedImiAndSubtype = imiAndSubtype; 353 subtypeRadioButton.setChecked(true); 354 } else { 355 subtypeRadioButton.setChecked(false); 356 } 357 } 358 } 359 return selectedImiAndSubtype; 360 } 361 362 private void updateRadioButtons() { 363 updateRadioButtonsByImiAndSubtype( 364 getCurrentInputMethodInfo(), mImm.getCurrentInputMethodSubtype()); 365 } 366 367 // Turn on the selected radio button at startup 368 private void updateRadioButtonsByImiAndSubtype( 369 InputMethodInfo imi, InputMethodSubtype subtype) { 370 if (imi == null) return; 371 if (DEBUG) { 372 Log.d(TAG, "Update radio buttons by " + imi.getId() + ", " + subtype); 373 } 374 for (View radioView: mRadioViewAndImiMap.keySet()) { 375 RadioButton subtypeRadioButton = 376 (RadioButton) radioView.findViewById(R.id.item_radio); 377 if (subtypeRadioButton == null) { 378 Log.w(TAG, "RadioButton was not found in the selected subtype view"); 379 return; 380 } 381 Pair<InputMethodInfo, InputMethodSubtype> imiAndSubtype = 382 mRadioViewAndImiMap.get(radioView); 383 if (imiAndSubtype.first.getId().equals(imi.getId()) 384 && (imiAndSubtype.second == null || imiAndSubtype.second.equals(subtype))) { 385 subtypeRadioButton.setChecked(true); 386 } else { 387 subtypeRadioButton.setChecked(false); 388 } 389 } 390 } 391 392 private TreeMap<InputMethodInfo, List<InputMethodSubtype>> 393 getEnabledInputMethodAndSubtypeList() { 394 String newEnabledIMIs = Settings.Secure.getString( 395 mContext.getContentResolver(), Settings.Secure.ENABLED_INPUT_METHODS); 396 String currentSystemLocaleString = 397 mContext.getResources().getConfiguration().locale.toString(); 398 if (!TextUtils.equals(mEnabledInputMethodAndSubtypesCacheStr, newEnabledIMIs) 399 || !TextUtils.equals(mLastSystemLocaleString, currentSystemLocaleString) 400 || mPackageChanged) { 401 mEnabledInputMethodAndSubtypesCache.clear(); 402 final List<InputMethodInfo> imis = mImm.getEnabledInputMethodList(); 403 for (InputMethodInfo imi: imis) { 404 mEnabledInputMethodAndSubtypesCache.put(imi, 405 mImm.getEnabledInputMethodSubtypeList(imi, true)); 406 } 407 mEnabledInputMethodAndSubtypesCacheStr = newEnabledIMIs; 408 mPackageChanged = false; 409 mLastSystemLocaleString = currentSystemLocaleString; 410 } 411 return mEnabledInputMethodAndSubtypesCache; 412 } 413 414 private InputMethodInfo getCurrentInputMethodInfo() { 415 String curInputMethodId = Settings.Secure.getString(getContext() 416 .getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); 417 Set<InputMethodInfo> cachedImiSet = mEnabledInputMethodAndSubtypesCache.keySet(); 418 // 1. Search IMI in cache 419 for (InputMethodInfo imi: cachedImiSet) { 420 if (imi.getId().equals(curInputMethodId)) { 421 return imi; 422 } 423 } 424 // 2. Get current enabled IMEs and search IMI 425 cachedImiSet = getEnabledInputMethodAndSubtypeList().keySet(); 426 for (InputMethodInfo imi: cachedImiSet) { 427 if (imi.getId().equals(curInputMethodId)) { 428 return imi; 429 } 430 } 431 return null; 432 } 433 434 private CharSequence getIMIName(InputMethodInfo imi) { 435 if (imi == null) return null; 436 return imi.loadLabel(mPackageManager); 437 } 438 439 private CharSequence getSubtypeName(InputMethodInfo imi, InputMethodSubtype subtype) { 440 if (imi == null || subtype == null) return null; 441 if (DEBUG) { 442 Log.d(TAG, "Get text from: " + imi.getPackageName() + subtype.getNameResId() 443 + imi.getServiceInfo().applicationInfo); 444 } 445 return subtype.getDisplayName( 446 mContext, imi.getPackageName(), imi.getServiceInfo().applicationInfo); 447 } 448 449 private Drawable getSubtypeIcon(InputMethodInfo imi, InputMethodSubtype subtype) { 450 if (imi != null) { 451 if (DEBUG) { 452 Log.d(TAG, "Update icons of IME: " + imi.getPackageName()); 453 if (subtype != null) { 454 Log.d(TAG, "subtype =" + subtype.getLocale() + "," + subtype.getMode()); 455 } 456 } 457 if (subtype != null) { 458 return mPackageManager.getDrawable(imi.getPackageName(), subtype.getIconResId(), 459 imi.getServiceInfo().applicationInfo); 460 } else if (imi.getSubtypeCount() > 0) { 461 return mPackageManager.getDrawable(imi.getPackageName(), 462 imi.getSubtypeAt(0).getIconResId(), 463 imi.getServiceInfo().applicationInfo); 464 } else { 465 try { 466 return mPackageManager.getApplicationInfo( 467 imi.getPackageName(), 0).loadIcon(mPackageManager); 468 } catch (PackageManager.NameNotFoundException e) { 469 Log.w(TAG, "IME can't be found: " + imi.getPackageName()); 470 } 471 } 472 } 473 return null; 474 } 475 476 private void onPackageChanged() { 477 if (DEBUG) { 478 Log.d(TAG, "onPackageChanged."); 479 } 480 mPackageChanged = true; 481 } 482 483 public interface OnHardKeyboardEnabledChangeListener { 484 public void onHardKeyboardEnabledChange(boolean enabled); 485 } 486 487 } 488