docs: Add new samples for N MR1

- AppShortcuts
- CommitContentSampleApp
- CommitContentSampleIME

Change-Id: I3cefc134839f944b1c0c5efc943fb779c7e7ee70
This commit is contained in:
Trevor Johns
2016-10-18 03:15:51 -07:00
parent bf0ff8fe8a
commit 0af1e772c4
43 changed files with 1683 additions and 0 deletions

View File

@@ -351,6 +351,9 @@ developers/build/prebuilts/gradle/DirectShare sam
developers/build/prebuilts/gradle/MidiScope samples/${PLATFORM_NAME}/media/MidiScope
developers/build/prebuilts/gradle/MidiSynth samples/${PLATFORM_NAME}/media/MidiSynth
developers/build/prebuilts/gradle/AsymmetricFingerprintDialog samples/${PLATFORM_NAME}/security/AsymmetricFingerprintDialog
developers/build/prebuilts/gradle/AppShortcuts samples/${PLATFORM_NAME}/system/AppShortcuts
developers/build/prebuilts/gradle/CommitContentSampleApp samples/${PLATFORM_NAME}/input/keyboard/CommitContentSampleApp
developers/build/prebuilts/gradle/CommitContentSampleIME samples/${PLATFORM_NAME}/input/keyboard/CommitContentSampleIME
developers/build/prebuilts/androidtv samples/${PLATFORM_NAME}/androidtv

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.appshortcuts">
<uses-sdk android:minSdkVersion="25" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="@string/app_name"
android:icon="@drawable/app"
android:resizeableActivity="true">
<activity android:name="com.example.android.appshortcuts.Main">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity>
<receiver android:name="com.example.android.appshortcuts.MyReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
</application>
</manifest>

View File

@@ -0,0 +1,13 @@
page.tags="AppShortcuts"
sample.group=System
@jd:body
<p>
This sample demonstrates how to use the Launcher Shortcuts API introduced in API 25.
This API allows an application to define a set of Intents which are displayed as
when a user long-presses on the app's launcher icon. Examples are given for
registering both links both statically in XML, as well as dynamically at runtime.
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<LinearLayout
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:orientation="vertical"
android:paddingLeft="8dip"
>
<TextView
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="16sp"
/>
<TextView
android:id="@+id/line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#444444"
/>
</LinearLayout>
<Button
android:id="@+id/remove"
android:text="@string/remove_shortcut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="visible"
style="@android:style/Widget.Material.Button.Borderless"/>
<Button
android:id="@+id/disable"
android:text="@string/disable_shortcut"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:visibility="visible"
style="@android:style/Widget.Material.Button.Borderless"/>
</LinearLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<Button
android:id="@+id/add"
android:text="@string/add_new_website"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="onAddPressed"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#444444"
android:text="@string/existing_shortcuts"
/>
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
android:enabled="true"
/>
</LinearLayout>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">アプリのショートカットサンプル</string>
<string name="add_new_website">ウェブサイト追加</string>
<string name="add_new_website_short">追加</string>
<string name="existing_shortcuts">既存のショートカット:</string>
<string name="remove_shortcut">削除</string>
<string name="disable_shortcut">無効</string>
<string name="enable_shortcut">有効</string>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">App Shortcuts Sample</string>
<string name="add_new_website">Add New Website</string>
<string name="add_new_website_short">Add Website</string>
<string name="existing_shortcuts">Existing shortcuts:</string>
<string name="remove_shortcut">Remove</string>
<string name="disable_shortcut">Disable</string>
<string name="enable_shortcut">Enable</string>
</resources>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android" >
<shortcut
android:shortcutId="add_website"
android:icon="@drawable/add"
android:shortcutShortLabel="@string/add_new_website_short"
android:shortcutLongLabel="@string/add_new_website"
>
<intent
android:action="com.example.android.appshortcuts.ADD_WEBSITE"
android:targetPackage="com.example.android.appshortcuts"
android:targetClass="com.example.android.appshortcuts.Main"
/>
</shortcut>
</shortcuts>

View File

