Home | History | Annotate | Download | only in inputmethod
      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 &lt;grant-uri-permissions&gt;} tag.</p>
    135      *
    136      * <p>Supported only on API &gt;= 25.</p>
    137      *
    138      * <p>On API &lt;= 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 &gt;= 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 &gt;= 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