/** * 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.app.Activity; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import com.facebook.internal.AttributionIdentifiers; import com.facebook.internal.Utility; import com.facebook.internal.Validate; import com.facebook.model.GraphObject; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.Iterator; /** * Class to encapsulate an app link, and provide methods for constructing the data from various sources */ public class AppLinkData { /** * Key that should be used to pull out the UTC Unix tap-time from the arguments for this app link. */ public static final String ARGUMENTS_TAPTIME_KEY = "com.facebook.platform.APPLINK_TAP_TIME_UTC"; /** * Key that should be used to get the "referer_data" field for this app link. */ public static final String ARGUMENTS_REFERER_DATA_KEY = "referer_data"; /** * Key that should be used to pull out the native class that would have been used if the applink was deferred. */ public static final String ARGUMENTS_NATIVE_CLASS_KEY = "com.facebook.platform.APPLINK_NATIVE_CLASS"; /** * Key that should be used to pull out the native url that would have been used if the applink was deferred. */ public static final String ARGUMENTS_NATIVE_URL = "com.facebook.platform.APPLINK_NATIVE_URL"; static final String BUNDLE_APPLINK_ARGS_KEY = "com.facebook.platform.APPLINK_ARGS"; private static final String BUNDLE_AL_APPLINK_DATA_KEY = "al_applink_data"; private static final String APPLINK_BRIDGE_ARGS_KEY = "bridge_args"; private static final String APPLINK_METHOD_ARGS_KEY = "method_args"; private static final String APPLINK_VERSION_KEY = "version"; private static final String BRIDGE_ARGS_METHOD_KEY = "method"; private static final String DEFERRED_APP_LINK_EVENT = "DEFERRED_APP_LINK"; private static final String DEFERRED_APP_LINK_PATH = "%s/activities"; private static final String DEFERRED_APP_LINK_ARGS_FIELD = "applink_args"; private static final String DEFERRED_APP_LINK_CLASS_FIELD = "applink_class"; private static final String DEFERRED_APP_LINK_CLICK_TIME_FIELD = "click_time"; private static final String DEFERRED_APP_LINK_URL_FIELD = "applink_url"; private static final String METHOD_ARGS_TARGET_URL_KEY = "target_url"; private static final String METHOD_ARGS_REF_KEY = "ref"; private static final String REFERER_DATA_REF_KEY = "fb_ref"; private static final String TAG = AppLinkData.class.getCanonicalName(); private String ref; private Uri targetUri; private JSONObject arguments; private Bundle argumentBundle; /** * Asynchronously fetches app link information that might have been stored for use * after installation of the app * @param context The context * @param completionHandler CompletionHandler to be notified with the AppLinkData object or null if none is * available. Must not be null. */ public static void fetchDeferredAppLinkData(Context context, CompletionHandler completionHandler) { fetchDeferredAppLinkData(context, null, completionHandler); } /** * Asynchronously fetches app link information that might have been stored for use * after installation of the app * @param context The context * @param applicationId Facebook application Id. If null, it is taken from the manifest * @param completionHandler CompletionHandler to be notified with the AppLinkData object or null if none is * available. Must not be null. */ public static void fetchDeferredAppLinkData( Context context, String applicationId, final CompletionHandler completionHandler) { Validate.notNull(context, "context"); Validate.notNull(completionHandler, "completionHandler"); if (applicationId == null) { applicationId = Utility.getMetadataApplicationId(context); } Validate.notNull(applicationId, "applicationId"); final Context applicationContext = context.getApplicationContext(); final String applicationIdCopy = applicationId; Settings.getExecutor().execute(new Runnable() { @Override public void run() { fetchDeferredAppLinkFromServer(applicationContext, applicationIdCopy, completionHandler); } }); } private static void fetchDeferredAppLinkFromServer( Context context, String applicationId, final CompletionHandler completionHandler) { GraphObject deferredApplinkParams = GraphObject.Factory.create(); deferredApplinkParams.setProperty("event", DEFERRED_APP_LINK_EVENT); Utility.setAppEventAttributionParameters(deferredApplinkParams, AttributionIdentifiers.getAttributionIdentifiers(context), AppEventsLogger.getAnonymousAppDeviceGUID(context), Settings.getLimitEventAndDataUsage(context)); deferredApplinkParams.setProperty("application_package_name", context.getPackageName()); String deferredApplinkUrlPath = String.format(DEFERRED_APP_LINK_PATH, applicationId); AppLinkData appLinkData = null; try { Request deferredApplinkRequest = Request.newPostRequest( null, deferredApplinkUrlPath, deferredApplinkParams, null); Response deferredApplinkResponse = deferredApplinkRequest.executeAndWait(); GraphObject wireResponse = deferredApplinkResponse.getGraphObject(); JSONObject jsonResponse = wireResponse != null ? wireResponse.getInnerJSONObject() : null; if (jsonResponse != null) { final String appLinkArgsJsonString = jsonResponse.optString(DEFERRED_APP_LINK_ARGS_FIELD); final long tapTimeUtc = jsonResponse.optLong(DEFERRED_APP_LINK_CLICK_TIME_FIELD, -1); final String appLinkClassName = jsonResponse.optString(DEFERRED_APP_LINK_CLASS_FIELD); final String appLinkUrl = jsonResponse.optString(DEFERRED_APP_LINK_URL_FIELD); if (!TextUtils.isEmpty(appLinkArgsJsonString)) { appLinkData = createFromJson(appLinkArgsJsonString); if (tapTimeUtc != -1) { try { if (appLinkData.arguments != null) { appLinkData.arguments.put(ARGUMENTS_TAPTIME_KEY, tapTimeUtc); } if (appLinkData.argumentBundle != null) { appLinkData.argumentBundle.putString(ARGUMENTS_TAPTIME_KEY, Long.toString(tapTimeUtc)); } } catch (JSONException e) { Log.d(TAG, "Unable to put tap time in AppLinkData.arguments"); } } if (appLinkClassName != null) { try { if (appLinkData.arguments != null) { appLinkData.arguments.put(ARGUMENTS_NATIVE_CLASS_KEY, appLinkClassName); } if (appLinkData.argumentBundle != null) { appLinkData.argumentBundle.putString(ARGUMENTS_NATIVE_CLASS_KEY, appLinkClassName); } } catch (JSONException e) { Log.d(TAG, "Unable to put tap time in AppLinkData.arguments"); } } if (appLinkUrl != null) { try { if (appLinkData.arguments != null) { appLinkData.arguments.put(ARGUMENTS_NATIVE_URL, appLinkUrl); } if (appLinkData.argumentBundle != null) { appLinkData.argumentBundle.putString(ARGUMENTS_NATIVE_URL, appLinkUrl); } } catch (JSONException e) { Log.d(TAG, "Unable to put tap time in AppLinkData.arguments"); } } } } } catch (Exception e) { Utility.logd(TAG, "Unable to fetch deferred applink from server"); } completionHandler.onDeferredAppLinkDataFetched(appLinkData); } /** * Parses out any app link data from the Intent of the Activity passed in. * @param activity Activity that was started because of an app link * @return AppLinkData if found. null if not. */ public static AppLinkData createFromActivity(Activity activity) { Validate.notNull(activity, "activity"); Intent intent = activity.getIntent(); if (intent == null) { return null; } AppLinkData appLinkData = createFromAlApplinkData(intent); if (appLinkData == null) { String appLinkArgsJsonString = intent.getStringExtra(BUNDLE_APPLINK_ARGS_KEY); appLinkData = createFromJson(appLinkArgsJsonString); } if (appLinkData == null) { // Try regular app linking appLinkData = createFromUri(intent.getData()); } return appLinkData; } private static AppLinkData createFromAlApplinkData(Intent intent) { Bundle applinks = intent.getBundleExtra(BUNDLE_AL_APPLINK_DATA_KEY); if (applinks == null) { return null; } AppLinkData appLinkData = new AppLinkData(); appLinkData.targetUri = intent.getData(); if (appLinkData.targetUri == null) { String targetUriString = applinks.getString(METHOD_ARGS_TARGET_URL_KEY); if (targetUriString != null) { appLinkData.targetUri = Uri.parse(targetUriString); } } appLinkData.argumentBundle = applinks; appLinkData.arguments = null; Bundle refererData = applinks.getBundle(ARGUMENTS_REFERER_DATA_KEY); if (refererData != null) { appLinkData.ref = refererData.getString(REFERER_DATA_REF_KEY); } return appLinkData; } private static AppLinkData createFromJson(String jsonString) { if (jsonString == null) { return null; } try { // Any missing or malformed data will result in a JSONException JSONObject appLinkArgsJson = new JSONObject(jsonString); String version = appLinkArgsJson.getString(APPLINK_VERSION_KEY); JSONObject bridgeArgs = appLinkArgsJson.getJSONObject(APPLINK_BRIDGE_ARGS_KEY); String method = bridgeArgs.getString(BRIDGE_ARGS_METHOD_KEY); if (method.equals("applink") && version.equals("2")) { // We have a new deep link AppLinkData appLinkData = new AppLinkData(); appLinkData.arguments = appLinkArgsJson.getJSONObject(APPLINK_METHOD_ARGS_KEY); // first look for the "ref" key in the top level args if (appLinkData.arguments.has(METHOD_ARGS_REF_KEY)) { appLinkData.ref = appLinkData.arguments.getString(METHOD_ARGS_REF_KEY); } else if (appLinkData.arguments.has(ARGUMENTS_REFERER_DATA_KEY)) { // if it's not in the top level args, it could be in the "referer_data" blob JSONObject refererData = appLinkData.arguments.getJSONObject(ARGUMENTS_REFERER_DATA_KEY); if (refererData.has(REFERER_DATA_REF_KEY)) { appLinkData.ref = refererData.getString(REFERER_DATA_REF_KEY); } } if (appLinkData.arguments.has(METHOD_ARGS_TARGET_URL_KEY)) { appLinkData.targetUri = Uri.parse(appLinkData.arguments.getString(METHOD_ARGS_TARGET_URL_KEY)); } appLinkData.argumentBundle = toBundle(appLinkData.arguments); return appLinkData; } } catch (JSONException e) { Log.d(TAG, "Unable to parse AppLink JSON", e); } catch (FacebookException e) { Log.d(TAG, "Unable to parse AppLink JSON", e); } return null; } private static AppLinkData createFromUri(Uri appLinkDataUri) { if (appLinkDataUri == null) { return null; } AppLinkData appLinkData = new AppLinkData(); appLinkData.targetUri = appLinkDataUri; return appLinkData; } private static Bundle toBundle(JSONObject node) throws JSONException { Bundle bundle = new Bundle(); @SuppressWarnings("unchecked") Iterator<String> fields = node.keys(); while (fields.hasNext()) { String key = fields.next(); Object value; value = node.get(key); if (value instanceof JSONObject) { bundle.putBundle(key, toBundle((JSONObject) value)); } else if (value instanceof JSONArray) { JSONArray valueArr = (JSONArray) value; if (valueArr.length() == 0) { bundle.putStringArray(key, new String[0]); } else { Object firstNode = valueArr.get(0); if (firstNode instanceof JSONObject) { Bundle[] bundles = new Bundle[valueArr.length()]; for (int i = 0; i < valueArr.length(); i++) { bundles[i] = toBundle(valueArr.getJSONObject(i)); } bundle.putParcelableArray(key, bundles); } else if (firstNode instanceof JSONArray) { // we don't support nested arrays throw new FacebookException("Nested arrays are not supported."); } else { // just use the string value String[] arrValues = new String[valueArr.length()]; for (int i = 0; i < valueArr.length(); i++) { arrValues[i] = valueArr.get(i).toString(); } bundle.putStringArray(key, arrValues); } } } else { bundle.putString(key, value.toString()); } } return bundle; } private AppLinkData() { } /** * Returns the target uri for this App Link. * @return target uri */ public Uri getTargetUri() { return targetUri; } /** * Returns the ref for this App Link. * @return ref */ public String getRef() { return ref; } /** * This method has been deprecated. Please use {@link AppLinkData#getArgumentBundle()} instead. * @return JSONObject property bag. */ @Deprecated public JSONObject getArguments() { return arguments; } /** * The full set of arguments for this app link. Properties like target uri & ref are typically * picked out of this set of arguments. * @return App link related arguments as a bundle. */ public Bundle getArgumentBundle() { return argumentBundle; } /** * The referer data associated with the app link. This will contain Facebook specific information like * fb_access_token, fb_expires_in, and fb_ref. * @return the referer data. */ public Bundle getRefererData() { if (argumentBundle != null) { return argumentBundle.getBundle(ARGUMENTS_REFERER_DATA_KEY); } return null; } /** * Interface to asynchronously receive AppLinkData after it has been fetched. */ public interface CompletionHandler { /** * This method is called when deferred app link data has been fetched. If no app link data was found, * this method is called with null * @param appLinkData The app link data that was fetched. Null if none was found. */ void onDeferredAppLinkDataFetched(AppLinkData appLinkData); } }