Home | History | Annotate | Download | only in appshortcuts
      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 
     53     public ShortcutHelper(Context context) {
     54         mContext = context;
     55         mShortcutManager = mContext.getSystemService(ShortcutManager.class);
     56     }
     57 
     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 
     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      */
     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      */
     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      */
    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 
    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 
    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 
    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 
    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 
    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 
    212     public void removeShortcut(ShortcutInfo shortcut) {
    213         mShortcutManager.removeDynamicShortcuts(Arrays.asList(shortcut.getId()));
    214     }
    215 
    216     public void disableShortcut(ShortcutInfo shortcut) {
    217         mShortcutManager.disableShortcuts(Arrays.asList(shortcut.getId()));
    218     }
    219 
    220     public void enableShortcut(ShortcutInfo shortcut) {
    221         mShortcutManager.enableShortcuts(Arrays.asList(shortcut.getId()));
    222     }
    223 
    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