1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.example.android.appshortcuts; 17 18 import android.content.Context; 19 import android.content.Intent; 20 import android.content.pm.ShortcutInfo; 21 import android.content.pm.ShortcutManager; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapFactory; 24 import android.graphics.drawable.Icon; 25 import android.net.Uri; 26 import android.os.AsyncTask; 27 import android.os.PersistableBundle; 28 import android.util.Log; 29 30 import java.io.BufferedInputStream; 31 import java.io.IOException; 32 import java.io.InputStream; 33 import java.net.URL; 34 import java.net.URLConnection; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.HashSet; 38 import java.util.List; 39 import java.util.function.BooleanSupplier; 40 41 public class ShortcutHelper { 42 private static final String TAG = Main.TAG; 43 44 private static final String EXTRA_LAST_REFRESH = 45 "com.example.android.shortcutsample.EXTRA_LAST_REFRESH"; 46 47 private static final long REFRESH_INTERVAL_MS = 60 * 60 * 1000; 48 49 private final Context mContext; 50 51 private final ShortcutManager mShortcutManager; 52 ShortcutHelper(Context context)53 public ShortcutHelper(Context context) { 54 mContext = context; 55 mShortcutManager = mContext.getSystemService(ShortcutManager.class); 56 } 57 maybeRestoreAllDynamicShortcuts()58 public void maybeRestoreAllDynamicShortcuts() { 59 if (mShortcutManager.getDynamicShortcuts().size() == 0) { 60 // NOTE: If this application is always supposed to have dynamic shortcuts, then publish 61 // them here. 62 // Note when an application is "restored" on a new device, all dynamic shortcuts 63 // will *not* be restored but the pinned shortcuts *will*. 64 } 65 } 66 reportShortcutUsed(String id)67 public void reportShortcutUsed(String id) { 68 mShortcutManager.reportShortcutUsed(id); 69 } 70 71 /** 72 * Use this when interacting with ShortcutManager to show consistent error messages. 73 */ callShortcutManager(BooleanSupplier r)74 private void callShortcutManager(BooleanSupplier r) { 75 try { 76 if (!r.getAsBoolean()) { 77 Utils.showToast(mContext, "Call to ShortcutManager is rate-limited"); 78 } 79 } catch (Exception e) { 80 Log.e(TAG, "Caught Exception", e); 81 Utils.showToast(mContext, "Error while calling ShortcutManager: " + e.toString()); 82 } 83 } 84 85 /** 86 * Return all mutable shortcuts from this app self. 87 */ getShortcuts()88 public List<ShortcutInfo> getShortcuts() { 89 // Load mutable dynamic shortcuts and pinned shortcuts and put them into a single list 90 // removing duplicates. 91 92 final List<ShortcutInfo> ret = new ArrayList<>(); 93 final HashSet<String> seenKeys = new HashSet<>(); 94 95 // Check existing shortcuts shortcuts 96 for (ShortcutInfo shortcut : mShortcutManager.getDynamicShortcuts()) { 97 if (!shortcut.isImmutable()) { 98 ret.add(shortcut); 99 seenKeys.add(shortcut.getId()); 100 } 101 } 102 for (ShortcutInfo shortcut : mShortcutManager.getPinnedShortcuts()) { 103 if (!shortcut.isImmutable() && !seenKeys.contains(shortcut.getId())) { 104 ret.add(shortcut); 105 seenKeys.add(shortcut.getId()); 106 } 107 } 108 return ret; 109 } 110 111 /** 112 * Called when the activity starts. Looks for shortcuts that have been pushed and refreshes 113 * them (but the refresh part isn't implemented yet...). 114 */ refreshShortcuts(boolean force)115 public void refreshShortcuts(boolean force) { 116 new AsyncTask<Void, Void, Void>() { 117 @Override 118 protected Void doInBackground(Void... params) { 119 Log.i(TAG, "refreshingShortcuts..."); 120 121 final long now = System.currentTimeMillis(); 122 final long staleThreshold = force ? now : now - REFRESH_INTERVAL_MS; 123 124 // Check all existing dynamic and pinned shortcut, and if their last refresh 125 // time is older than a certain threshold, update them. 126 127 final List<ShortcutInfo> updateList = new ArrayList<>(); 128 129 for (ShortcutInfo shortcut : getShortcuts()) { 130 if (shortcut.isImmutable()) { 131 continue; 132 } 133 134 final PersistableBundle extras = shortcut.getExtras(); 135 if (extras != null && extras.getLong(EXTRA_LAST_REFRESH) >= staleThreshold) { 136 // Shortcut still fresh. 137 continue; 138 } 139 Log.i(TAG, "Refreshing shortcut: " + shortcut.getId()); 140 141 final ShortcutInfo.Builder b = new ShortcutInfo.Builder( 142 mContext, shortcut.getId()); 143 144 setSiteInformation(b, shortcut.getIntent().getData()); 145 setExtras(b); 146 147 updateList.add(b.build()); 148 } 149 // Call update. 150 if (updateList.size() > 0) { 151 callShortcutManager(() -> mShortcutManager.updateShortcuts(updateList)); 152 } 153 154 return null; 155 } 156 }.execute(); 157 } 158 createShortcutForUrl(String urlAsString)159 private ShortcutInfo createShortcutForUrl(String urlAsString) { 160 Log.i(TAG, "createShortcutForUrl: " + urlAsString); 161 162 final ShortcutInfo.Builder b = new ShortcutInfo.Builder(mContext, urlAsString); 163 164 final Uri uri = Uri.parse(urlAsString); 165 b.setIntent(new Intent(Intent.ACTION_VIEW, uri)); 166 167 setSiteInformation(b, uri); 168 setExtras(b); 169 170 return b.build(); 171 } 172 setSiteInformation(ShortcutInfo.Builder b, Uri uri)173 private ShortcutInfo.Builder setSiteInformation(ShortcutInfo.Builder b, Uri uri) { 174 // TODO Get the actual site <title> and use it. 175 // TODO Set the current locale to accept-language to get localized title. 176 b.setShortLabel(uri.getHost()); 177 b.setLongLabel(uri.toString()); 178 179 Bitmap bmp = fetchFavicon(uri); 180 if (bmp != null) { 181 b.setIcon(Icon.createWithBitmap(bmp)); 182 } else { 183 b.setIcon(Icon.createWithResource(mContext, R.drawable.link)); 184 } 185 186 return b; 187 } 188 setExtras(ShortcutInfo.Builder b)189 private ShortcutInfo.Builder setExtras(ShortcutInfo.Builder b) { 190 final PersistableBundle extras = new PersistableBundle(); 191 extras.putLong(EXTRA_LAST_REFRESH, System.currentTimeMillis()); 192 b.setExtras(extras); 193 return b; 194 } 195 normalizeUrl(String urlAsString)196 private String normalizeUrl(String urlAsString) { 197 if (urlAsString.startsWith("http://") || urlAsString.startsWith("https://")) { 198 return urlAsString; 199 } else { 200 return "http://" + urlAsString; 201 } 202 } 203 addWebSiteShortcut(String urlAsString)204 public void addWebSiteShortcut(String urlAsString) { 205 final String uriFinal = urlAsString; 206 callShortcutManager(() -> { 207 final ShortcutInfo shortcut = createShortcutForUrl(normalizeUrl(uriFinal)); 208 return mShortcutManager.addDynamicShortcuts(Arrays.asList(shortcut)); 209 }); 210 } 211 removeShortcut(ShortcutInfo shortcut)212 public void removeShortcut(ShortcutInfo shortcut) { 213 mShortcutManager.removeDynamicShortcuts(Arrays.asList(shortcut.getId())); 214 } 215 disableShortcut(ShortcutInfo shortcut)216 public void disableShortcut(ShortcutInfo shortcut) { 217 mShortcutManager.disableShortcuts(Arrays.asList(shortcut.getId())); 218 } 219 enableShortcut(ShortcutInfo shortcut)220 public void enableShortcut(ShortcutInfo shortcut) { 221 mShortcutManager.enableShortcuts(Arrays.asList(shortcut.getId())); 222 } 223 fetchFavicon(Uri uri)224 private Bitmap fetchFavicon(Uri uri) { 225 final Uri iconUri = uri.buildUpon().path("favicon.ico").build(); 226 Log.i(TAG, "Fetching favicon from: " + iconUri); 227 228 InputStream is = null; 229 BufferedInputStream bis = null; 230 try 231 { 232 URLConnection conn = new URL(iconUri.toString()).openConnection(); 233 conn.connect(); 234 is = conn.getInputStream(); 235 bis = new BufferedInputStream(is, 8192); 236 return BitmapFactory.decodeStream(bis); 237 } catch (IOException e) { 238 Log.w(TAG, "Failed to fetch favicon from " + iconUri, e); 239 return null; 240 } 241 } 242 } 243