Home | History | Annotate | Download | only in ime
      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 com.example.android.commitcontent.ime;
     18 
     19 import android.app.AppOpsManager;
     20 import android.content.ClipDescription;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.pm.PackageManager;
     24 import android.inputmethodservice.InputMethodService;
     25 import android.net.Uri;
     26 import android.os.Build;
     27 import android.support.annotation.NonNull;
     28 import android.support.annotation.Nullable;
     29 import android.support.annotation.RawRes;
     30 import android.support.v13.view.inputmethod.EditorInfoCompat;
     31 import android.support.v13.view.inputmethod.InputConnectionCompat;
     32 import android.support.v13.view.inputmethod.InputContentInfoCompat;
     33 import android.support.v4.content.FileProvider;
     34 import android.util.Log;
     35 import android.view.View;
     36 import android.view.inputmethod.EditorInfo;
     37 import android.view.inputmethod.InputBinding;
     38 import android.view.inputmethod.InputConnection;
     39 import android.widget.Button;
     40 import android.widget.LinearLayout;
     41 
     42 import java.io.File;
     43 import java.io.FileOutputStream;
     44 import java.io.IOException;
     45 import java.io.InputStream;
     46 import java.io.OutputStream;
     47 
     48 
     49 public class ImageKeyboard extends InputMethodService {
     50 
     51     private static final String TAG = "ImageKeyboard";
     52     private static final String AUTHORITY = "com.example.android.commitcontent.ime.inputcontent";
     53     private static final String MIME_TYPE_GIF = "image/gif";
     54     private static final String MIME_TYPE_PNG = "image/png";
     55     private static final String MIME_TYPE_WEBP = "image/webp";
     56 
     57     private File mPngFile;
     58     private File mGifFile;
     59     private File mWebpFile;
     60     private Button mGifButton;
     61     private Button mPngButton;
     62     private Button mWebpButton;
     63 
     64     private boolean isCommitContentSupported(
     65             @Nullable EditorInfo editorInfo, @NonNull String mimeType) {
     66         if (editorInfo == null) {
     67             return false;
     68         }
     69 
     70         final InputConnection ic = getCurrentInputConnection();
     71         if (ic == null) {
     72             return false;
     73         }
     74 
     75         if (!validatePackageName(editorInfo)) {
     76             return false;
     77         }
     78 
     79         final String[] supportedMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);
     80         for (String supportedMimeType : supportedMimeTypes) {
     81             if (ClipDescription.compareMimeTypes(mimeType, supportedMimeType)) {
     82                 return true;
     83             }
     84         }
     85         return false;
     86     }
     87 
     88     private void doCommitContent(@NonNull String description, @NonNull String mimeType,
     89             @NonNull File file) {
     90         final EditorInfo editorInfo = getCurrentInputEditorInfo();
     91 
     92         // Validate packageName again just in case.
     93         if (!validatePackageName(editorInfo)) {
     94             return;
     95         }
     96 
     97         final Uri contentUri = FileProvider.getUriForFile(this, AUTHORITY, file);
     98 
     99         // As you as an IME author are most likely to have to implement your own content provider
    100         // to support CommitContent API, it is important to have a clear spec about what
    101         // applications are going to be allowed to access the content that your are going to share.
    102         final int flag;
    103         if (Build.VERSION.SDK_INT >= 25) {
    104             // On API 25 and later devices, as an analogy of Intent.FLAG_GRANT_READ_URI_PERMISSION,
    105             // you can specify InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION to give
    106             // a temporary read access to the recipient application without exporting your content
    107             // provider.
    108             flag = InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
    109         } else {
    110             // On API 24 and prior devices, we cannot rely on
    111             // InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION. You as an IME author
    112             // need to decide what access control is needed (or not needed) for content URIs that
    113             // you are going to expose. This sample uses Context.grantUriPermission(), but you can
    114             // implement your own mechanism that satisfies your own requirements.
    115             flag = 0;
    116             try {
    117                 // TODO: Use revokeUriPermission to revoke as needed.
    118                 grantUriPermission(
    119                         editorInfo.packageName, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
    120             } catch (Exception e){
    121                 Log.e(TAG, "grantUriPermission failed packageName=" + editorInfo.packageName
    122                         + " contentUri=" + contentUri, e);
    123             }
    124         }
    125 
    126         final InputContentInfoCompat inputContentInfoCompat = new InputContentInfoCompat(
    127                 contentUri,
    128                 new ClipDescription(description, new String[]{mimeType}),
    129                 null /* linkUrl */);
    130         InputConnectionCompat.commitContent(
    131                 getCurrentInputConnection(), getCurrentInputEditorInfo(), inputContentInfoCompat,
    132                 flag, null);
    133     }
    134 
    135     private boolean validatePackageName(@Nullable EditorInfo editorInfo) {
    136         if (editorInfo == null) {
    137             return false;
    138         }
    139         final String packageName = editorInfo.packageName;
    140         if (packageName == null) {
    141             return false;
    142         }
    143 
    144         // In Android L MR-1 and prior devices, EditorInfo.packageName is not a reliable identifier
    145         // of the target application because:
    146         //   1. the system does not verify it [1]
    147         //   2. InputMethodManager.startInputInner() had filled EditorInfo.packageName with
    148         //      view.getContext().getPackageName() [2]
    149         // [1]: https://android.googlesource.com/platform/frameworks/base/+/a0f3ad1b5aabe04d9eb1df8bad34124b826ab641
    150         // [2]: https://android.googlesource.com/platform/frameworks/base/+/02df328f0cd12f2af87ca96ecf5819c8a3470dc8
    151         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    152             return true;
    153         }
    154 
    155         final InputBinding inputBinding = getCurrentInputBinding();
    156         if (inputBinding == null) {
    157             // Due to b.android.com/225029, it is possible that getCurrentInputBinding() returns
    158             // null even after onStartInputView() is called.
    159             // TODO: Come up with a way to work around this bug....
    160             Log.e(TAG, "inputBinding should not be null here. "
    161                     + "You are likely to be hitting b.android.com/225029");
    162             return false;
    163         }
    164         final int packageUid = inputBinding.getUid();
    165 
    166         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    167             final AppOpsManager appOpsManager =
    168                     (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
    169             try {
    170                 appOpsManager.checkPackage(packageUid, packageName);
    171             } catch (Exception e) {
    172                 return false;
    173             }
    174             return true;
    175         }
    176 
    177         final PackageManager packageManager = getPackageManager();
    178         final String possiblePackageNames[] = packageManager.getPackagesForUid(packageUid);
    179         for (final String possiblePackageName : possiblePackageNames) {
    180             if (packageName.equals(possiblePackageName)) {
    181                 return true;
    182             }
    183         }
    184         return false;
    185     }
    186 
    187     @Override
    188     public void onCreate() {
    189         super.onCreate();
    190 
    191         // TODO: Avoid file I/O in the main thread.
    192         final File imagesDir = new File(getFilesDir(), "images");
    193         imagesDir.mkdirs();
    194         mGifFile = getFileForResource(this, R.raw.animated_gif, imagesDir, "image.gif");
    195         mPngFile = getFileForResource(this, R.raw.dessert_android, imagesDir, "image.png");
    196         mWebpFile = getFileForResource(this, R.raw.animated_webp, imagesDir, "image.webp");
    197     }
    198 
    199     @Override
    200     public View onCreateInputView() {
    201         mGifButton = new Button(this);
    202         mGifButton.setText("Insert GIF");
    203         mGifButton.setOnClickListener(new View.OnClickListener() {
    204             @Override
    205             public void onClick(View view) {
    206                 ImageKeyboard.this.doCommitContent("A waving flag", MIME_TYPE_GIF, mGifFile);
    207             }
    208         });
    209 
    210         mPngButton = new Button(this);
    211         mPngButton.setText("Insert PNG");
    212         mPngButton.setOnClickListener(new View.OnClickListener() {
    213             @Override
    214             public void onClick(View view) {
    215                 ImageKeyboard.this.doCommitContent("A droid logo", MIME_TYPE_PNG, mPngFile);
    216             }
    217         });
    218 
    219         mWebpButton = new Button(this);
    220         mWebpButton.setText("Insert WebP");
    221         mWebpButton.setOnClickListener(new View.OnClickListener() {
    222             @Override
    223             public void onClick(View view) {
    224                 ImageKeyboard.this.doCommitContent(
    225                         "Android N recovery animation", MIME_TYPE_WEBP, mWebpFile);
    226             }
    227         });
    228 
    229         final LinearLayout layout = new LinearLayout(this);
    230         layout.setOrientation(LinearLayout.VERTICAL);
    231         layout.addView(mGifButton);
    232         layout.addView(mPngButton);
    233         layout.addView(mWebpButton);
    234         return layout;
    235     }
    236 
    237     @Override
    238     public boolean onEvaluateFullscreenMode() {
    239         // In full-screen mode the inserted content is likely to be hidden by the IME. Hence in this
    240         // sample we simply disable full-screen mode.
    241         return false;
    242     }
    243 
    244     @Override
    245     public void onStartInputView(EditorInfo info, boolean restarting) {
    246         mGifButton.setEnabled(mGifFile != null && isCommitContentSupported(info, MIME_TYPE_GIF));
    247         mPngButton.setEnabled(mPngFile != null && isCommitContentSupported(info, MIME_TYPE_PNG));
    248         mWebpButton.setEnabled(mWebpFile != null && isCommitContentSupported(info, MIME_TYPE_WEBP));
    249     }
    250 
    251     private static File getFileForResource(
    252             @NonNull Context context, @RawRes int res, @NonNull File outputDir,
    253             @NonNull String filename) {
    254         final File outputFile = new File(outputDir, filename);
    255         final byte[] buffer = new byte[4096];
    256         InputStream resourceReader = null;
    257         try {
    258             try {
    259                 resourceReader = context.getResources().openRawResource(res);
    260                 OutputStream dataWriter = null;
    261                 try {
    262                     dataWriter = new FileOutputStream(outputFile);
    263                     while (true) {
    264                         final int numRead = resourceReader.read(buffer);
    265                         if (numRead <= 0) {
    266                             break;
    267                         }
    268                         dataWriter.write(buffer, 0, numRead);
    269                     }
    270                     return outputFile;
    271                 } finally {
    272                     if (dataWriter != null) {
    273                         dataWriter.flush();
    274                         dataWriter.close();
    275                     }
    276                 }
    277             } finally {
    278                 if (resourceReader != null) {
    279                     resourceReader.close();
    280                 }
    281             }
    282         } catch (IOException e) {
    283             return null;
    284         }
    285     }
    286 }
    287