/* * Copyright (c) 2014, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ package bolts; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; import android.util.SparseArray; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.URL; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Represents a pending request to navigate to an App Link. Most developers will simply use * {@link #navigateInBackground(android.content.Context, android.net.Uri)} to open a URL, but * developers can build custom requests with additional navigation and app data attached to them * by creating AppLinkNavigations themselves. */ public class AppLinkNavigation { private static final String KEY_NAME_USER_AGENT = "user_agent"; private static final String KEY_NAME_VERSION = "version"; private static final String KEY_NAME_REFERER_APP_LINK = "referer_app_link"; private static final String KEY_NAME_REFERER_APP_LINK_APP_NAME = "app_name"; private static final String KEY_NAME_REFERER_APP_LINK_PACKAGE = "package"; private static final String VERSION = "1.0"; private static AppLinkResolver defaultResolver; /** * The result of calling {@link #navigate(android.content.Context)} on an * {@link bolts.AppLinkNavigation}. */ public static enum NavigationResult { /** * Indicates that the navigation failed and no app was opened. */ FAILED("failed", false), /** * Indicates that the navigation succeeded by opening the URL in the browser. */ WEB("web", true), /** * Indicates that the navigation succeeded by opening the URL in an app on the device. */ APP("app", true); private String code; private boolean succeeded; public String getCode() { return code; } public boolean isSucceeded() { return succeeded; } NavigationResult(String code, boolean success) { this.code = code; this.succeeded = success; } } private final AppLink appLink; private final Bundle extras; private final Bundle appLinkData; /** * Creates an AppLinkNavigation with the given link, extras, and App Link data. * * @param appLink the AppLink being navigated to. * @param extras the extras to include in the App Link navigation. * @param appLinkData additional App Link data for the navigation. */ public AppLinkNavigation(AppLink appLink, Bundle extras, Bundle appLinkData) { if (appLink == null) { throw new IllegalArgumentException("appLink must not be null."); } if (extras == null) { extras = new Bundle(); } if (appLinkData == null) { appLinkData = new Bundle(); } this.appLink = appLink; this.extras = extras; this.appLinkData = appLinkData; } /** * @return the App Link to navigate to. */ public AppLink getAppLink() { return appLink; } /** * Gets the al_applink_data for the AppLinkNavigation. This will generally contain data common * to navigation attempts such as back-links, user agents, and other information that may be used * in routing and handling an App Link request. * * @return the App Link data. */ public Bundle getAppLinkData() { return appLinkData; } /** * The extras for the AppLinkNavigation. This will generally contain application-specific data * that should be passed along with the request, such as advertiser or affiliate IDs or other such * metadata relevant on this device. * * @return the extras for the AppLinkNavigation. */ public Bundle getExtras() { return extras; } /** * Creates a bundle containing the final, constructed App Link data to be used in navigation. */ private Bundle buildAppLinkDataForNavigation(Context context) { Bundle data = new Bundle(); Bundle refererAppLinkData = new Bundle(); if (context != null) { String refererAppPackage = context.getPackageName(); if (refererAppPackage != null) { refererAppLinkData.putString(KEY_NAME_REFERER_APP_LINK_PACKAGE, refererAppPackage); } ApplicationInfo appInfo = context.getApplicationInfo(); if (appInfo != null) { String refererAppName = context.getString(appInfo.labelRes); if (refererAppName != null) { refererAppLinkData.putString(KEY_NAME_REFERER_APP_LINK_APP_NAME, refererAppName); } } } data.putAll(getAppLinkData()); data.putString(AppLinks.KEY_NAME_TARGET, getAppLink().getSourceUrl().toString()); data.putString(KEY_NAME_VERSION, VERSION); data.putString(KEY_NAME_USER_AGENT, "Bolts Android " + Bolts.VERSION); data.putBundle(KEY_NAME_REFERER_APP_LINK, refererAppLinkData); data.putBundle(AppLinks.KEY_NAME_EXTRAS, getExtras()); return data; } /** * Gets a JSONObject-compatible value for the given object. */ private Object getJSONValue(Object value) throws JSONException { if (value instanceof Bundle) { return getJSONForBundle((Bundle) value); } else if (value instanceof CharSequence) { return value.toString(); } else if (value instanceof List) { JSONArray array = new JSONArray(); for (Object listValue : (List<?>) value) { array.put(getJSONValue(listValue)); } return array; } else if (value instanceof SparseArray) { JSONArray array = new JSONArray(); SparseArray<?> sparseValue = (SparseArray<?>) value; for (int i = 0; i < sparseValue.size(); i++) { array.put(sparseValue.keyAt(i), getJSONValue(sparseValue.valueAt(i))); } return array; } else if (value instanceof Character) { return value.toString(); } else if (value instanceof Boolean) { return value; } else if (value instanceof Number) { if (value instanceof Double || value instanceof Float) { return ((Number) value).doubleValue(); } else { return ((Number) value).longValue(); } } else if (value instanceof boolean[]) { JSONArray array = new JSONArray(); for (boolean arrValue : (boolean[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof char[]) { JSONArray array = new JSONArray(); for (char arrValue : (char[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof CharSequence[]) { JSONArray array = new JSONArray(); for (CharSequence arrValue : (CharSequence[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof double[]) { JSONArray array = new JSONArray(); for (double arrValue : (double[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof float[]) { JSONArray array = new JSONArray(); for (float arrValue : (float[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof int[]) { JSONArray array = new JSONArray(); for (int arrValue : (int[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof long[]) { JSONArray array = new JSONArray(); for (long arrValue : (long[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof short[]) { JSONArray array = new JSONArray(); for (short arrValue : (short[]) value) { array.put(getJSONValue(arrValue)); } return array; } else if (value instanceof String[]) { JSONArray array = new JSONArray(); for (String arrValue : (String[]) value) { array.put(getJSONValue(arrValue)); } return array; } return null; } /** * Gets a JSONObject equivalent to the input bundle for use when falling back to a web navigation. */ private JSONObject getJSONForBundle(Bundle bundle) throws JSONException { JSONObject root = new JSONObject(); for (String key : bundle.keySet()) { root.put(key, getJSONValue(bundle.get(key))); } return root; } /** * Performs the navigation. * * @param context the Context from which the navigation should be performed. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public NavigationResult navigate(Context context) { PackageManager pm = context.getPackageManager(); Bundle finalAppLinkData = buildAppLinkDataForNavigation(context); Intent eligibleTargetIntent = null; for (AppLink.Target target : getAppLink().getTargets()) { Intent targetIntent = new Intent(Intent.ACTION_VIEW); if (target.getUrl() != null) { targetIntent.setData(target.getUrl()); } else { targetIntent.setData(appLink.getSourceUrl()); } targetIntent.setPackage(target.getPackageName()); if (target.getClassName() != null) { targetIntent.setClassName(target.getPackageName(), target.getClassName()); } targetIntent.putExtra(AppLinks.KEY_NAME_APPLINK_DATA, finalAppLinkData); ResolveInfo resolved = pm.resolveActivity(targetIntent, PackageManager.MATCH_DEFAULT_ONLY); if (resolved != null) { eligibleTargetIntent = targetIntent; break; } } Intent outIntent = null; NavigationResult result = NavigationResult.FAILED; if (eligibleTargetIntent != null) { outIntent = eligibleTargetIntent; result = NavigationResult.APP; } else { // Fall back to the web if it's available Uri webUrl = getAppLink().getWebUrl(); if (webUrl != null) { JSONObject appLinkDataJson; try { appLinkDataJson = getJSONForBundle(finalAppLinkData); } catch (JSONException e) { sendAppLinkNavigateEventBroadcast(context, eligibleTargetIntent, NavigationResult.FAILED, e); throw new RuntimeException(e); } webUrl = webUrl.buildUpon() .appendQueryParameter(AppLinks.KEY_NAME_APPLINK_DATA, appLinkDataJson.toString()) .build(); outIntent = new Intent(Intent.ACTION_VIEW, webUrl); result = NavigationResult.WEB; } } sendAppLinkNavigateEventBroadcast(context, outIntent, result, null); if (outIntent != null) { context.startActivity(outIntent); } return result; } private void sendAppLinkNavigateEventBroadcast(Context context, Intent intent, NavigationResult type, JSONException e) { Map<String, String> extraLoggingData = new HashMap<String, String>(); if (e != null) { extraLoggingData.put("error", e.getLocalizedMessage()); } extraLoggingData.put("success", type.isSucceeded() ? "1" : "0"); extraLoggingData.put("type", type.getCode()); MeasurementEvent.sendBroadcastEvent( context, MeasurementEvent.APP_LINK_NAVIGATE_OUT_EVENT_NAME, intent, extraLoggingData); } /** * Sets the default resolver to be used for App Link resolution. Setting this to null will cause * the {@link #navigateInBackground(android.content.Context, android.net.Uri)} methods to use the * basic, built-in resolver provided by Bolts. * * @param resolver the resolver to use by default. */ public static void setDefaultResolver(AppLinkResolver resolver) { defaultResolver = resolver; } /** * Gets the default resolver to be used for App Link resolution. If the developer has not set a * default resolver, this will return {@code null}, but the basic, built-in resolver provided by * Bolts will be used. * * @return the default resolver, or {@code null} if none is set. */ public static AppLinkResolver getDefaultResolver() { return defaultResolver; } private static AppLinkResolver getResolver(Context context) { if (getDefaultResolver() != null) { return getDefaultResolver(); } return new WebViewAppLinkResolver(context); } /** * Navigates to an {@link bolts.AppLink}. * * @param context the Context from which the navigation should be performed. * @param appLink the AppLink being navigated to. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public static NavigationResult navigate(Context context, AppLink appLink) { return new AppLinkNavigation(appLink, null, null).navigate(context); } /** * Navigates to an {@link bolts.AppLink} for the given destination using the App Link resolution * strategy specified. * * @param context the Context from which the navigation should be performed. * @param destination the destination URL for the App Link. * @param resolver the resolver to use for fetching App Link metadata. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public static Task<NavigationResult> navigateInBackground(final Context context, Uri destination, AppLinkResolver resolver) { return resolver.getAppLinkFromUrlInBackground(destination) .onSuccess(new Continuation<AppLink, NavigationResult>() { @Override public NavigationResult then(Task<AppLink> task) throws Exception { return navigate(context, task.getResult()); } }, Task.UI_THREAD_EXECUTOR); } /** * Navigates to an {@link bolts.AppLink} for the given destination using the App Link resolution * strategy specified. * * @param context the Context from which the navigation should be performed. * @param destination the destination URL for the App Link. * @param resolver the resolver to use for fetching App Link metadata. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public static Task<NavigationResult> navigateInBackground(Context context, URL destination, AppLinkResolver resolver) { return navigateInBackground(context, Uri.parse(destination.toString()), resolver); } /** * Navigates to an {@link bolts.AppLink} for the given destination using the App Link resolution * strategy specified. * * @param context the Context from which the navigation should be performed. * @param destinationUrl the destination URL for the App Link. * @param resolver the resolver to use for fetching App Link metadata. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public static Task<NavigationResult> navigateInBackground(Context context, String destinationUrl, AppLinkResolver resolver) { return navigateInBackground(context, Uri.parse(destinationUrl), resolver); } /** * Navigates to an {@link bolts.AppLink} for the given destination using the default * App Link resolution strategy. * * @param context the Context from which the navigation should be performed. * @param destination the destination URL for the App Link. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public static Task<NavigationResult> navigateInBackground(Context context, Uri destination) { return navigateInBackground(context, destination, getResolver(context)); } /** * Navigates to an {@link bolts.AppLink} for the given destination using the default * App Link resolution strategy. * * @param context the Context from which the navigation should be performed. * @param destination the destination URL for the App Link. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public static Task<NavigationResult> navigateInBackground(Context context, URL destination) { return navigateInBackground(context, destination, getResolver(context)); } /** * Navigates to an {@link bolts.AppLink} for the given destination using the default * App Link resolution strategy. * * @param context the Context from which the navigation should be performed. * @param destinationUrl the destination URL for the App Link. * @return the {@link bolts.AppLinkNavigation.NavigationResult} performed by navigating. */ public static Task<NavigationResult> navigateInBackground(Context context, String destinationUrl) { return navigateInBackground(context, destinationUrl, getResolver(context)); } }