ShortcutManager sample application.

This is slightly more practical than ShortuctDemo.

This app allows the user to publish shortcuts to websites.

Bug 30259354

Change-Id: I0e7358aadf016c75c4926595abbe6ce117d69c2d
This commit is contained in:
Makoto Onuki
2016-07-22 15:05:37 -07:00
parent 43790dbaaa
commit 8e1f716acc
14 changed files with 773 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
#
# 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.
#
# We build two apps from the same source
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := ShortcutSample
LOCAL_MODULE_TAGS := samples tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
# TODO Change to 25
LOCAL_SDK_VERSION := current
include $(BUILD_PACKAGE)

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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.shortcutsample">
<uses-sdk android:minSdkVersion="25" />
<application
android:label="@string/app_name"
android:icon="@drawable/app"
android:resizeableActivity="true">
<activity android:name="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="MyReceiver">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 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">Shortcut 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.shortcutsample.ADD_WEBSITE"
android:targetPackage="com.example.android.shortcutsample"
android:targetClass="com.example.android.shortcutsample.Main"
/>
</shortcut>
</shortcuts>

View File

@@ -0,0 +1,234 @@
/*
* 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.shortcutsample;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.content.Context;
import android.content.pm.ShortcutInfo;
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) {
mHelper.addWebSiteShortcut(url);
refreshList();
}
})
.show();
}
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.shortcutsample;
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,212 @@
/*
* 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.shortcutsample;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PersistableBundle;
import android.util.Log;
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());
// TODO Fetch the favicon from the URI and sets to the icon.
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()));
}
}

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.shortcutsample;
import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.os.AsyncTask;
import android.os.PersistableBundle;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class Utils {
private Utils() {
}
public static void showToast(Context context, String message) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
}
}