package com.plugins.shortcuts; import java.security.InvalidParameterException; import java.util.Set; import java.util.ArrayList; import java.util.Iterator; import org.apache.cordova.CallbackContext; import org.apache.cordova.CordovaPlugin; import org.apache.cordova.PluginResult; import org.json.JSONObject; import org.json.JSONArray; import org.json.JSONException; import android.net.Uri; import android.util.Base64; import android.util.Log; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Icon; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager; import android.content.res.Resources; import android.support.v4.content.pm.ShortcutInfoCompat; import android.support.v4.content.pm.ShortcutManagerCompat; import android.support.v4.graphics.drawable.IconCompat; public class ShortcutsPlugin extends CordovaPlugin { private static final String TAG = "ShortcutsPlugin"; private static final String ACTION_SUPPORTS_DYNAMIC = "supportsDynamic"; private static final String ACTION_SUPPORTS_PINNED = "supportsPinned"; private static final String ACTION_SET_DYNAMIC = "setDynamic"; private static final String ACTION_ADD_PINNED = "addPinned"; private static final String ACTION_GET_INTENT = "getIntent"; private static final String ACTION_ON_NEW_INTENT = "onNewIntent"; private CallbackContext onNewIntentCallbackContext = null; @Override public boolean execute( String action, JSONArray args, CallbackContext callbackContext) { try { if (action.equals(ACTION_SUPPORTS_DYNAMIC)) { boolean supported = Build.VERSION.SDK_INT >= 25; callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, supported)); return true; } else if (action.equals(ACTION_SUPPORTS_PINNED)) { boolean supported = this.supportsPinned(); callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, supported)); return true; } else if (action.equals(ACTION_SET_DYNAMIC)) { setDynamicShortcuts(args); callbackContext.success(); return true; } else if (action.equals(ACTION_ADD_PINNED)) { boolean success = addPinnedShortcut(args); if (success) { callbackContext.success(); } else { callbackContext.error("Pinned shortcuts are not supported by the default launcher."); } } else if (action.equals(ACTION_GET_INTENT)) { getIntent(callbackContext); return true; } else if (action.equals(ACTION_ON_NEW_INTENT)) { subscribeOnNewIntent(args, callbackContext); return true; } } catch (Exception e) { Log.e(TAG, "Exception executing Cordova action: " + e.getMessage()); callbackContext.error(e.getMessage()); return true; } return false; } @Override public void onNewIntent( Intent intent ) { try { if (this.onNewIntentCallbackContext != null) { PluginResult result = new PluginResult(PluginResult.Status.OK, buildIntent(intent)); result.setKeepCallback(true); this.onNewIntentCallbackContext.sendPluginResult(result); } } catch (Exception e) { Log.e(TAG, "Exception handling onNewIntent: " + e.getMessage()); } } private boolean supportsPinned() { Context context = this.cordova.getActivity().getApplicationContext(); return ShortcutManagerCompat.isRequestPinShortcutSupported(context); } private void subscribeOnNewIntent( JSONArray args, CallbackContext callbackContext ) throws JSONException { boolean remove = args.optBoolean(0); if (remove) { this.onNewIntentCallbackContext = null; Log.i(TAG, "Removed callback for onNewIntent"); } else { this.onNewIntentCallbackContext = callbackContext; Log.i(TAG, "Added a new callback for onNewIntent"); } } private void getIntent(CallbackContext callbackContext) throws JSONException { Intent intent = this.cordova.getActivity().getIntent(); PluginResult result = new PluginResult(PluginResult.Status.OK, buildIntent(intent)); callbackContext.sendPluginResult(result); } private JSONObject buildIntent( Intent intent ) throws JSONException { JSONObject jsonIntent = new JSONObject(); jsonIntent.put("action", intent.getAction()); jsonIntent.put("flags", intent.getFlags()); Set<String> categories = intent.getCategories(); if (categories != null) { jsonIntent.put("categories", new JSONArray(categories)); } Uri data = intent.getData(); if (data != null) { jsonIntent.put("data", data.toString()); } Bundle extras = intent.getExtras(); if (extras != null) { JSONObject jsonExtras = new JSONObject(); jsonIntent.put("extras", jsonExtras); Iterator<String> keys = extras.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); Object value = extras.get(key); if (value instanceof Boolean) { jsonExtras.put(key, (Boolean)value); } else if (value instanceof Integer) { jsonExtras.put(key, (Integer)value); } else if (value instanceof Long) { jsonExtras.put(key, (Long)value); } else if (value instanceof Float) { jsonExtras.put(key, (Float)value); } else if (value instanceof Double) { jsonExtras.put(key, (Double)value); } else { jsonExtras.put(key, value.toString()); } } } return jsonIntent; } private Intent parseIntent( JSONObject jsonIntent ) throws JSONException { Intent intent = new Intent(); String activityClass = jsonIntent.optString( "activityClass", this.cordova.getActivity().getClass().getName()); String activityPackage = jsonIntent.optString( "activityPackage", this.cordova.getActivity().getPackageName()); intent.setClassName(activityPackage, activityClass); String action = jsonIntent.optString("action", Intent.ACTION_VIEW); if (action.indexOf('.') < 0) { action = activityPackage + '.' + action; } Log.i(TAG, "Creating new intent with action: " + action); intent.setAction(action); int flags = jsonIntent.optInt("flags", Intent.FLAG_ACTIVITY_NEW_TASK + Intent.FLAG_ACTIVITY_CLEAR_TOP); intent.setFlags(flags); // TODO: Support passing different flags JSONArray jsonCategories = jsonIntent.optJSONArray("categories"); if (jsonCategories != null) { int count = jsonCategories.length(); for (int i = 0; i < count; ++i) { String category = jsonCategories.getString(i); if (category.indexOf('.') < 0) { category = activityPackage + '.' + category; } intent.addCategory(category); } } String data = jsonIntent.optString("data"); if (data.length() > 0) { intent.setData(Uri.parse(data)); } JSONObject extras = jsonIntent.optJSONObject("extras"); if (extras != null) { Iterator<String> keys = extras.keys(); while (keys.hasNext()) { String key = keys.next(); Object value = extras.get(key); if (value != null) { if (key.indexOf('.') < 0) { key = activityPackage + "." + key; } if (value instanceof Boolean) { intent.putExtra(key, (Boolean)value); } else if (value instanceof Integer) { intent.putExtra(key, (Integer)value); } else if (value instanceof Long) { intent.putExtra(key, (Long)value); } else if (value instanceof Float) { intent.putExtra(key, (Float)value); } else if (value instanceof Double) { intent.putExtra(key, (Double)value); } else { intent.putExtra(key, value.toString()); } } } } return intent; } private ShortcutInfo buildDynamicShortcut( JSONObject jsonShortcut) throws PackageManager.NameNotFoundException, JSONException { if (jsonShortcut == null) { throw new InvalidParameterException("Shortcut object cannot be null"); } Context context = this.cordova.getActivity().getApplicationContext(); String shortcutId = jsonShortcut.optString("id"); if (shortcutId.length() == 0) { throw new InvalidParameterException("A value for 'id' is required"); } ShortcutInfo.Builder builder = new ShortcutInfo.Builder(context, shortcutId); String shortLabel = jsonShortcut.optString("shortLabel"); String longLabel = jsonShortcut.optString("longLabel"); if (shortLabel.length() == 0 && longLabel.length() == 0) { throw new InvalidParameterException("A value for either 'shortLabel' or 'longLabel' is required"); } if (shortLabel.length() == 0) { shortLabel = longLabel; } if (longLabel.length() == 0) { longLabel = shortLabel; } Icon icon; String iconBitmap = jsonShortcut.optString("iconBitmap"); String iconFromResource = jsonShortcut.optString("iconFromResource"); String activityPackage = this.cordova.getActivity().getPackageName(); if (iconBitmap.length() > 0) { icon = Icon.createWithBitmap(decodeBase64Bitmap(iconBitmap)); } if (iconFromResource.length() > 0){ Resources activityRes = this.cordova.getActivity().getResources(); int iconId = activityRes.getIdentifier(iconFromResource, "drawable", activityPackage); icon = Icon.createWithResource(context, iconId); } else { PackageManager pm = context.getPackageManager(); ApplicationInfo applicationInfo = pm.getApplicationInfo(activityPackage, PackageManager.GET_META_DATA); icon = Icon.createWithResource(activityPackage, applicationInfo.icon); } JSONObject jsonIntent = jsonShortcut.optJSONObject("intent"); if (jsonIntent == null) { jsonIntent = new JSONObject(); } Intent intent = parseIntent(jsonIntent); return builder .setShortLabel(shortLabel) .setLongLabel(longLabel) .setIntent(intent) .setIcon(icon) .build(); } private void setDynamicShortcuts( JSONArray args) throws PackageManager.NameNotFoundException, JSONException { int count = args.length(); ArrayList<ShortcutInfo> shortcuts = new ArrayList<ShortcutInfo>(count); for (int i = 0; i < count; ++i) { shortcuts.add(buildDynamicShortcut(args.optJSONObject(i))); } Context context = this.cordova.getActivity().getApplicationContext(); ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class); shortcutManager.setDynamicShortcuts(shortcuts); Log.i(TAG, String.format("Saved % dynamic shortcuts.", count)); } private ShortcutInfoCompat buildPinnedShortcut( JSONObject jsonShortcut ) throws PackageManager.NameNotFoundException, JSONException { if (jsonShortcut == null) { throw new InvalidParameterException("Parameters must include a valid shorcut."); } Context context = this.cordova.getActivity().getApplicationContext(); String shortcutId = jsonShortcut.optString("id"); if (shortcutId.length() == 0) { throw new InvalidParameterException("A value for 'id' is required"); } ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(context, shortcutId); String shortLabel = jsonShortcut.optString("shortLabel"); String longLabel = jsonShortcut.optString("longLabel"); if (shortLabel.length() == 0 && longLabel.length() == 0) { throw new InvalidParameterException("A value for either 'shortLabel' or 'longLabel' is required"); } if (shortLabel.length() == 0) { shortLabel = longLabel; } if (longLabel.length() == 0) { longLabel = shortLabel; } IconCompat icon; String iconBitmap = jsonShortcut.optString("iconBitmap"); if (iconBitmap.length() > 0) { icon = IconCompat.createWithBitmap(decodeBase64Bitmap(iconBitmap)); } else { String activityPackage = this.cordova.getActivity().getPackageName(); PackageManager pm = context.getPackageManager(); ApplicationInfo applicationInfo = pm.getApplicationInfo(activityPackage, PackageManager.GET_META_DATA); icon = IconCompat.createWithResource(context, applicationInfo.icon); } JSONObject jsonIntent = jsonShortcut.optJSONObject("intent"); if (jsonIntent == null) { jsonIntent = new JSONObject(); } Intent intent = parseIntent(jsonIntent); return builder .setActivity(intent.getComponent()) .setShortLabel(shortLabel) .setLongLabel(longLabel) .setIcon(icon) .setIntent(intent) .build(); } private boolean addPinnedShortcut( JSONArray args ) throws PackageManager.NameNotFoundException, JSONException { ShortcutInfoCompat shortcut = buildPinnedShortcut(args.optJSONObject(0)); Context context = this.cordova.getActivity().getApplicationContext(); return ShortcutManagerCompat.requestPinShortcut(context, shortcut, null); } private static Bitmap decodeBase64Bitmap( String input) { byte[] decodedByte = Base64.decode(input, 0); return BitmapFactory.decodeByteArray(decodedByte, 0, decodedByte.length); } }