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 isCommitContentSupported( @ullable EditorInfo editorInfo, @NonNull String mimeType)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 doCommitContent(@onNull String description, @NonNull String mimeType, @NonNull File file)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 validatePackageName(@ullable EditorInfo editorInfo)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 onCreate()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 onCreateInputView()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 onEvaluateFullscreenMode()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 onStartInputView(EditorInfo info, boolean restarting)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 getFileForResource( @onNull Context context, @RawRes int res, @NonNull File outputDir, @NonNull String filename)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