content: /**
   * 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.os.Handler;
  import android.os.Looper;
  import android.util.Log;
  import com.facebook.internal.*;
  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),
                  Utility.getHashedDeviceAndAppID(context, applicationId),
                  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 (appLinkArgsJsonString != null && 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);
      }
  }
  