| /* | |
| * Copyright (C) 2016 The Android Open Source Project | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.example.android.commitcontent.ime; | |
| import android.app.AppOpsManager; | |
| import android.content.ClipDescription; | |
| import android.content.Context; | |
| import android.content.Intent; | |
| import android.content.pm.PackageManager; | |
| import android.inputmethodservice.InputMethodService; | |
| import android.net.Uri; | |
| import android.os.Build; | |
| import android.support.annotation.NonNull; | |
| import android.support.annotation.Nullable; | |
| import android.support.annotation.RawRes; | |
| import android.support.v13.view.inputmethod.EditorInfoCompat; | |
| import android.support.v13.view.inputmethod.InputConnectionCompat; | |
| import android.support.v13.view.inputmethod.InputContentInfoCompat; | |
| import android.support.v4.content.FileProvider; | |
| import android.util.Log; | |
| import android.view.View; | |
| import android.view.inputmethod.EditorInfo; | |
| import android.view.inputmethod.InputBinding; | |
| import android.view.inputmethod.InputConnection; | |
| import android.widget.Button; | |
| import android.widget.LinearLayout; | |
| import java.io.File; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| public class ImageKeyboard extends InputMethodService { | |
| private static final String TAG = "ImageKeyboard"; | |
| private static final String AUTHORITY = "com.example.android.commitcontent.ime.inputcontent"; | |
| private static final String MIME_TYPE_GIF = "image/gif"; | |
| private static final String MIME_TYPE_PNG = "image/png"; | |
| private static final String MIME_TYPE_WEBP = "image/webp"; | |
| private File mPngFile; | |
| private File mGifFile; | |
| private File mWebpFile; | |
| private Button mGifButton; | |
| private Button mPngButton; | |
| private Button mWebpButton; | |
| private boolean isCommitContentSupported( | |
| @Nullable EditorInfo editorInfo, @NonNull String mimeType) { | |
| if (editorInfo == null) { | |
| return false; | |
| } | |
| final InputConnection ic = getCurrentInputConnection(); | |
| if (ic == null) { | |
| return false; | |
| } | |
| if (!validatePackageName(editorInfo)) { | |
| return false; | |
| } | |
| final String[] supportedMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo); | |
| for (String supportedMimeType : supportedMimeTypes) { | |
| if (ClipDescription.compareMimeTypes(mimeType, supportedMimeType)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| private void doCommitContent(@NonNull String description, @NonNull String mimeType, | |
| @NonNull File file) { | |
| final EditorInfo editorInfo = getCurrentInputEditorInfo(); | |
| // Validate packageName again just in case. | |
| if (!validatePackageName(editorInfo)) { | |
| return; | |
| } | |
| final Uri contentUri = FileProvider.getUriForFile(this, AUTHORITY, file); | |
| // As you as an IME author are most likely to have to implement your own content provider | |
| // to support CommitContent API, it is important to have a clear spec about what | |
| // applications are going to be allowed to access the content that your are going to share. | |
| final int flag; | |
| if (Build.VERSION.SDK_INT >= 25) { | |
| // On API 25 and later devices, as an analogy of Intent.FLAG_GRANT_READ_URI_PERMISSION, | |
| // you can specify InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION to give | |
| // a temporary read access to the recipient application without exporting your content | |
| // provider. | |
| flag = InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION; | |
| } else { | |
| // On API 24 and prior devices, we cannot rely on | |
| // InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION. You as an IME author | |
| // need to decide what access control is needed (or not needed) for content URIs that | |
| // you are going to expose. This sample uses Context.grantUriPermission(), but you can | |
| // implement your own mechanism that satisfies your own requirements. | |
| flag = 0; | |
| try { | |
| // TODO: Use revokeUriPermission to revoke as needed. | |
| grantUriPermission( | |
| editorInfo.packageName, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); | |
| } catch (Exception e){ | |
| Log.e(TAG, "grantUriPermission failed packageName=" + editorInfo.packageName | |
| + " contentUri=" + contentUri, e); | |
| } | |
| } | |
| final InputContentInfoCompat inputContentInfoCompat = new InputContentInfoCompat( | |
| contentUri, | |
| new ClipDescription(description, new String[]{mimeType}), | |
| null /* linkUrl */); | |
| InputConnectionCompat.commitContent( | |
| getCurrentInputConnection(), getCurrentInputEditorInfo(), inputContentInfoCompat, | |
| flag, null); | |
| } | |
| private boolean validatePackageName(@Nullable EditorInfo editorInfo) { | |
| if (editorInfo == null) { | |
| return false; | |
| } | |
| final String packageName = editorInfo.packageName; | |
| if (packageName == null) { | |
| return false; | |
| } | |
| // In Android L MR-1 and prior devices, EditorInfo.packageName is not a reliable identifier | |
| // of the target application because: | |
| // 1. the system does not verify it [1] | |
| // 2. InputMethodManager.startInputInner() had filled EditorInfo.packageName with | |
| // view.getContext().getPackageName() [2] | |
| // [1]: https://android.googlesource.com/platform/frameworks/base/+/a0f3ad1b5aabe04d9eb1df8bad34124b826ab641 | |
| // [2]: https://android.googlesource.com/platform/frameworks/base/+/02df328f0cd12f2af87ca96ecf5819c8a3470dc8 | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | |
| return true; | |
| } | |
| final InputBinding inputBinding = getCurrentInputBinding(); | |
| if (inputBinding == null) { | |
| // Due to b.android.com/225029, it is possible that getCurrentInputBinding() returns | |
| // null even after onStartInputView() is called. | |
| // TODO: Come up with a way to work around this bug.... | |
| Log.e(TAG, "inputBinding should not be null here. " | |
| + "You are likely to be hitting b.android.com/225029"); | |
| return false; | |
| } | |
| final int packageUid = inputBinding.getUid(); | |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | |
| final AppOpsManager appOpsManager = | |
| (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); | |
| try { | |
| appOpsManager.checkPackage(packageUid, packageName); | |
| } catch (Exception e) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| final PackageManager packageManager = getPackageManager(); | |
| final String possiblePackageNames[] = packageManager.getPackagesForUid(packageUid); | |
| for (final String possiblePackageName : possiblePackageNames) { | |
| if (packageName.equals(possiblePackageName)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| @Override | |
| public void onCreate() { | |
| super.onCreate(); | |
| // TODO: Avoid file I/O in the main thread. | |
| final File imagesDir = new File(getFilesDir(), "images"); | |
| imagesDir.mkdirs(); | |
| mGifFile = getFileForResource(this, R.raw.animated_gif, imagesDir, "image.gif"); | |
| mPngFile = getFileForResource(this, R.raw.dessert_android, imagesDir, "image.png"); | |
| mWebpFile = getFileForResource(this, R.raw.animated_webp, imagesDir, "image.webp"); | |
| } | |
| @Override | |
| public View onCreateInputView() { | |
| mGifButton = new Button(this); | |
| mGifButton.setText("Insert GIF"); | |
| mGifButton.setOnClickListener(new View.OnClickListener() { | |
| @Override | |
| public void onClick(View view) { | |
| ImageKeyboard.this.doCommitContent("A waving flag", MIME_TYPE_GIF, mGifFile); | |
| } | |
| }); | |
| mPngButton = new Button(this); | |
| mPngButton.setText("Insert PNG"); | |
| mPngButton.setOnClickListener(new View.OnClickListener() { | |
| @Override | |
| public void onClick(View view) { | |
| ImageKeyboard.this.doCommitContent("A droid logo", MIME_TYPE_PNG, mPngFile); | |
| } | |
| }); | |
| mWebpButton = new Button(this); | |
| mWebpButton.setText("Insert WebP"); | |
| mWebpButton.setOnClickListener(new View.OnClickListener() { | |
| @Override | |
| public void onClick(View view) { | |
| ImageKeyboard.this.doCommitContent( | |
| "Android N recovery animation", MIME_TYPE_WEBP, mWebpFile); | |
| } | |
| }); | |
| final LinearLayout layout = new LinearLayout(this); | |
| layout.setOrientation(LinearLayout.VERTICAL); | |
| layout.addView(mGifButton); | |
| layout.addView(mPngButton); | |
| layout.addView(mWebpButton); | |
| return layout; | |
| } | |
| @Override | |
| public boolean onEvaluateFullscreenMode() { | |
| // In full-screen mode the inserted content is likely to be hidden by the IME. Hence in this | |
| // sample we simply disable full-screen mode. | |
| return false; | |
| } | |
| @Override | |
| public void onStartInputView(EditorInfo info, boolean restarting) { | |
| mGifButton.setEnabled(mGifFile != null && isCommitContentSupported(info, MIME_TYPE_GIF)); | |
| mPngButton.setEnabled(mPngFile != null && isCommitContentSupported(info, MIME_TYPE_PNG)); | |
| mWebpButton.setEnabled(mWebpFile != null && isCommitContentSupported(info, MIME_TYPE_WEBP)); | |
| } | |
| private static File getFileForResource( | |
| @NonNull Context context, @RawRes int res, @NonNull File outputDir, | |
| @NonNull String filename) { | |
| final File outputFile = new File(outputDir, filename); | |
| final byte[] buffer = new byte[4096]; | |
| InputStream resourceReader = null; | |
| try { | |
| try { | |
| resourceReader = context.getResources().openRawResource(res); | |
| OutputStream dataWriter = null; | |
| try { | |
| dataWriter = new FileOutputStream(outputFile); | |
| while (true) { | |
| final int numRead = resourceReader.read(buffer); | |
| if (numRead <= 0) { | |
| break; | |
| } | |
| dataWriter.write(buffer, 0, numRead); | |
| } | |
| return outputFile; | |
| } finally { | |
| if (dataWriter != null) { | |
| dataWriter.flush(); | |
| dataWriter.close(); | |
| } | |
| } | |
| } finally { | |
| if (resourceReader != null) { | |
| resourceReader.close(); | |
| } | |
| } | |
| } catch (IOException e) { | |
| return null; | |
| } | |
| } | |
| } |