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