diff options
author | Trevor Johns <trevorjohns@google.com> | 2016-10-18 03:15:51 -0700 |
---|---|---|
committer | Trevor Johns <trevorjohns@google.com> | 2016-10-18 03:19:42 -0700 |
commit | 0af1e772c478d145b554be76fbc00dd83548ec3b (patch) | |
tree | b337a73da05dbdb889cb4b16940ae5c2c50e79d7 | |
parent | bf0ff8fe8a491ac6264be7d9aa4ed29d8d58b8e8 (diff) | |
download | android_development-0af1e772c478d145b554be76fbc00dd83548ec3b.tar.gz android_development-0af1e772c478d145b554be76fbc00dd83548ec3b.tar.bz2 android_development-0af1e772c478d145b554be76fbc00dd83548ec3b.zip |
docs: Add new samples for N MR1
- AppShortcuts
- CommitContentSampleApp
- CommitContentSampleIME
Change-Id: I3cefc134839f944b1c0c5efc943fb779c7e7ee70
43 files changed, 1683 insertions, 0 deletions
diff --git a/build/sdk.atree b/build/sdk.atree index 19917d57e..8843d73f1 100644 --- a/build/sdk.atree +++ b/build/sdk.atree @@ -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 diff --git a/samples/browseable/AppShortcuts/AndroidManifest.xml b/samples/browseable/AppShortcuts/AndroidManifest.xml new file mode 100644 index 000000000..9dfc7a3a8 --- /dev/null +++ b/samples/browseable/AppShortcuts/AndroidManifest.xml @@ -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> diff --git a/samples/browseable/AppShortcuts/_index.jd b/samples/browseable/AppShortcuts/_index.jd new file mode 100644 index 000000000..2d3ed6202 --- /dev/null +++ b/samples/browseable/AppShortcuts/_index.jd @@ -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> diff --git a/samples/browseable/AppShortcuts/res/drawable-nodpi/add.png b/samples/browseable/AppShortcuts/res/drawable-nodpi/add.png Binary files differnew file mode 100644 index 000000000..86a2ebd9e --- /dev/null +++ b/samples/browseable/AppShortcuts/res/drawable-nodpi/add.png diff --git a/samples/browseable/AppShortcuts/res/drawable-nodpi/app.png b/samples/browseable/AppShortcuts/res/drawable-nodpi/app.png Binary files differnew file mode 100644 index 000000000..39ca2f973 --- /dev/null +++ b/samples/browseable/AppShortcuts/res/drawable-nodpi/app.png diff --git a/samples/browseable/AppShortcuts/res/drawable-nodpi/link.png b/samples/browseable/AppShortcuts/res/drawable-nodpi/link.png Binary files differnew file mode 100644 index 000000000..c4297c48b --- /dev/null +++ b/samples/browseable/AppShortcuts/res/drawable-nodpi/link.png diff --git a/samples/browseable/AppShortcuts/res/layout/list_item.xml b/samples/browseable/AppShortcuts/res/layout/list_item.xml new file mode 100644 index 000000000..f7129a892 --- /dev/null +++ b/samples/browseable/AppShortcuts/res/layout/list_item.xml @@ -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>
\ No newline at end of file diff --git a/samples/browseable/AppShortcuts/res/layout/main.xml b/samples/browseable/AppShortcuts/res/layout/main.xml new file mode 100644 index 000000000..2d87c0760 --- /dev/null +++ b/samples/browseable/AppShortcuts/res/layout/main.xml @@ -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> + + diff --git a/samples/browseable/AppShortcuts/res/values-ja/strings.xml b/samples/browseable/AppShortcuts/res/values-ja/strings.xml new file mode 100644 index 000000000..d35d3343d --- /dev/null +++ b/samples/browseable/AppShortcuts/res/values-ja/strings.xml @@ -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> diff --git a/samples/browseable/AppShortcuts/res/values/strings.xml b/samples/browseable/AppShortcuts/res/values/strings.xml new file mode 100644 index 000000000..269bd933a --- /dev/null +++ b/samples/browseable/AppShortcuts/res/values/strings.xml @@ -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> diff --git a/samples/browseable/AppShortcuts/res/xml/shortcuts.xml b/samples/browseable/AppShortcuts/res/xml/shortcuts.xml new file mode 100644 index 000000000..1430f431c --- /dev/null +++ b/samples/browseable/AppShortcuts/res/xml/shortcuts.xml @@ -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> diff --git a/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/Main.java b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/Main.java new file mode 100644 index 000000000..22b29b57a --- /dev/null +++ b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/Main.java @@ -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); + } + } +} diff --git a/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/MyReceiver.java b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/MyReceiver.java new file mode 100644 index 000000000..adb95326c --- /dev/null +++ b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/MyReceiver.java @@ -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); + } + } +} diff --git a/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/ShortcutHelper.java b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/ShortcutHelper.java new file mode 100644 index 000000000..31f61961b --- /dev/null +++ b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/ShortcutHelper.java @@ -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; + } + } +} diff --git a/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/Utils.java b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/Utils.java new file mode 100644 index 000000000..585273479 --- /dev/null +++ b/samples/browseable/AppShortcuts/src/com.example.android.appshortcuts/Utils.java @@ -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(); + }); + } +} diff --git a/samples/browseable/CommitContentSampleApp/AndroidManifest.xml b/samples/browseable/CommitContentSampleApp/AndroidManifest.xml new file mode 100644 index 000000000..16c93b117 --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/AndroidManifest.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleApp/_index.jd b/samples/browseable/CommitContentSampleApp/_index.jd new file mode 100644 index 000000000..9b1785cc4 --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/_index.jd @@ -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> diff --git a/samples/browseable/CommitContentSampleApp/res/layout/commit_content.xml b/samples/browseable/CommitContentSampleApp/res/layout/commit_content.xml new file mode 100644 index 000000000..414df184f --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/layout/commit_content.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleApp/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/CommitContentSampleApp/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..cde69bccc --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/mipmap-hdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleApp/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/CommitContentSampleApp/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..c133a0cbd --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/mipmap-mdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleApp/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/CommitContentSampleApp/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..bfa42f0e7 --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/mipmap-xhdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleApp/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/CommitContentSampleApp/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..324e72cdd --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/mipmap-xxhdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleApp/res/mipmap-xxxhdpi/ic_launcher.png b/samples/browseable/CommitContentSampleApp/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..aee44e138 --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleApp/res/values/colors.xml b/samples/browseable/CommitContentSampleApp/res/values/colors.xml new file mode 100644 index 000000000..3ab3e9cbc --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/values/colors.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleApp/res/values/strings.xml b/samples/browseable/CommitContentSampleApp/res/values/strings.xml new file mode 100644 index 000000000..5bcf8eab3 --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="app_name">CommitContentSampleApp</string> +</resources> diff --git a/samples/browseable/CommitContentSampleApp/res/values/styles.xml b/samples/browseable/CommitContentSampleApp/res/values/styles.xml new file mode 100644 index 000000000..5885930df --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/res/values/styles.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleApp/src/com.example.android.commitcontent.app/MainActivity.java b/samples/browseable/CommitContentSampleApp/src/com.example.android.commitcontent.app/MainActivity.java new file mode 100644 index 000000000..72a5378f4 --- /dev/null +++ b/samples/browseable/CommitContentSampleApp/src/com.example.android.commitcontent.app/MainActivity.java @@ -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); + } + +} diff --git a/samples/browseable/CommitContentSampleIME/AndroidManifest.xml b/samples/browseable/CommitContentSampleIME/AndroidManifest.xml new file mode 100644 index 000000000..f9891ffcb --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/AndroidManifest.xml @@ -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>
\ No newline at end of file diff --git a/samples/browseable/CommitContentSampleIME/_index.jd b/samples/browseable/CommitContentSampleIME/_index.jd new file mode 100644 index 000000000..2885fc1ea --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/_index.jd @@ -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> diff --git a/samples/browseable/CommitContentSampleIME/res/mipmap-hdpi/ic_launcher.png b/samples/browseable/CommitContentSampleIME/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..cde69bccc --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/mipmap-hdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleIME/res/mipmap-mdpi/ic_launcher.png b/samples/browseable/CommitContentSampleIME/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..c133a0cbd --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/mipmap-mdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleIME/res/mipmap-xhdpi/ic_launcher.png b/samples/browseable/CommitContentSampleIME/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..bfa42f0e7 --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/mipmap-xhdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleIME/res/mipmap-xxhdpi/ic_launcher.png b/samples/browseable/CommitContentSampleIME/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..324e72cdd --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/mipmap-xxhdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleIME/res/mipmap-xxxhdpi/ic_launcher.png b/samples/browseable/CommitContentSampleIME/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..aee44e138 --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/samples/browseable/CommitContentSampleIME/res/raw/animated_gif.gif b/samples/browseable/CommitContentSampleIME/res/raw/animated_gif.gif Binary files differnew file mode 100644 index 000000000..51baf1586 --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/raw/animated_gif.gif diff --git a/samples/browseable/CommitContentSampleIME/res/raw/animated_webp.webp b/samples/browseable/CommitContentSampleIME/res/raw/animated_webp.webp Binary files differnew file mode 100644 index 000000000..d753a1b02 --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/raw/animated_webp.webp diff --git a/samples/browseable/CommitContentSampleIME/res/raw/dessert_android.png b/samples/browseable/CommitContentSampleIME/res/raw/dessert_android.png Binary files differnew file mode 100644 index 000000000..2b47c1900 --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/raw/dessert_android.png diff --git a/samples/browseable/CommitContentSampleIME/res/values/colors.xml b/samples/browseable/CommitContentSampleIME/res/values/colors.xml new file mode 100644 index 000000000..576656b91 --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/values/colors.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleIME/res/values/strings.xml b/samples/browseable/CommitContentSampleIME/res/values/strings.xml new file mode 100644 index 000000000..952f8731d --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/values/strings.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleIME/res/values/styles.xml b/samples/browseable/CommitContentSampleIME/res/values/styles.xml new file mode 100644 index 000000000..4655b40de --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/values/styles.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleIME/res/xml/file_paths.xml b/samples/browseable/CommitContentSampleIME/res/xml/file_paths.xml new file mode 100644 index 000000000..63ac52afd --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/xml/file_paths.xml @@ -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> diff --git a/samples/browseable/CommitContentSampleIME/res/xml/method.xml b/samples/browseable/CommitContentSampleIME/res/xml/method.xml new file mode 100644 index 000000000..defe3e31a --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/res/xml/method.xml @@ -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" /> diff --git a/samples/browseable/CommitContentSampleIME/src/com.example.android.commitcontent.ime/ImageKeyboard.java b/samples/browseable/CommitContentSampleIME/src/com.example.android.commitcontent.ime/ImageKeyboard.java new file mode 100644 index 000000000..eb5dae7af --- /dev/null +++ b/samples/browseable/CommitContentSampleIME/src/com.example.android.commitcontent.ime/ImageKeyboard.java @@ -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; + } + } +} |