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.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_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>() {
              @Override
              public 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("type", "al");
          appLinkRequestParameters.putString("ids", graphRequestFields.toString());
          appLinkRequestParameters.putString(
                  "fields",
                  String.format("%s,%s", 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 */
                      @Override
                      public 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());
                                  JSONArray rawTargets = urlData.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, urlData);
                                  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 target
              return 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 url
                  return 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 url
              return webUri != null ? webUri: sourceUrl;
          } catch (JSONException e) {
              // If we were missing a web target, just use the source as the web url
              return 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;
          }
      }
  }
  