SpellCheckerSession.java revision 44b75030931d9c65c9e495a86d11d71da59b4429
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 17package android.view.textservice; 18 19import com.android.internal.textservice.ISpellCheckerSession; 20import com.android.internal.textservice.ISpellCheckerSessionListener; 21import com.android.internal.textservice.ITextServicesManager; 22import com.android.internal.textservice.ITextServicesSessionListener; 23 24import android.os.Handler; 25import android.os.Message; 26import android.os.RemoteException; 27import android.util.Log; 28import android.view.textservice.SpellCheckerInfo; 29import android.view.textservice.SuggestionsInfo; 30import android.view.textservice.TextInfo; 31 32import java.util.LinkedList; 33import java.util.Queue; 34 35/** 36 * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService. 37 * 38 * 39 * <a name="Applications"></a> 40 * <h3>Applications</h3> 41 * 42 * <p>In most cases, applications that are using the standard 43 * {@link android.widget.TextView} or its subclasses will have little they need 44 * to do to work well with spell checker services. The main things you need to 45 * be aware of are:</p> 46 * 47 * <ul> 48 * <li> Properly set the {@link android.R.attr#inputType} in your editable 49 * text views, so that the spell checker will have enough context to help the 50 * user in editing text in them. 51 * </ul> 52 * 53 * <p>For the rare people amongst us writing client applications that use the spell checker service 54 * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or 55 * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker 56 * service by yourself.</p> 57 * 58 * <h3>Security</h3> 59 * 60 * <p>There are a lot of security issues associated with spell checkers, 61 * since they could monitor all the text being sent to them 62 * through, for instance, {@link android.widget.TextView}. 63 * The Android spell checker framework also allows 64 * arbitrary third party spell checkers, so care must be taken to restrict their 65 * selection and interactions.</p> 66 * 67 * <p>Here are some key points about the security architecture behind the 68 * spell checker framework:</p> 69 * 70 * <ul> 71 * <li>Only the system is allowed to directly access a spell checker framework's 72 * {@link android.service.textservice.SpellCheckerService} interface, via the 73 * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission. This is 74 * enforced in the system by not binding to a spell checker service that does 75 * not require this permission. 76 * 77 * <li>The user must explicitly enable a new spell checker in settings before 78 * they can be enabled, to confirm with the system that they know about it 79 * and want to make it available for use. 80 * </ul> 81 * 82 */ 83public class SpellCheckerSession { 84 private static final String TAG = SpellCheckerSession.class.getSimpleName(); 85 private static final boolean DBG = false; 86 /** 87 * Name under which a SpellChecker service component publishes information about itself. 88 * This meta-data must reference an XML resource. 89 **/ 90 public static final String SERVICE_META_DATA = "android.view.textservice.scs"; 91 92 93 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; 94 95 private final InternalListener mInternalListener; 96 private final ITextServicesManager mTextServicesManager; 97 private final SpellCheckerInfo mSpellCheckerInfo; 98 private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; 99 100 private boolean mIsUsed; 101 private SpellCheckerSessionListener mSpellCheckerSessionListener; 102 103 /** Handler that will execute the main tasks */ 104 private final Handler mHandler = new Handler() { 105 @Override 106 public void handleMessage(Message msg) { 107 switch (msg.what) { 108 case MSG_ON_GET_SUGGESTION_MULTIPLE: 109 handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); 110 break; 111 } 112 } 113 }; 114 115 /** 116 * Constructor 117 * @hide 118 */ 119 public SpellCheckerSession( 120 SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) { 121 if (info == null || listener == null || tsm == null) { 122 throw new NullPointerException(); 123 } 124 mSpellCheckerInfo = info; 125 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); 126 mInternalListener = new InternalListener(); 127 mTextServicesManager = tsm; 128 mIsUsed = true; 129 mSpellCheckerSessionListener = listener; 130 } 131 132 /** 133 * @return true if the connection to a text service of this session is disconnected and not 134 * alive. 135 */ 136 public boolean isSessionDisconnected() { 137 return mSpellCheckerSessionListenerImpl.isDisconnected(); 138 } 139 140 /** 141 * Get the spell checker service info this spell checker session has. 142 * @return SpellCheckerInfo for the specified locale. 143 */ 144 public SpellCheckerInfo getSpellChecker() { 145 return mSpellCheckerInfo; 146 } 147 148 /** 149 * Finish this session and allow TextServicesManagerService to disconnect the bound spell 150 * checker. 151 */ 152 public void close() { 153 mIsUsed = false; 154 try { 155 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); 156 } catch (RemoteException e) { 157 // do nothing 158 } 159 } 160 161 /** 162 * Get candidate strings for a substring of the specified text. 163 * @param textInfo text metadata for a spell checker 164 * @param suggestionsLimit the number of limit of suggestions returned 165 */ 166 public void getSuggestions(TextInfo textInfo, int suggestionsLimit) { 167 getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false); 168 } 169 170 /** 171 * A batch process of getSuggestions 172 * @param textInfos an array of text metadata for a spell checker 173 * @param suggestionsLimit the number of limit of suggestions returned 174 * @param sequentialWords true if textInfos can be treated as sequential words. 175 */ 176 public void getSuggestions( 177 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 178 if (DBG) { 179 Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId()); 180 } 181 // TODO: Handle multiple words suggestions by using WordBreakIterator 182 mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( 183 textInfos, suggestionsLimit, sequentialWords); 184 } 185 186 private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) { 187 mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); 188 } 189 190 private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub { 191 private static final int TASK_CANCEL = 1; 192 private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; 193 private final Queue<SpellCheckerParams> mPendingTasks = 194 new LinkedList<SpellCheckerParams>(); 195 private final Handler mHandler; 196 197 private boolean mOpened; 198 private ISpellCheckerSession mISpellCheckerSession; 199 200 public SpellCheckerSessionListenerImpl(Handler handler) { 201 mOpened = false; 202 mHandler = handler; 203 } 204 205 private static class SpellCheckerParams { 206 public final int mWhat; 207 public final TextInfo[] mTextInfos; 208 public final int mSuggestionsLimit; 209 public final boolean mSequentialWords; 210 public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, 211 boolean sequentialWords) { 212 mWhat = what; 213 mTextInfos = textInfos; 214 mSuggestionsLimit = suggestionsLimit; 215 mSequentialWords = sequentialWords; 216 } 217 } 218 219 private void processTask(SpellCheckerParams scp) { 220 switch (scp.mWhat) { 221 case TASK_CANCEL: 222 processCancel(); 223 break; 224 case TASK_GET_SUGGESTIONS_MULTIPLE: 225 processGetSuggestionsMultiple(scp); 226 break; 227 } 228 } 229 230 public synchronized void onServiceConnected(ISpellCheckerSession session) { 231 mISpellCheckerSession = session; 232 mOpened = true; 233 if (DBG) 234 Log.d(TAG, "onServiceConnected - Success"); 235 while (!mPendingTasks.isEmpty()) { 236 processTask(mPendingTasks.poll()); 237 } 238 } 239 240 public void getSuggestionsMultiple( 241 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 242 if (DBG) { 243 Log.w(TAG, "getSuggestionsMultiple"); 244 } 245 processOrEnqueueTask( 246 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos, 247 suggestionsLimit, sequentialWords)); 248 } 249 250 public boolean isDisconnected() { 251 return mOpened && mISpellCheckerSession == null; 252 } 253 254 public boolean checkOpenConnection() { 255 if (mISpellCheckerSession != null) { 256 return true; 257 } 258 Log.e(TAG, "not connected to the spellchecker service."); 259 return false; 260 } 261 262 private void processOrEnqueueTask(SpellCheckerParams scp) { 263 if (DBG) { 264 Log.d(TAG, "process or enqueue task: " + mISpellCheckerSession); 265 } 266 if (mISpellCheckerSession == null) { 267 mPendingTasks.offer(scp); 268 } else { 269 processTask(scp); 270 } 271 } 272 273 private void processCancel() { 274 if (!checkOpenConnection()) { 275 return; 276 } 277 if (DBG) { 278 Log.w(TAG, "Cancel spell checker tasks."); 279 } 280 try { 281 mISpellCheckerSession.onCancel(); 282 } catch (RemoteException e) { 283 Log.e(TAG, "Failed to cancel " + e); 284 } 285 } 286 287 private void processGetSuggestionsMultiple(SpellCheckerParams scp) { 288 if (!checkOpenConnection()) { 289 return; 290 } 291 if (DBG) { 292 Log.w(TAG, "Get suggestions from the spell checker."); 293 } 294 try { 295 mISpellCheckerSession.onGetSuggestionsMultiple( 296 scp.mTextInfos, scp.mSuggestionsLimit, scp.mSequentialWords); 297 } catch (RemoteException e) { 298 Log.e(TAG, "Failed to get suggestions " + e); 299 } 300 } 301 302 @Override 303 public void onGetSuggestions(SuggestionsInfo[] results) { 304 mHandler.sendMessage(Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE, results)); 305 } 306 } 307 308 /** 309 * Callback for getting results from text services 310 */ 311 public interface SpellCheckerSessionListener { 312 /** 313 * Callback for "getSuggestions" 314 * @param results an array of results of getSuggestions 315 */ 316 public void onGetSuggestions(SuggestionsInfo[] results); 317 } 318 319 private class InternalListener extends ITextServicesSessionListener.Stub { 320 @Override 321 public void onServiceConnected(ISpellCheckerSession session) { 322 if (DBG) { 323 Log.w(TAG, "SpellCheckerSession connected."); 324 } 325 mSpellCheckerSessionListenerImpl.onServiceConnected(session); 326 } 327 } 328 329 @Override 330 protected void finalize() throws Throwable { 331 super.finalize(); 332 if (mIsUsed) { 333 Log.e(TAG, "SpellCheckerSession was not finished properly." + 334 "You should call finishShession() when you finished to use a spell checker."); 335 close(); 336 } 337 } 338 339 /** 340 * @hide 341 */ 342 public ITextServicesSessionListener getTextServicesSessionListener() { 343 return mInternalListener; 344 } 345 346 /** 347 * @hide 348 */ 349 public ISpellCheckerSessionListener getSpellCheckerSessionListener() { 350 return mSpellCheckerSessionListenerImpl; 351 } 352} 353