@@ -0,0 +1,249 @@
/*
* 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.appshortcuts;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
public class Main extends ListActivity implements OnClickListener {
static final String TAG = "ShortcutSample";
private static final String ID_ADD_WEBSITE = "add_website";
private static final String ACTION_ADD_WEBSITE =
"com.example.android.shortcutsample.ADD_WEBSITE";
private MyAdapter mAdapter;
private ShortcutHelper mHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mHelper = new ShortcutHelper(this);
mHelper.maybeRestoreAllDynamicShortcuts();
mHelper.refreshShortcuts(/*force=*/ false);
if (ACTION_ADD_WEBSITE.equals(getIntent().getAction())) {
// Invoked via the manifest shortcut.
addWebSite();
}
mAdapter = new MyAdapter(this.getApplicationContext());
setListAdapter(mAdapter);
}
@Override
protected void onResume() {
super.onResume();
refreshList();
}
/**
* Handle the add button.
*/
public void onAddPressed(View v) {
addWebSite();
}
private void addWebSite() {
Log.i(TAG, "addWebSite");
// This is important. This allows the launcher to build a prediction model.
mHelper.reportShortcutUsed(ID_ADD_WEBSITE);
final EditText editUri = new EditText(this);
editUri.setHint("http://www.android.com/");
editUri.setInputType(EditorInfo.TYPE_TEXT_VARIATION_URI);
new AlertDialog.Builder(this)
.setTitle("Add new website")
.setMessage("Type URL of a website")
.setView(editUri)
.setPositiveButton("Add", (dialog, whichButton) -> {
final String url = editUri.getText().toString().trim();
if (url.length() > 0) {
addUriAsync(url);
}
})
.show();
}
private void addUriAsync(String uri) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
mHelper.addWebSiteShortcut(uri);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
refreshList();
}
}.execute();
}
private void refreshList() {
mAdapter.setShortcuts(mHelper.getShortcuts());
}
@Override
public void onClick(View v) {
final ShortcutInfo shortcut = (ShortcutInfo) ((View) v.getParent()).getTag();
switch (v.getId()) {
case R.id.disable:
if (shortcut.isEnabled()) {
mHelper.disableShortcut(shortcut);
} else {
mHelper.enableShortcut(shortcut);
}
refreshList();
break;
case R.id.remove:
mHelper.removeShortcut(shortcut);
refreshList();
break;
}
}
private static final List<ShortcutInfo> EMPTY_LIST = new ArrayList<>();
private String getType(ShortcutInfo shortcut) {
final StringBuilder sb = new StringBuilder();
String sep = "";
if (shortcut.isDynamic()) {
sb.append(sep);
sb.append("Dynamic");
sep = ", ";
}
if (shortcut.isPinned()) {
sb.append(sep);
sb.append("Pinned");
sep = ", ";
}
if (!shortcut.isEnabled()) {
sb.append(sep);
sb.append("Disabled");
sep = ", ";
}
return sb.toString();
}
private class MyAdapter extends BaseAdapter {
private final Context mContext;
private final LayoutInflater mInflater;
private List<ShortcutInfo> mList = EMPTY_LIST;
public MyAdapter(Context context) {
mContext = context;
mInflater = mContext.getSystemService(LayoutInflater.class);
}
@Override
public int getCount() {
return mList.size();
}
@Override
public Object getItem(int position) {
return mList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean areAllItemsEnabled() {
return true;
}
@Override
public boolean isEnabled(int position) {
return true;
}
public void setShortcuts(List<ShortcutInfo> list) {
mList = list;
notifyDataSetChanged();
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final View view;
if (convertView != null) {
view = convertView;
} else {
view = mInflater.inflate(R.layout.list_item, null);
}
bindView(view, position, mList.get(position));
return view;
}
public void bindView(View view, int position, ShortcutInfo shortcut) {
view.setTag(shortcut);
final TextView line1 = (TextView) view.findViewById(R.id.line1);
final TextView line2 = (TextView) view.findViewById(R.id.line2);
line1.setText(shortcut.getLongLabel());
line2.setText(getType(shortcut));
final Button remove = (Button) view.findViewById(R.id.remove);
final Button disable = (Button) view.findViewById(R.id.disable);
disable.setText(
shortcut.isEnabled() ? R.string.disable_shortcut : R.string.enable_shortcut);
remove.setOnClickListener(Main.this);
disable.setOnClickListener(Main.this);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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.appshortcuts;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class MyReceiver extends BroadcastReceiver {
private static final String TAG = Main.TAG;
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive: " + intent);
if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
// Refresh all shortcut to update the labels.
// (Right now shortcut labels don't contain localized strings though.)
new ShortcutHelper(context).refreshShortcuts(/*force=*/ true);
}
}
}

View File

@@ -0,0 +1,242 @@
/*
* 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.appshortcuts;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PersistableBundle;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.function.BooleanSupplier;
public class ShortcutHelper {
private static final String TAG = Main.TAG;
private static final String EXTRA_LAST_REFRESH =
"com.example.android.shortcutsample.EXTRA_LAST_REFRESH";
private static final long REFRESH_INTERVAL_MS = 60 * 60 * 1000;
private final Context mContext;
private final ShortcutManager mShortcutManager;
public ShortcutHelper(Context context) {
mContext = context;
mShortcutManager = mContext.getSystemService(ShortcutManager.class);
}
public void maybeRestoreAllDynamicShortcuts() {
if (mShortcutManager.getDynamicShortcuts().size() == 0) {
// NOTE: If this application is always supposed to have dynamic shortcuts, then publish
// them here.
// Note when an application is "restored" on a new device, all dynamic shortcuts
// will *not* be restored but the pinned shortcuts *will*.
}
}
public void reportShortcutUsed(String id) {
mShortcutManager.reportShortcutUsed(id);
}
/**
* Use this when interacting with ShortcutManager to show consistent error messages.
*/
private void callShortcutManager(BooleanSupplier r) {
try {
if (!r.getAsBoolean()) {
Utils.showToast(mContext, "Call to ShortcutManager is rate-limited");
}
} catch (Exception e) {
Log.e(TAG, "Caught Exception", e);
Utils.showToast(mContext, "Error while calling ShortcutManager: " + e.toString());
}
}
/**
* Return all mutable shortcuts from this app self.
*/
public List<ShortcutInfo> getShortcuts() {
// Load mutable dynamic shortcuts and pinned shortcuts and put them into a single list
// removing duplicates.
final List<ShortcutInfo> ret = new ArrayList<>();
final HashSet<String> seenKeys = new HashSet<>();
// Check existing shortcuts shortcuts
for (ShortcutInfo shortcut : mShortcutManager.getDynamicShortcuts()) {
if (!shortcut.isImmutable()) {
ret.add(shortcut);
seenKeys.add(shortcut.getId());
}
}
for (ShortcutInfo shortcut : mShortcutManager.getPinnedShortcuts()) {
if (!shortcut.isImmutable() && !seenKeys.contains(shortcut.getId())) {
ret.add(shortcut);
seenKeys.add(shortcut.getId());
}
}
return ret;
}
/**
* Called when the activity starts. Looks for shortcuts that have been pushed and refreshes
* them (but the refresh part isn't implemented yet...).
*/
public void refreshShortcuts(boolean force) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Log.i(TAG, "refreshingShortcuts...");
final long now = System.currentTimeMillis();
final long staleThreshold = force ? now : now - REFRESH_INTERVAL_MS;
// Check all existing dynamic and pinned shortcut, and if their last refresh
// time is older than a certain threshold, update them.
final List<ShortcutInfo> updateList = new ArrayList<>();
for (ShortcutInfo shortcut : getShortcuts()) {
if (shortcut.isImmutable()) {
continue;
}
final PersistableBundle extras = shortcut.getExtras();
if (extras != null && extras.getLong(EXTRA_LAST_REFRESH) >= staleThreshold) {
// Shortcut still fresh.
continue;
}
Log.i(TAG, "Refreshing shortcut: " + shortcut.getId());
final ShortcutInfo.Builder b = new ShortcutInfo.Builder(
mContext, shortcut.getId());
setSiteInformation(b, shortcut.getIntent().getData());
setExtras(b);
updateList.add(b.build());
}
// Call update.
if (updateList.size() > 0) {
callShortcutManager(() -> mShortcutManager.updateShortcuts(updateList));
}
return null;
}
}.execute();
}
private ShortcutInfo createShortcutForUrl(String urlAsString) {
Log.i(TAG, "createShortcutForUrl: " + urlAsString);
final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mContext, urlAsString);
final Uri uri = Uri.parse(urlAsString);
b.setIntent(new Intent(Intent.ACTION_VIEW, uri));
setSiteInformation(b, uri);
setExtras(b);
return b.build();
}
private ShortcutInfo.Builder setSiteInformation(ShortcutInfo.Builder b, Uri uri) {
// TODO Get the actual site <title> and use it.
// TODO Set the current locale to accept-language to get localized title.
b.setShortLabel(uri.getHost());
b.setLongLabel(uri.toString());
Bitmap bmp = fetchFavicon(uri);
if (bmp != null) {
b.setIcon(Icon.createWithBitmap(bmp));
} else {
b.setIcon(Icon.createWithResource(mContext, R.drawable.link));
}
return b;
}
private ShortcutInfo.Builder setExtras(ShortcutInfo.Builder b) {
final PersistableBundle extras = new PersistableBundle();
extras.putLong(EXTRA_LAST_REFRESH, System.currentTimeMillis());
b.setExtras(extras);
return b;
}
private String normalizeUrl(String urlAsString) {
if (urlAsString.startsWith("http://") || urlAsString.startsWith("https://")) {
return urlAsString;
} else {
return "http://" + urlAsString;
}
}
public void addWebSiteShortcut(String urlAsString) {
final String uriFinal = urlAsString;
callShortcutManager(() -> {
final ShortcutInfo shortcut = createShortcutForUrl(normalizeUrl(uriFinal));
return mShortcutManager.addDynamicShortcuts(Arrays.asList(shortcut));
});
}
public void removeShortcut(ShortcutInfo shortcut) {
mShortcutManager.removeDynamicShortcuts(Arrays.asList(shortcut.getId()));
}
public void disableShortcut(ShortcutInfo shortcut) {
mShortcutManager.disableShortcuts(Arrays.asList(shortcut.getId()));
}
public void enableShortcut(ShortcutInfo shortcut) {
mShortcutManager.enableShortcuts(Arrays.asList(shortcut.getId()));
}
private Bitmap fetchFavicon(Uri uri) {
final Uri iconUri = uri.buildUpon().path("favicon.ico").build();
Log.i(TAG, "Fetching favicon from: " + iconUri);
InputStream is = null;
BufferedInputStream bis = null;
try
{
URLConnection conn = new URL(iconUri.toString()).openConnection();
conn.connect();
is = conn.getInputStream();
bis = new BufferedInputStream(is, 8192);
return BitmapFactory.decodeStream(bis);
} catch (IOException e) {
Log.w(TAG, "Failed to fetch favicon from " + iconUri, e);
return null;
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.appshortcuts;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
public class Utils {
private Utils() {
}
public static void showToast(Context context, String message) {
new Handler(Looper.getMainLooper()).post(() -> {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
});
}
}

View File

@@ -0,0 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.commitcontent.app">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,11 @@
page.tags="CommitContentSampleApp"
sample.group=Input
@jd:body
<p>
This sample demonstrates how to write an application which accepts rich content
(such as images) sent from a keyboard using the Commit Content API.
</p>

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<WebView
android:id="@+id/commit_content_webview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#77000000"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fadeScrollbars="false"
android:scrollbars="horizontal">
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TableRow>
<TextView
android:layout_column="1"
android:gravity="end"
android:padding="3dip"
android:text="MIME"
android:textColor="@android:color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/text_commit_content_mime_types"
android:padding="3dip"
android:textColor="@android:color/white" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:gravity="end"
android:padding="3dip"
android:text="Label"
android:textColor="@android:color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/text_commit_content_label"
android:padding="3dip"
android:textColor="@android:color/white" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:gravity="end"
android:padding="3dip"
android:text="URI"
android:textColor="@android:color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/text_commit_content_content_uri"
android:padding="3dip"
android:textColor="@android:color/white" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:gravity="end"
android:padding="3dip"
android:text="Link"
android:textColor="@android:color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/text_commit_content_link_uri"
android:padding="3dip"
android:textColor="@android:color/white" />
</TableRow>
<TableRow>
<TextView
android:layout_column="1"
android:gravity="end"
android:padding="3dip"
android:text="Flags"
android:textColor="@android:color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/text_commit_content_link_flags"
android:padding="3dip"
android:textColor="@android:color/white" />
</TableRow>
</TableLayout>
</HorizontalScrollView>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fadeScrollbars="false"
android:scrollbars="vertical">
<LinearLayout
android:id="@+id/commit_content_sample_edit_boxes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
</ScrollView>
</LinearLayout>
</FrameLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">CommitContentSampleApp</string>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View File

@@ -0,0 +1,250 @@
/*
* 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.app;
import android.support.v13.view.inputmethod.EditorInfoCompat;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.app.Activity;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Log;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.webkit.WebView;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Arrays;
public class MainActivity extends Activity {
private static final String INPUT_CONTENT_INFO_KEY = "COMMIT_CONTENT_INPUT_CONTENT_INFO";
private static final String COMMIT_CONTENT_FLAGS_KEY = "COMMIT_CONTENT_FLAGS";
private static String TAG = "CommitContentSupport";
private WebView mWebView;
private TextView mLabel;
private TextView mContentUri;
private TextView mLinkUri;
private TextView mMimeTypes;
private TextView mFlags;
private InputContentInfoCompat mCurrentInputContentInfo;
private int mCurrentFlags;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.commit_content);
final LinearLayout layout =
(LinearLayout) findViewById(R.id.commit_content_sample_edit_boxes);
// This declares that the IME cannot commit any content with
// InputConnectionCompat#commitContent().
layout.addView(createEditTextWithContentMimeTypes(null));
// This declares that the IME can commit contents with
// InputConnectionCompat#commitContent() if they match "image/gif".
layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/gif"}));
// This declares that the IME can commit contents with
// InputConnectionCompat#commitContent() if they match "image/png".
layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/png"}));
// This declares that the IME can commit contents with
// InputConnectionCompat#commitContent() if they match "image/jpeg".
layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/jpeg"}));
// This declares that the IME can commit contents with
// InputConnectionCompat#commitContent() if they match "image/webp".
layout.addView(createEditTextWithContentMimeTypes(new String[]{"image/webp"}));
// This declares that the IME can commit contents with
// InputConnectionCompat#commitContent() if they match "image/png", "image/gif",
// "image/jpeg", or "image/webp".
layout.addView(createEditTextWithContentMimeTypes(
new String[]{"image/png", "image/gif", "image/jpeg", "image/webp"}));
mWebView = (WebView) findViewById(R.id.commit_content_webview);
mMimeTypes = (TextView) findViewById(R.id.text_commit_content_mime_types);
mLabel = (TextView) findViewById(R.id.text_commit_content_label);
mContentUri = (TextView) findViewById(R.id.text_commit_content_content_uri);
mLinkUri = (TextView) findViewById(R.id.text_commit_content_link_uri);
mFlags = (TextView) findViewById(R.id.text_commit_content_link_flags);
if (savedInstanceState != null) {
final InputContentInfoCompat previousInputContentInfo = InputContentInfoCompat.wrap(
savedInstanceState.getParcelable(INPUT_CONTENT_INFO_KEY));
final int previousFlags = savedInstanceState.getInt(COMMIT_CONTENT_FLAGS_KEY);
if (previousInputContentInfo != null) {
onCommitContentInternal(previousInputContentInfo, previousFlags);
}
}
}
private boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags,
Bundle opts, String[] contentMimeTypes) {
// Clear the temporary permission (if any). See below about why we do this here.
try {
if (mCurrentInputContentInfo != null) {
mCurrentInputContentInfo.releasePermission();
}
} catch (Exception e) {
Log.e(TAG, "InputContentInfoCompat#releasePermission() failed.", e);
} finally {
mCurrentInputContentInfo = null;
}
mWebView.loadUrl("about:blank");
mMimeTypes.setText("");
mContentUri.setText("");
mLabel.setText("");
mLinkUri.setText("");
mFlags.setText("");
boolean supported = false;
for (final String mimeType : contentMimeTypes) {
if (inputContentInfo.getDescription().hasMimeType(mimeType)) {
supported = true;
break;
}
}
if (!supported) {
return false;
}
return onCommitContentInternal(inputContentInfo, flags);
}
private boolean onCommitContentInternal(InputContentInfoCompat inputContentInfo, int flags) {
if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
Log.e(TAG, "InputContentInfoCompat#requestPermission() failed.", e);
return false;
}
}
mMimeTypes.setText(
Arrays.toString(inputContentInfo.getDescription().filterMimeTypes("*/*")));
mContentUri.setText(inputContentInfo.getContentUri().toString());
mLabel.setText(inputContentInfo.getDescription().getLabel());
Uri linkUri = inputContentInfo.getLinkUri();
mLinkUri.setText(linkUri != null ? linkUri.toString() : "null");
mFlags.setText(flagsToString(flags));
mWebView.loadUrl(inputContentInfo.getContentUri().toString());
mWebView.setBackgroundColor(Color.TRANSPARENT);
// Due to the asynchronous nature of WebView, it is a bit too early to call
// inputContentInfo.releasePermission() here. Hence we call IC#releasePermission() when this
// method is called next time. Note that calling IC#releasePermission() is just to be a
// good citizen. Even if we failed to call that method, the system would eventually revoke
// the permission sometime after inputContentInfo object gets garbage-collected.
mCurrentInputContentInfo = inputContentInfo;
mCurrentFlags = flags;
return true;
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
if (mCurrentInputContentInfo != null) {
savedInstanceState.putParcelable(INPUT_CONTENT_INFO_KEY,
(Parcelable) mCurrentInputContentInfo.unwrap());
savedInstanceState.putInt(COMMIT_CONTENT_FLAGS_KEY, mCurrentFlags);
}
mCurrentInputContentInfo = null;
mCurrentFlags = 0;
super.onSaveInstanceState(savedInstanceState);
}
/**
* Creates a new instance of {@link EditText} that is configured to specify the given content
* MIME types to EditorInfo#contentMimeTypes so that developers can locally test how the current
* input method behaves for such content MIME types.
*
* @param contentMimeTypes A {@link String} array that indicates the supported content MIME
* types
* @return a new instance of {@link EditText}, which specifies EditorInfo#contentMimeTypes with
* the given content MIME types
*/
private EditText createEditTextWithContentMimeTypes(String[] contentMimeTypes) {
final CharSequence hintText;
final String[] mimeTypes; // our own copy of contentMimeTypes.
if (contentMimeTypes == null || contentMimeTypes.length == 0) {
hintText = "MIME: []";
mimeTypes = new String[0];
} else {
hintText = "MIME: " + Arrays.toString(contentMimeTypes);
mimeTypes = Arrays.copyOf(contentMimeTypes, contentMimeTypes.length);
}
EditText exitText = new EditText(this) {
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
final InputConnection ic = super.onCreateInputConnection(editorInfo);
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
final InputConnectionCompat.OnCommitContentListener callback =
new InputConnectionCompat.OnCommitContentListener() {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo,
int flags, Bundle opts) {
return MainActivity.this.onCommitContent(
inputContentInfo, flags, opts, mimeTypes);
}
};
return InputConnectionCompat.createWrapper(ic, editorInfo, callback);
}
};
exitText.setHint(hintText);
exitText.setTextColor(Color.WHITE);
exitText.setHintTextColor(Color.WHITE);
return exitText;
}
/**
* Converts {@code flags} specified in {@link InputConnectionCompat#commitContent(
* InputConnection, EditorInfo, InputContentInfoCompat, int, Bundle)} to a human readable
* string.
*
* @param flags the 2nd parameter of
* {@link InputConnectionCompat#commitContent(InputConnection, EditorInfo,
* InputContentInfoCompat, int, Bundle)}
* @return a human readable string that corresponds to the given {@code flags}
*/
private static String flagsToString(int flags) {
final ArrayList<String> tokens = new ArrayList<>();
if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
tokens.add("INPUT_CONTENT_GRANT_READ_URI_PERMISSION");
flags &= ~InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION;
}
if (flags != 0) {
tokens.add("0x" + Integer.toHexString(flags));
}
return TextUtils.join(" | ", tokens);
}
}

View File

@@ -0,0 +1,32 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.commitcontent.ime">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<service
android:name="com.example.android.commitcontent.ime.ImageKeyboard"
android:permission="android.permission.BIND_INPUT_METHOD">
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data android:name="android.view.im" android:resource="@xml/method" />
</service>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.android.commitcontent.ime.inputcontent"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,11 @@
page.tags="CommitContentSampleIME"
sample.group=Input
@jd:body
<p>
This sample demonstrates how to write an keyboard which sends rich content
(such as images) to text fields using the Commit Content API.
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* 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.
*/
-->
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* 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.
*/
-->
<resources>
<string name="app_name">CommitContentSampleIme</string>
</resources>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* 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.
*/
-->
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* 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.
*/
-->
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
</paths>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* 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.
*/
-->
<input-method xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -0,0 +1,286 @@
/*
* 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.supportv13.sampleime.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;
}
}
}