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.internal;
  
  import android.content.Context;
  import android.net.Uri;
  import android.os.Bundle;
  import android.os.Parcelable;
  import android.provider.Settings.Secure;
  import android.text.TextUtils;
  import android.util.Log;
  import android.webkit.CookieManager;
  import android.webkit.CookieSyncManager;
  import com.facebook.*;
  import com.facebook.android.BuildConfig;
  import com.facebook.model.GraphObject;
  import org.json.JSONArray;
  import org.json.JSONException;
  import org.json.JSONObject;
  import org.json.JSONTokener;
  
  import java.io.*;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.net.HttpURLConnection;
  import java.net.URLConnection;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.util.*;
  import java.util.concurrent.ConcurrentHashMap;
  
  /**
   * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of
   * any of the classes in this package is unsupported, and they may be modified or removed without warning at
   * any time.
   */
  public final class Utility {
      static final String LOG_TAG = "FacebookSDK";
      private static final String HASH_ALGORITHM_MD5 = "MD5";
      private static final String HASH_ALGORITHM_SHA1 = "SHA-1";
      private static final String URL_SCHEME = "https";
      private static final String SUPPORTS_ATTRIBUTION = "supports_attribution";
      private static final String SUPPORTS_IMPLICIT_SDK_LOGGING = "supports_implicit_sdk_logging";
      private static final String NUX_CONTENT = "gdpv4_nux_content";
      private static final String NUX_ENABLED = "gdpv4_nux_enabled";
  
      private static final String [] APP_SETTING_FIELDS = new String[] {
              SUPPORTS_ATTRIBUTION,
              SUPPORTS_IMPLICIT_SDK_LOGGING,
              NUX_CONTENT,
              NUX_ENABLED
      };
      private static final String APPLICATION_FIELDS = "fields";
  
      // This is the default used by the buffer streams, but they trace a warning if you do not specify.
      public static final int DEFAULT_STREAM_BUFFER_SIZE = 8192;
  
      private static Map<String, FetchedAppSettings> fetchedAppSettings =
              new ConcurrentHashMap<String, FetchedAppSettings>();
  
      public static class FetchedAppSettings {
          private boolean supportsAttribution;
          private boolean supportsImplicitLogging;
          private String nuxContent;
          private boolean nuxEnabled;
  
          private FetchedAppSettings(boolean supportsAttribution,
                                     boolean supportsImplicitLogging,
                                     String nuxContent,
                                     boolean nuxEnabled) {
              this.supportsAttribution = supportsAttribution;
              this.supportsImplicitLogging = supportsImplicitLogging;
              this.nuxContent = nuxContent;
              this.nuxEnabled = nuxEnabled;
          }
  
          public boolean supportsAttribution() {
              return supportsAttribution;
          }
  
          public boolean supportsImplicitLogging() {
              return supportsImplicitLogging;
          }
  
          public String getNuxContent() {
              return nuxContent;
          }
  
          public boolean getNuxEnabled() {
              return nuxEnabled;
          }
      }
  
      // Returns true iff all items in subset are in superset, treating null and
      // empty collections as
      // the same.
      public static <T> boolean isSubset(Collection<T> subset, Collection<T> superset) {
          if ((superset == null) || (superset.size() == 0)) {
              return ((subset == null) || (subset.size() == 0));
          }
  
          HashSet<T> hash = new HashSet<T>(superset);
          for (T t : subset) {
              if (!hash.contains(t)) {
                  return false;
              }
          }
          return true;
      }
  
      public static <T> boolean isNullOrEmpty(Collection<T> c) {
          return (c == null) || (c.size() == 0);
      }
  
      public static boolean isNullOrEmpty(String s) {
          return (s == null) || (s.length() == 0);
      }
  
      public static <T> Collection<T> unmodifiableCollection(T... ts) {
          return Collections.unmodifiableCollection(Arrays.asList(ts));
      }
  
      public static <T> ArrayList<T> arrayList(T... ts) {
          ArrayList<T> arrayList = new ArrayList<T>(ts.length);
          for (T t : ts) {
              arrayList.add(t);
          }
          return arrayList;
      }
  
      static String md5hash(String key) {
          return hashWithAlgorithm(HASH_ALGORITHM_MD5, key);
      }
  
      static String sha1hash(String key) {
          return hashWithAlgorithm(HASH_ALGORITHM_SHA1, key);
      }
  
      static String sha1hash(byte[] bytes) {
          return hashWithAlgorithm(HASH_ALGORITHM_SHA1, bytes);
      }
  
      private static String hashWithAlgorithm(String algorithm, String key) {
          return hashWithAlgorithm(algorithm, key.getBytes());
      }
  
      private static String hashWithAlgorithm(String algorithm, byte[] bytes) {
          MessageDigest hash;
          try {
              hash = MessageDigest.getInstance(algorithm);
          } catch (NoSuchAlgorithmException e) {
              return null;
          }
          return hashBytes(hash, bytes);
      }
  
      private static String hashBytes(MessageDigest hash, byte[] bytes) {
          hash.update(bytes);
          byte[] digest = hash.digest();
          StringBuilder builder = new StringBuilder();
          for (int b : digest) {
              builder.append(Integer.toHexString((b >> 4) & 0xf));
              builder.append(Integer.toHexString((b >> 0) & 0xf));
          }
          return builder.toString();
      }
  
      public static Uri buildUri(String authority, String path, Bundle parameters) {
          Uri.Builder builder = new Uri.Builder();
          builder.scheme(URL_SCHEME);
          builder.authority(authority);
          builder.path(path);
          for (String key : parameters.keySet()) {
              Object parameter = parameters.get(key);
              if (parameter instanceof String) {
                  builder.appendQueryParameter(key, (String) parameter);
              }
          }
          return builder.build();
      }
  
      public static void putObjectInBundle(Bundle bundle, String key, Object value) {
          if (value instanceof String) {
              bundle.putString(key, (String) value);
          } else if (value instanceof Parcelable) {
              bundle.putParcelable(key, (Parcelable) value);
          } else if (value instanceof byte[]) {
              bundle.putByteArray(key, (byte[]) value);
          } else {
              throw new FacebookException("attempted to add unsupported type to Bundle");
          }
      }
  
      public static void closeQuietly(Closeable closeable) {
          try {
              if (closeable != null) {
                  closeable.close();
              }
          } catch (IOException ioe) {
              // ignore
          }
      }
  
      public static void disconnectQuietly(URLConnection connection) {
          if (connection instanceof HttpURLConnection) {
              ((HttpURLConnection)connection).disconnect();
          }
      }
  
      public static String getMetadataApplicationId(Context context) {
          Validate.notNull(context, "context");
  
          Settings.loadDefaultsFromMetadata(context);
  
          return Settings.getApplicationId();
      }
  
      static Map<String, Object> convertJSONObjectToHashMap(JSONObject jsonObject) {
          HashMap<String, Object> map = new HashMap<String, Object>();
          JSONArray keys = jsonObject.names();
          for (int i = 0; i < keys.length(); ++i) {
              String key;
              try {
                  key = keys.getString(i);
                  Object value = jsonObject.get(key);
                  if (value instanceof JSONObject) {
                      value = convertJSONObjectToHashMap((JSONObject) value);
                  }
                  map.put(key, value);
              } catch (JSONException e) {
              }
          }
          return map;
      }
  
      // Returns either a JSONObject or JSONArray representation of the 'key' property of 'jsonObject'.
      public static Object getStringPropertyAsJSON(JSONObject jsonObject, String key, String nonJSONPropertyKey)
              throws JSONException {
          Object value = jsonObject.opt(key);
          if (value != null && value instanceof String) {
              JSONTokener tokener = new JSONTokener((String) value);
              value = tokener.nextValue();
          }
  
          if (value != null && !(value instanceof JSONObject || value instanceof JSONArray)) {
              if (nonJSONPropertyKey != null) {
                  // Facebook sometimes gives us back a non-JSON value such as
                  // literal "true" or "false" as a result.
                  // If we got something like that, we present it to the caller as
                  // a GraphObject with a single
                  // property. We only do this if the caller wants that behavior.
                  jsonObject = new JSONObject();
                  jsonObject.putOpt(nonJSONPropertyKey, value);
                  return jsonObject;
              } else {
                  throw new FacebookException("Got an unexpected non-JSON object.");
              }
          }
  
          return value;
  
      }
  
      public static String readStreamToString(InputStream inputStream) throws IOException {
          BufferedInputStream bufferedInputStream = null;
          InputStreamReader reader = null;
          try {
              bufferedInputStream = new BufferedInputStream(inputStream);
              reader = new InputStreamReader(bufferedInputStream);
              StringBuilder stringBuilder = new StringBuilder();
  
              final int bufferSize = 1024 * 2;
              char[] buffer = new char[bufferSize];
              int n = 0;
              while ((n = reader.read(buffer)) != -1) {
                  stringBuilder.append(buffer, 0, n);
              }
  
              return stringBuilder.toString();
          } finally {
              closeQuietly(bufferedInputStream);
              closeQuietly(reader);
          }
      }
  
      public static boolean stringsEqualOrEmpty(String a, String b) {
          boolean aEmpty = TextUtils.isEmpty(a);
          boolean bEmpty = TextUtils.isEmpty(b);
  
          if (aEmpty && bEmpty) {
              // Both null or empty, they match.
              return true;
          }
          if (!aEmpty && !bEmpty) {
              // Both non-empty, check equality.
              return a.equals(b);
          }
          // One empty, one non-empty, can't match.
          return false;
      }
  
      private static void clearCookiesForDomain(Context context, String domain) {
          // This is to work around a bug where CookieManager may fail to instantiate if CookieSyncManager
          // has never been created.
          CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
          syncManager.sync();
  
          CookieManager cookieManager = CookieManager.getInstance();
  
          String cookies = cookieManager.getCookie(domain);
          if (cookies == null) {
              return;
          }
  
          String[] splitCookies = cookies.split(";");
          for (String cookie : splitCookies) {
              String[] cookieParts = cookie.split("=");
              if (cookieParts.length > 0) {
                  String newCookie = cookieParts[0].trim() + "=;expires=Sat, 1 Jan 2000 00:00:01 UTC;";
                  cookieManager.setCookie(domain, newCookie);
              }
          }
          cookieManager.removeExpiredCookie();
      }
  
      public static void clearFacebookCookies(Context context) {
          // setCookie acts differently when trying to expire cookies between builds of Android that are using
          // Chromium HTTP stack and those that are not. Using both of these domains to ensure it works on both.
          clearCookiesForDomain(context, "facebook.com");
          clearCookiesForDomain(context, ".facebook.com");
          clearCookiesForDomain(context, "https://facebook.com");
          clearCookiesForDomain(context, "https://.facebook.com");
      }
  
      public static void logd(String tag, Exception e) {
          if (BuildConfig.DEBUG && tag != null && e != null) {
              Log.d(tag, e.getClass().getSimpleName() + ": " + e.getMessage());
          }
      }
  
      public static void logd(String tag, String msg) {
          if (BuildConfig.DEBUG && tag != null && msg != null) {
              Log.d(tag, msg);
          }
      }
  
      public static <T> boolean areObjectsEqual(T a, T b) {
          if (a == null) {
              return b == null;
          }
          return a.equals(b);
      }
  
      // Note that this method makes a synchronous Graph API call, so should not be called from the main thread.
      public static FetchedAppSettings queryAppSettings(final String applicationId, final boolean forceRequery) {
  
          // Cache the last app checked results.
          if (!forceRequery && fetchedAppSettings.containsKey(applicationId)) {
              return fetchedAppSettings.get(applicationId);
          }
  
          Bundle appSettingsParams = new Bundle();
          appSettingsParams.putString(APPLICATION_FIELDS, TextUtils.join(",", APP_SETTING_FIELDS));
  
          Request request = Request.newGraphPathRequest(null, applicationId, null);
          request.setParameters(appSettingsParams);
  
          GraphObject supportResponse = request.executeAndWait().getGraphObject();
          FetchedAppSettings result = new FetchedAppSettings(
                  safeGetBooleanFromResponse(supportResponse, SUPPORTS_ATTRIBUTION),
                  safeGetBooleanFromResponse(supportResponse, SUPPORTS_IMPLICIT_SDK_LOGGING),
                  safeGetStringFromResponse(supportResponse, NUX_CONTENT),
                  safeGetBooleanFromResponse(supportResponse, NUX_ENABLED)
                  );
  
          fetchedAppSettings.put(applicationId, result);
  
          return result;
      }
  
      private static boolean safeGetBooleanFromResponse(GraphObject response, String propertyName) {
          Object result = false;
          if (response != null) {
              result = response.getProperty(propertyName);
          }
          if (!(result instanceof Boolean)) {
              result = false;
          }
          return (Boolean) result;
      }
  
      private static String safeGetStringFromResponse(GraphObject response, String propertyName) {
          Object result = "";
          if (response != null) {
              result = response.getProperty(propertyName);
          }
          if (!(result instanceof String)) {
              result = "";
          }
          return (String) result;
      }
  
      public static void clearCaches(Context context) {
          ImageDownloader.clearCache(context);
      }
  
      public static void deleteDirectory(File directoryOrFile) {
          if (!directoryOrFile.exists()) {
              return;
          }
  
          if (directoryOrFile.isDirectory()) {
              for (File child : directoryOrFile.listFiles()) {
                  deleteDirectory(child);
              }
          }
          directoryOrFile.delete();
      }
  
      public static <T> List<T> asListNoNulls(T... array) {
          ArrayList<T> result = new ArrayList<T>();
          for (T t : array) {
              if (t != null) {
                  result.add(t);
              }
          }
          return result;
      }
  
      // Return a hash of the android_id combined with the appid.  Intended to dedupe requests on the server side
      // in order to do counting of users unknown to Facebook.  Because we put the appid into the key prior to hashing,
      // we cannot do correlation of the same user across multiple apps -- this is intentional.  When we transition to
      // the Google advertising ID, we'll get rid of this and always send that up.
      public static String getHashedDeviceAndAppID(Context context, String applicationId) {
          String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
  
          if (androidId == null) {
              return null;
          } else {
              return sha1hash(androidId + applicationId);
          }
      }
  
      public static void setAppEventAttributionParameters(GraphObject params,
              AttributionIdentifiers attributionIdentifiers, String hashedDeviceAndAppId, boolean limitEventUsage) {
          // Send attributionID if it exists, otherwise send a hashed device+appid specific value as the advertiser_id.
          if (attributionIdentifiers != null && attributionIdentifiers.getAttributionId() != null) {
              params.setProperty("attribution", attributionIdentifiers.getAttributionId());
          }
  
          if (attributionIdentifiers != null && attributionIdentifiers.getAndroidAdvertiserId() != null) {
              params.setProperty("advertiser_id", attributionIdentifiers.getAndroidAdvertiserId());
              params.setProperty("advertiser_tracking_enabled", !attributionIdentifiers.isTrackingLimited());
          } else if (hashedDeviceAndAppId != null) {
              params.setProperty("advertiser_id", hashedDeviceAndAppId);
          }
  
          params.setProperty("application_tracking_enabled", !limitEventUsage);
      }
  
      public static Method getMethodQuietly(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
          try {
              return clazz.getMethod(methodName, parameterTypes);
          } catch (NoSuchMethodException ex) {
              return null;
          }
      }
  
      public static Method getMethodQuietly(String className, String methodName, Class<?>... parameterTypes) {
          try {
              Class<?> clazz = Class.forName(className);
              return getMethodQuietly(clazz, methodName, parameterTypes);
          } catch (ClassNotFoundException ex) {
              return null;
          }
      }
  
      public static Object invokeMethodQuietly(Object receiver, Method method, Object... args) {
          try {
              return method.invoke(receiver, args);
          } catch (IllegalAccessException ex) {
              return null;
          } catch (InvocationTargetException ex) {
              return null;
          }
      }
  }
  