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