Blame | Last modification | View Log | RSS feed
/*** Copyright 2010-present Facebook.** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.facebook;import android.net.Uri;import android.os.Bundle;import bolts.AppLink;import bolts.AppLinkResolver;import bolts.Continuation;import bolts.Task;import com.facebook.model.GraphObject;import org.json.JSONArray;import org.json.JSONException;import org.json.JSONObject;import java.util.*;/*** Provides an implementation for the {@link AppLinkResolver AppLinkResolver} interface that uses the Facebook App Link* index to solve App Links, given a Url. It also provides an additional helper method that can resolve multiple App* Links in a single call.*/public class FacebookAppLinkResolver implements AppLinkResolver {private static final String APP_LINK_KEY = "app_links";private static final String APP_LINK_ANDROID_TARGET_KEY = "android";private static final String APP_LINK_WEB_TARGET_KEY = "web";private static final String APP_LINK_TARGET_PACKAGE_KEY = "package";private static final String APP_LINK_TARGET_CLASS_KEY = "class";private static final String APP_LINK_TARGET_APP_NAME_KEY = "app_name";private static final String APP_LINK_TARGET_URL_KEY = "url";private static final String APP_LINK_TARGET_SHOULD_FALLBACK_KEY = "should_fallback";private final HashMap<Uri, AppLink> cachedAppLinks = new HashMap<Uri, AppLink>();/*** Asynchronously resolves App Link data for the passed in Uri** @param uri Uri to be resolved into an App Link* @return A Task that, when successful, will return an AppLink for the passed in Uri. This may be null if no App* Link data was found for this Uri.* In the case of general server errors, the task will be completed with the corresponding error.*/public Task<AppLink> getAppLinkFromUrlInBackground(final Uri uri) {ArrayList<Uri> uris = new ArrayList<Uri>();uris.add(uri);Task<Map<Uri, AppLink>> resolveTask = getAppLinkFromUrlsInBackground(uris);return resolveTask.onSuccess(new Continuation<Map<Uri, AppLink>, AppLink>() {@Overridepublic AppLink then(Task<Map<Uri, AppLink>> resolveUrisTask) throws Exception {return resolveUrisTask.getResult().get(uri);}});}/*** Asynchronously resolves App Link data for multiple Urls** @param uris A list of Uri objects to resolve into App Links* @return A Task that, when successful, will return a Map of Uri->AppLink for each Uri that was successfully* resolved into an App Link. Uris that could not be resolved into App Links will not be present in the Map.* In the case of general server errors, the task will be completed with the corresponding error.*/public Task<Map<Uri, AppLink>> getAppLinkFromUrlsInBackground(List<Uri> uris) {final Map<Uri, AppLink> appLinkResults = new HashMap<Uri, AppLink>();final HashSet<Uri> urisToRequest = new HashSet<Uri>();StringBuilder graphRequestFields = new StringBuilder();for (Uri uri : uris) {AppLink appLink = null;synchronized (cachedAppLinks) {appLink = cachedAppLinks.get(uri);}if (appLink != null) {appLinkResults.put(uri, appLink);} else {if (!urisToRequest.isEmpty()) {graphRequestFields.append(',');}graphRequestFields.append(uri.toString());urisToRequest.add(uri);}}if (urisToRequest.isEmpty()) {return Task.forResult(appLinkResults);}final Task<Map<Uri, AppLink>>.TaskCompletionSource taskCompletionSource = Task.create();Bundle appLinkRequestParameters = new Bundle();appLinkRequestParameters.putString("ids", graphRequestFields.toString());appLinkRequestParameters.putString("fields",String.format("%s.fields(%s,%s)", APP_LINK_KEY, APP_LINK_ANDROID_TARGET_KEY, APP_LINK_WEB_TARGET_KEY));Request appLinkRequest = new Request(null, /* Session */"", /* Graph path */appLinkRequestParameters, /* Query parameters */null, /* HttpMethod */new Request.Callback() { /* Callback */@Overridepublic void onCompleted(Response response) {FacebookRequestError error = response.getError();if (error != null) {taskCompletionSource.setError(error.getException());return;}GraphObject responseObject = response.getGraphObject();JSONObject responseJson = responseObject != null ? responseObject.getInnerJSONObject() : null;if (responseJson == null) {taskCompletionSource.setResult(appLinkResults);return;}for (Uri uri : urisToRequest) {String uriString = uri.toString();if (!responseJson.has(uriString)) {continue;}JSONObject urlData = null;try {urlData = responseJson.getJSONObject(uri.toString());JSONObject appLinkData = urlData.getJSONObject(APP_LINK_KEY);JSONArray rawTargets = appLinkData.getJSONArray(APP_LINK_ANDROID_TARGET_KEY);int targetsCount = rawTargets.length();List<AppLink.Target> targets = new ArrayList<AppLink.Target>(targetsCount);for (int i = 0; i < targetsCount; i++) {AppLink.Target target = getAndroidTargetFromJson(rawTargets.getJSONObject(i));if (target != null) {targets.add(target);}}Uri webFallbackUrl = getWebFallbackUriFromJson(uri, appLinkData);AppLink appLink = new AppLink(uri, targets, webFallbackUrl);appLinkResults.put(uri, appLink);synchronized (cachedAppLinks) {cachedAppLinks.put(uri, appLink);}} catch (JSONException e) {// The data for this uri was missing or badly formed.continue;}}taskCompletionSource.setResult(appLinkResults);}});appLinkRequest.executeAsync();return taskCompletionSource.getTask();}private static AppLink.Target getAndroidTargetFromJson(JSONObject targetJson) {String packageName = tryGetStringFromJson(targetJson, APP_LINK_TARGET_PACKAGE_KEY, null);if (packageName == null) {// Package name is mandatory for each Android targetreturn null;}String className = tryGetStringFromJson(targetJson, APP_LINK_TARGET_CLASS_KEY, null);String appName = tryGetStringFromJson(targetJson, APP_LINK_TARGET_APP_NAME_KEY, null);String targetUrlString = tryGetStringFromJson(targetJson, APP_LINK_TARGET_URL_KEY, null);Uri targetUri = null;if (targetUrlString != null) {targetUri = Uri.parse(targetUrlString);}return new AppLink.Target(packageName, className, targetUri, appName);}private static Uri getWebFallbackUriFromJson(Uri sourceUrl, JSONObject urlData) {// Try and get a web target. This is best effort. Any failures results in null being returned.try {JSONObject webTarget = urlData.getJSONObject(APP_LINK_WEB_TARGET_KEY);boolean shouldFallback = tryGetBooleanFromJson(webTarget, APP_LINK_TARGET_SHOULD_FALLBACK_KEY, true);if (!shouldFallback) {// Don't use a fallback urlreturn null;}String webTargetUrlString = tryGetStringFromJson(webTarget, APP_LINK_TARGET_URL_KEY, null);Uri webUri = null;if (webTargetUrlString != null) {webUri = Uri.parse(webTargetUrlString);}// If we weren't able to parse a url from the web target, use the source urlreturn webUri != null ? webUri: sourceUrl;} catch (JSONException e) {// If we were missing a web target, just use the source as the web urlreturn sourceUrl;}}private static String tryGetStringFromJson(JSONObject json, String propertyName, String defaultValue) {try {return json.getString(propertyName);} catch(JSONException e) {return defaultValue;}}private static boolean tryGetBooleanFromJson(JSONObject json, String propertyName, boolean defaultValue) {try {return json.getBoolean(propertyName);} catch (JSONException e) {return defaultValue;}}}