1/** 2 * Copyright (C) 2016 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.support.v13.view.inputmethod; 18 19import android.support.annotation.RequiresApi; 20import android.content.ClipDescription; 21import android.net.Uri; 22import android.os.Build; 23import android.os.Bundle; 24import android.os.ResultReceiver; 25import android.support.annotation.NonNull; 26import android.support.annotation.Nullable; 27import android.text.TextUtils; 28import android.view.inputmethod.EditorInfo; 29import android.view.inputmethod.InputConnection; 30import android.view.inputmethod.InputConnectionWrapper; 31import android.view.inputmethod.InputContentInfo; 32 33/** 34 * Helper for accessing features in {@link InputConnection} introduced after API level 13 in a 35 * backwards compatible fashion. 36 */ 37public final class InputConnectionCompat { 38 39 private interface InputConnectionCompatImpl { 40 boolean commitContent(@NonNull InputConnection inputConnection, 41 @NonNull InputContentInfoCompat inputContentInfo, int flags, @Nullable Bundle opts); 42 43 @NonNull 44 InputConnection createWrapper(@NonNull InputConnection ic, 45 @NonNull EditorInfo editorInfo, @NonNull OnCommitContentListener callback); 46 } 47 48 static final class InputContentInfoCompatBaseImpl implements InputConnectionCompatImpl { 49 50 private static String COMMIT_CONTENT_ACTION = 51 "android.support.v13.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT"; 52 private static String COMMIT_CONTENT_CONTENT_URI_KEY = 53 "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_URI"; 54 private static String COMMIT_CONTENT_DESCRIPTION_KEY = 55 "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION"; 56 private static String COMMIT_CONTENT_LINK_URI_KEY = 57 "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI"; 58 private static String COMMIT_CONTENT_OPTS_KEY = 59 "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_OPTS"; 60 private static String COMMIT_CONTENT_FLAGS_KEY = 61 "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS"; 62 private static String COMMIT_CONTENT_RESULT_RECEIVER = 63 "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER"; 64 65 @Override 66 public boolean commitContent(@NonNull InputConnection inputConnection, 67 @NonNull InputContentInfoCompat inputContentInfo, int flags, 68 @Nullable Bundle opts) { 69 final Bundle params = new Bundle(); 70 params.putParcelable(COMMIT_CONTENT_CONTENT_URI_KEY, inputContentInfo.getContentUri()); 71 params.putParcelable(COMMIT_CONTENT_DESCRIPTION_KEY, inputContentInfo.getDescription()); 72 params.putParcelable(COMMIT_CONTENT_LINK_URI_KEY, inputContentInfo.getLinkUri()); 73 params.putInt(COMMIT_CONTENT_FLAGS_KEY, flags); 74 params.putParcelable(COMMIT_CONTENT_OPTS_KEY, opts); 75 // TODO: Support COMMIT_CONTENT_RESULT_RECEIVER. 76 return inputConnection.performPrivateCommand(COMMIT_CONTENT_ACTION, params); 77 } 78 79 @NonNull 80 @Override 81 public InputConnection createWrapper(@NonNull InputConnection ic, 82 @NonNull EditorInfo editorInfo, 83 @NonNull OnCommitContentListener onCommitContentListener) { 84 String[] contentMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo); 85 if (contentMimeTypes.length == 0) { 86 return ic; 87 } 88 final OnCommitContentListener listener = onCommitContentListener; 89 return new InputConnectionWrapper(ic, false /* mutable */) { 90 @Override 91 public boolean performPrivateCommand(String action, Bundle data) { 92 if (InputContentInfoCompatBaseImpl.handlePerformPrivateCommand(action, data, 93 listener)) { 94 return true; 95 } 96 return super.performPrivateCommand(action, data); 97 } 98 }; 99 } 100 101 static boolean handlePerformPrivateCommand( 102 @Nullable String action, 103 @NonNull Bundle data, 104 @NonNull OnCommitContentListener onCommitContentListener) { 105 if (!TextUtils.equals(COMMIT_CONTENT_ACTION, action)) { 106 return false; 107 } 108 if (data == null) { 109 return false; 110 } 111 ResultReceiver resultReceiver = null; 112 boolean result = false; 113 try { 114 resultReceiver = data.getParcelable(COMMIT_CONTENT_RESULT_RECEIVER); 115 final Uri contentUri = data.getParcelable(COMMIT_CONTENT_CONTENT_URI_KEY); 116 final ClipDescription description = data.getParcelable( 117 COMMIT_CONTENT_DESCRIPTION_KEY); 118 final Uri linkUri = data.getParcelable(COMMIT_CONTENT_LINK_URI_KEY); 119 final int flags = data.getInt(COMMIT_CONTENT_FLAGS_KEY); 120 final Bundle opts = data.getParcelable(COMMIT_CONTENT_OPTS_KEY); 121 final InputContentInfoCompat inputContentInfo = 122 new InputContentInfoCompat(contentUri, description, linkUri); 123 result = onCommitContentListener.onCommitContent(inputContentInfo, flags, opts); 124 } finally { 125 if (resultReceiver != null) { 126 resultReceiver.send(result ? 1 : 0, null); 127 } 128 } 129 return result; 130 } 131 } 132 133 @RequiresApi(25) 134 private static final class InputContentInfoCompatApi25Impl 135 implements InputConnectionCompatImpl { 136 @Override 137 public boolean commitContent(@NonNull InputConnection inputConnection, 138 @NonNull InputContentInfoCompat inputContentInfo, int flags, 139 @Nullable Bundle opts) { 140 return inputConnection.commitContent((InputContentInfo) inputContentInfo.unwrap(), 141 flags, opts); 142 } 143 144 @Nullable 145 @Override 146 public InputConnection createWrapper( 147 @Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo, 148 @Nullable OnCommitContentListener onCommitContentListener) { 149 final OnCommitContentListener listener = onCommitContentListener; 150 return new InputConnectionWrapper(inputConnection, false /* mutable */) { 151 @Override 152 public boolean commitContent(InputContentInfo inputContentInfo, int flags, 153 Bundle opts) { 154 if (listener.onCommitContent(InputContentInfoCompat.wrap(inputContentInfo), 155 flags, opts)) { 156 return true; 157 } 158 return super.commitContent(inputContentInfo, flags, opts); 159 } 160 }; 161 } 162 } 163 164 private static final InputConnectionCompatImpl IMPL; 165 static { 166 if (Build.VERSION.SDK_INT >= 25) { 167 IMPL = new InputContentInfoCompatApi25Impl(); 168 } else { 169 IMPL = new InputContentInfoCompatBaseImpl(); 170 } 171 } 172 173 /** 174 * Calls commitContent API, in a backwards compatible fashion. 175 * 176 * @param inputConnection {@link InputConnection} with which commitContent API will be called 177 * @param editorInfo {@link EditorInfo} associated with the given {@code inputConnection} 178 * @param inputContentInfo content information to be passed to the editor 179 * @param flags {@code 0} or {@link #INPUT_CONTENT_GRANT_READ_URI_PERMISSION} 180 * @param opts optional bundle data. This can be {@code null} 181 * @return {@code true} if this request is accepted by the application, no matter if the request 182 * is already handled or still being handled in background 183 */ 184 public static boolean commitContent(@NonNull InputConnection inputConnection, 185 @NonNull EditorInfo editorInfo, @NonNull InputContentInfoCompat inputContentInfo, 186 int flags, @Nullable Bundle opts) { 187 final ClipDescription description = inputContentInfo.getDescription(); 188 boolean supported = false; 189 for (String mimeType : EditorInfoCompat.getContentMimeTypes(editorInfo)) { 190 if (description.hasMimeType(mimeType)) { 191 supported = true; 192 break; 193 } 194 } 195 if (!supported) { 196 return false; 197 } 198 199 return IMPL.commitContent(inputConnection, inputContentInfo, flags, opts); 200 } 201 202 /** 203 * When this flag is used, the editor will be able to request temporary access permissions to 204 * the content URI contained in the {@link InputContentInfoCompat} object, in a similar manner 205 * that has been recommended in 206 * <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files</a>. 207 * 208 * <p>Make sure that the content provider owning the Uri sets the 209 * {@link android.R.attr#grantUriPermissions grantUriPermissions} attribute in its manifest or 210 * included the {@code <grant-uri-permissions>} tag.</p> 211 * 212 * <p>Supported only on API >= 25.</p> 213 * 214 * <p>On API <= 24 devices, IME developers need to ensure that the content URI is accessible 215 * only from the target application, for example, by generating a URL with a unique name that 216 * others cannot guess. IME developers can also rely on the following information of the target 217 * application to do additional access checks in their {@link android.content.ContentProvider}. 218 * </p> 219 * <ul> 220 * <li>On API >= 23 {@link EditorInfo#packageName} is guaranteed to not be spoofed, which 221 * can later be compared with {@link android.content.ContentProvider#getCallingPackage()} in 222 * the {@link android.content.ContentProvider}. 223 * </li> 224 * <li>{@link android.view.inputmethod.InputBinding#getUid()} is guaranteed to not be 225 * spoofed, which can later be compared with {@link android.os.Binder#getCallingUid()} in 226 * the {@link android.content.ContentProvider}.</li> 227 * </ul> 228 */ 229 public static int INPUT_CONTENT_GRANT_READ_URI_PERMISSION = 0x00000001; 230 231 /** 232 * Listener for commitContent method call, in a backwards compatible fashion. 233 */ 234 public interface OnCommitContentListener { 235 /** 236 * Intercepts InputConnection#commitContent API calls. 237 * 238 * @param inputContentInfo content to be committed 239 * @param flags {@code 0} or {@link #INPUT_CONTENT_GRANT_READ_URI_PERMISSION} 240 * @param opts optional bundle data. This can be {@code null} 241 * @return {@code true} if this request is accepted by the application, no matter if the 242 * request is already handled or still being handled in background. {@code false} to use the 243 * default implementation 244 */ 245 boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts); 246 } 247 248 /** 249 * Creates a wrapper {@link InputConnection} object from an existing {@link InputConnection} 250 * and {@link OnCommitContentListener} that can be returned to the system. 251 * 252 * <p>By returning the wrapper object to the IME, the editor can be notified by 253 * {@link OnCommitContentListener#onCommitContent(InputContentInfoCompat, int, Bundle)} 254 * when the IME calls 255 * {@link InputConnectionCompat#commitContent(InputConnection, EditorInfo, 256 * InputContentInfoCompat, int, Bundle)} and the corresponding Framework API that is available 257 * on API >= 25.</p> 258 * 259 * @param inputConnection {@link InputConnection} to be wrapped 260 * @param editorInfo {@link EditorInfo} associated with the given {@code inputConnection} 261 * @param onCommitContentListener the listener that the wrapper object will call 262 * @return a wrapper {@link InputConnection} object that can be returned to the IME 263 * @throws IllegalArgumentException when {@code inputConnection}, {@code editorInfo}, or 264 * {@code onCommitContentListener} is {@code null} 265 */ 266 @NonNull 267 public static InputConnection createWrapper(@NonNull InputConnection inputConnection, 268 @NonNull EditorInfo editorInfo, 269 @NonNull OnCommitContentListener onCommitContentListener) { 270 if (inputConnection == null) { 271 throw new IllegalArgumentException("inputConnection must be non-null"); 272 } 273 if (editorInfo == null) { 274 throw new IllegalArgumentException("editorInfo must be non-null"); 275 } 276 if (onCommitContentListener == null) { 277 throw new IllegalArgumentException("onCommitContentListener must be non-null"); 278 } 279 return IMPL.createWrapper(inputConnection, editorInfo, onCommitContentListener); 280 } 281 282} 283