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