Skip to content

Most visited

Recently visited

navigation
CommitContentSampleIME / src / com.example.android.commitcontent.ime /

ImageKeyboard.java

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
}