/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.util.Log
import com.facebook.FacebookSdk
import com.facebook.GraphRequest
import com.facebook.appevents.codeless.internal.UnityReflection
import com.facebook.appevents.internal.AutomaticAnalyticsLogger
import com.facebook.appevents.internal.Constants
import com.facebook.core.BuildConfig
import com.facebook.internal.FetchedAppGateKeepersManager.queryAppGateKeepers
import com.facebook.internal.InternalSettings.isUnityApp
import com.facebook.internal.SmartLoginOption.Companion.parseOptions
import com.facebook.internal.Utility.isNullOrEmpty
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicReference
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject

/**
 * 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.
 */
object FetchedAppSettingsManager {
  private val TAG = FetchedAppSettingsManager::class.java.simpleName
  private const val APP_SETTINGS_PREFS_STORE = "com.facebook.internal.preferences.APP_SETTINGS"
  private const val APP_SETTINGS_PREFS_KEY_FORMAT = "com.facebook.internal.APP_SETTINGS.%s"
  private const val APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING = "supports_implicit_sdk_logging"
  private const val APP_SETTING_NUX_CONTENT = "gdpv4_nux_content"
  private const val APP_SETTING_NUX_ENABLED = "gdpv4_nux_enabled"
  private const val APP_SETTING_DIALOG_CONFIGS = "android_dialog_configs"
  private const val APP_SETTING_ANDROID_SDK_ERROR_CATEGORIES = "android_sdk_error_categories"
  private const val APP_SETTING_APP_EVENTS_SESSION_TIMEOUT = "app_events_session_timeout"
  private const val APP_SETTING_APP_EVENTS_FEATURE_BITMASK = "app_events_feature_bitmask"
  private const val APP_SETTING_APP_EVENTS_EVENT_BINDINGS = "auto_event_mapping_android"
  private const val APP_SETTING_RESTRICTIVE_EVENT_FILTER_FIELD = "restrictive_data_filter_params"
  private const val AUTOMATIC_LOGGING_ENABLED_BITMASK_FIELD = 1 shl 3

  // The second bit of app_events_feature_bitmask is used for iOS in-app purchase automatic
  // logging, while the fourth bit is used for Android in-app purchase automatic logging.
  private const val IAP_AUTOMATIC_LOGGING_ENABLED_BITMASK_FIELD = 1 shl 4
  private const val CODELESS_EVENTS_ENABLED_BITMASK_FIELD = 1 shl 5
  private const val TRACK_UNINSTALL_ENABLED_BITMASK_FIELD = 1 shl 8
  private const val MONITOR_ENABLED_BITMASK_FIELD = 1 shl 14
  private const val APP_SETTING_SMART_LOGIN_OPTIONS = "seamless_login"
  private const val SMART_LOGIN_BOOKMARK_ICON_URL = "smart_login_bookmark_icon_url"
  private const val SMART_LOGIN_MENU_ICON_URL = "smart_login_menu_icon_url"
  private const val SDK_UPDATE_MESSAGE = "sdk_update_message"
  private const val APP_SETTING_APP_EVENTS_AAM_RULE = "aam_rules"
  private const val SUGGESTED_EVENTS_SETTING = "suggested_events_setting"

  private const val PROTECTED_MODE_RULES = "protected_mode_rules"
  private const val STANDARD_PARAMS_KEY = "standard_params"
  private const val MACA_RULES_KEY = "maca_rules"

  private val APP_SETTING_FIELDS =
      listOf(
          APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING,
          APP_SETTING_NUX_CONTENT,
          APP_SETTING_NUX_ENABLED,
          APP_SETTING_DIALOG_CONFIGS,
          APP_SETTING_ANDROID_SDK_ERROR_CATEGORIES,
          APP_SETTING_APP_EVENTS_SESSION_TIMEOUT,
          APP_SETTING_APP_EVENTS_FEATURE_BITMASK,
          APP_SETTING_APP_EVENTS_EVENT_BINDINGS,
          APP_SETTING_SMART_LOGIN_OPTIONS,
          SMART_LOGIN_BOOKMARK_ICON_URL,
          SMART_LOGIN_MENU_ICON_URL,
          APP_SETTING_RESTRICTIVE_EVENT_FILTER_FIELD,
          APP_SETTING_APP_EVENTS_AAM_RULE,
          SUGGESTED_EVENTS_SETTING,
          PROTECTED_MODE_RULES)
  private const val APPLICATION_FIELDS = GraphRequest.FIELDS_PARAM
  private val fetchedAppSettings: MutableMap<String, FetchedAppSettings> = ConcurrentHashMap()
  private val loadingState = AtomicReference(FetchAppSettingState.NOT_LOADED)
  private val fetchedAppSettingsCallbacks = ConcurrentLinkedQueue<FetchedAppSettingsCallback>()
  private var printedSDKUpdatedMessage = false
  private var isUnityInit = false
  private var unityEventBindings: JSONArray? = null

  @JvmStatic
  fun loadAppSettingsAsync() {
    val context = FacebookSdk.getApplicationContext()
    val applicationId = FacebookSdk.getApplicationId()
    if (Utility.isNullOrEmpty(applicationId)) {
      loadingState.set(FetchAppSettingState.ERROR)
      pollCallbacks()
      return
    } else if (fetchedAppSettings.containsKey(applicationId)) {
      loadingState.set(FetchAppSettingState.SUCCESS)
      pollCallbacks()
      return
    }
    val canStartLoading =
        loadingState.compareAndSet(FetchAppSettingState.NOT_LOADED, FetchAppSettingState.LOADING) ||
            loadingState.compareAndSet(FetchAppSettingState.ERROR, FetchAppSettingState.LOADING)
    if (!canStartLoading) {
      pollCallbacks()
      return
    }
    val settingsKey = String.format(APP_SETTINGS_PREFS_KEY_FORMAT, applicationId)
    FacebookSdk.getExecutor().execute { // See if we had a cached copy and use that immediately.
      val sharedPrefs = context.getSharedPreferences(APP_SETTINGS_PREFS_STORE, Context.MODE_PRIVATE)
      val settingsJSONString = sharedPrefs.getString(settingsKey, null)
      var appSettings: FetchedAppSettings? = null
      if (!Utility.isNullOrEmpty(settingsJSONString)) {
        checkNotNull(settingsJSONString)
        var settingsJSON: JSONObject? = null
        try {
          settingsJSON = JSONObject(settingsJSONString)
        } catch (je: JSONException) {
          Utility.logd(Utility.LOG_TAG, je)
        }
        if (settingsJSON != null) {
          appSettings = parseAppSettingsFromJSON(applicationId, settingsJSON)
        }
      }
      val resultJSON = getAppSettingsQueryResponse(applicationId)
      if (resultJSON != null) {
        parseAppSettingsFromJSON(applicationId, resultJSON)
        sharedPrefs.edit().putString(settingsKey, resultJSON.toString()).apply()
      }

      // Print log to notify developers to upgrade SDK when version is too old
      if (appSettings != null) {
        val updateMessage = appSettings.sdkUpdateMessage
        if (!printedSDKUpdatedMessage && updateMessage != null && updateMessage.length > 0) {
          printedSDKUpdatedMessage = true
          Log.w(TAG, updateMessage)
        }
      }

      // Fetch GateKeepers
      queryAppGateKeepers(applicationId, true)

      // Start log activate & deactivate app events, in case autoLogAppEvents flag is set
      AutomaticAnalyticsLogger.logActivateAppEvent()

      loadingState.set(
          if (fetchedAppSettings.containsKey(applicationId)) FetchAppSettingState.SUCCESS
          else FetchAppSettingState.ERROR)
      pollCallbacks()
    }
  }

  // This call only gets the app settings if they're already fetched
  @JvmStatic
  fun getAppSettingsWithoutQuery(applicationId: String?): FetchedAppSettings? {
    return if (applicationId != null) fetchedAppSettings[applicationId] else null
  }

  /**
   * Run callback with app settings if available. It is possible that app settings take a while to
   * load due to latency or it is requested too early in the application lifecycle.
   *
   * @param callback Callback to be run after app settings are available
   */
  @JvmStatic
  fun getAppSettingsAsync(callback: FetchedAppSettingsCallback) {
    fetchedAppSettingsCallbacks.add(callback)
    loadAppSettingsAsync()
  }

  /**
   * Run all available callbacks and remove them. If app settings are available, run the success
   * callback, error otherwise.
   */
  @Synchronized
  private fun pollCallbacks() {
    val currentState = loadingState.get()
    if (FetchAppSettingState.NOT_LOADED == currentState ||
        FetchAppSettingState.LOADING == currentState) {
      return
    }
    val applicationId = FacebookSdk.getApplicationId()
    val appSettings = fetchedAppSettings[applicationId]
    val handler = Handler(Looper.getMainLooper())
    if (FetchAppSettingState.ERROR == currentState) {
      while (!fetchedAppSettingsCallbacks.isEmpty()) {
        val callback = fetchedAppSettingsCallbacks.poll()
        handler.post { callback.onError() }
      }
      return
    }
    while (!fetchedAppSettingsCallbacks.isEmpty()) {
      val callback = fetchedAppSettingsCallbacks.poll()
      handler.post { callback.onSuccess(appSettings) }
    }
  }

  // Note that this method makes a synchronous Graph API call, so should not be called from the
  // main thread. This call can block for long time if network is not available and network
  // timeout is long.
  @JvmStatic
  fun queryAppSettings(applicationId: String, forceRequery: Boolean): FetchedAppSettings? {
    // Cache the last app checked results.
    if (!forceRequery && fetchedAppSettings.containsKey(applicationId)) {
      return fetchedAppSettings[applicationId]
    }
    val response = getAppSettingsQueryResponse(applicationId) ?: return null
    val fetchedAppSettings = parseAppSettingsFromJSON(applicationId, response)
    if (applicationId == FacebookSdk.getApplicationId()) {
      loadingState.set(FetchAppSettingState.SUCCESS)
      pollCallbacks()
    }
    return fetchedAppSettings
  }

  internal fun parseAppSettingsFromJSON(
      applicationId: String,
      settingsJSON: JSONObject
  ): FetchedAppSettings {
    val errorClassificationJSON =
        settingsJSON.optJSONArray(APP_SETTING_ANDROID_SDK_ERROR_CATEGORIES)
    val errorClassification =
        FacebookRequestErrorClassification.createFromJSON(errorClassificationJSON)
            ?: FacebookRequestErrorClassification.defaultErrorClassification
    val featureBitmask = settingsJSON.optInt(APP_SETTING_APP_EVENTS_FEATURE_BITMASK, 0)
    val automaticLoggingEnabled = featureBitmask and AUTOMATIC_LOGGING_ENABLED_BITMASK_FIELD != 0
    val inAppPurchaseAutomaticLoggingEnabled =
        featureBitmask and IAP_AUTOMATIC_LOGGING_ENABLED_BITMASK_FIELD != 0
    val codelessEventsEnabled = featureBitmask and CODELESS_EVENTS_ENABLED_BITMASK_FIELD != 0
    val trackUninstallEnabled = featureBitmask and TRACK_UNINSTALL_ENABLED_BITMASK_FIELD != 0
    val monitorEnabled = featureBitmask and MONITOR_ENABLED_BITMASK_FIELD != 0
    val eventBindings = settingsJSON.optJSONArray(APP_SETTING_APP_EVENTS_EVENT_BINDINGS)
    unityEventBindings = eventBindings
    if (unityEventBindings != null && isUnityApp) {
      UnityReflection.sendEventMapping(eventBindings?.toString())
    }
    val result =
        FetchedAppSettings(
            settingsJSON.optBoolean(APP_SETTING_SUPPORTS_IMPLICIT_SDK_LOGGING, false),
            settingsJSON.optString(APP_SETTING_NUX_CONTENT, ""),
            settingsJSON.optBoolean(APP_SETTING_NUX_ENABLED, false),
            settingsJSON.optInt(
                APP_SETTING_APP_EVENTS_SESSION_TIMEOUT,
                Constants.getDefaultAppEventsSessionTimeoutInSeconds()),
            parseOptions(settingsJSON.optLong(APP_SETTING_SMART_LOGIN_OPTIONS)),
            parseDialogConfigurations(settingsJSON.optJSONObject(APP_SETTING_DIALOG_CONFIGS)),
            automaticLoggingEnabled,
            errorClassification,
            settingsJSON.optString(SMART_LOGIN_BOOKMARK_ICON_URL),
            settingsJSON.optString(SMART_LOGIN_MENU_ICON_URL),
            inAppPurchaseAutomaticLoggingEnabled,
            codelessEventsEnabled,
            eventBindings,
            settingsJSON.optString(SDK_UPDATE_MESSAGE),
            trackUninstallEnabled,
            monitorEnabled,
            settingsJSON.optString(APP_SETTING_APP_EVENTS_AAM_RULE),
            settingsJSON.optString(SUGGESTED_EVENTS_SETTING),
            settingsJSON.optString(APP_SETTING_RESTRICTIVE_EVENT_FILTER_FIELD),
            parseProtectedModeRules(settingsJSON.optJSONObject(PROTECTED_MODE_RULES), STANDARD_PARAMS_KEY),
            parseProtectedModeRules(settingsJSON.optJSONObject(PROTECTED_MODE_RULES), MACA_RULES_KEY),
        )
    fetchedAppSettings[applicationId] = result
    return result
  }

  @JvmStatic
  fun setIsUnityInit(flag: Boolean) {
    isUnityInit = flag
    if (unityEventBindings != null && isUnityInit) {
      UnityReflection.sendEventMapping(unityEventBindings.toString())
    }
  }

  // Note that this method makes a synchronous Graph API call, so should not be called from the
  // main thread. This call can block for long time if network is not available and network
  // timeout is long.
  private fun getAppSettingsQueryResponse(applicationId: String): JSONObject {
    val appSettingsParams = Bundle()
    val appSettingFields = mutableListOf<String>().apply { addAll(APP_SETTING_FIELDS) }
    if (BuildConfig.DEBUG) {
      appSettingFields.add(SDK_UPDATE_MESSAGE)
    }
    appSettingsParams.putString(APPLICATION_FIELDS, TextUtils.join(",", appSettingFields))
    val request = GraphRequest.newGraphPathRequest(null, "app", null)
    request.setForceApplicationRequest(true)
    request.parameters = appSettingsParams

    return request.executeAndWait().jsonObject ?: JSONObject()
  }

  private fun parseDialogConfigurations(
      dialogConfigResponse: JSONObject?
  ): Map<String, MutableMap<String, FetchedAppSettings.DialogFeatureConfig>> {
    val dialogConfigMap =
        HashMap<String, MutableMap<String, FetchedAppSettings.DialogFeatureConfig>>()
    if (dialogConfigResponse != null) {
      val dialogConfigData = dialogConfigResponse.optJSONArray("data")
      if (dialogConfigData != null) {
        for (i in 0 until dialogConfigData.length()) {
          val dialogConfig =
              FetchedAppSettings.DialogFeatureConfig.parseDialogConfig(
                  dialogConfigData.optJSONObject(i))
                  ?: continue
          val dialogName = dialogConfig.dialogName
          var featureMap = dialogConfigMap[dialogName]
          if (featureMap == null) {
            featureMap = HashMap()
            dialogConfigMap[dialogName] = featureMap
          }
          featureMap[dialogConfig.featureName] = dialogConfig
        }
      }
    }
    return dialogConfigMap
  }

  private fun parseProtectedModeRules(
    protectedModeSettings: JSONObject?,
    ruleType: String,
  ): JSONArray? {
    var rule: JSONArray? = null
    if (protectedModeSettings != null) {
        rule = protectedModeSettings.optJSONArray(ruleType)
    }
    return rule
  }

  internal enum class FetchAppSettingState {
    NOT_LOADED,
    LOADING,
    SUCCESS,
    ERROR
  }

  interface FetchedAppSettingsCallback {
    fun onSuccess(fetchedAppSettings: FetchedAppSettings?)
    fun onError()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.util.Log
import com.facebook.FacebookSdk
import com.facebook.LoggingBehavior
import com.facebook.internal.Logger.Companion.log
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.FilenameFilter
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.InvalidParameterException
import java.util.Date
import java.util.PriorityQueue
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import org.json.JSONException
import org.json.JSONObject
import org.json.JSONTokener

// This class is intended to be thread-safe.
//
// There are two classes of files:  buffer files and cache files:
// - A buffer file is in the process of being written, and there is an open stream on the file.
//   These files are named as "bufferN" where N is an incrementing integer.  On startup, we delete
//   all existing files of this form. Once the stream is closed, we rename the buffer file to a
//   cache file or attempt to delete if this fails.  We do not otherwise ever attempt to delete
//   these files.
// - A cache file is a non-changing file that is named by the md5 hash of the cache key.  We monitor
//   the size of these files in aggregate and remove the oldest one(s) to stay under quota.  This
//   process does not block threads calling into this class, so theoretically we could go
//   arbitrarily over quota but in practice this should not happen because deleting files should be
//   much cheaper than downloading new file content.
//
// Since there can only ever be one thread accessing a particular buffer file, we do not synchronize
// access to these. We do assume that file rename is atomic when converting a buffer file to a cache
// file, and that if multiple files are renamed to a single target that exactly one of them
// continues to exist.
//
// Standard POSIX file semantics guarantee being able to continue to use a file handle even after
// the corresponding file has been deleted.  Given this and that cache files never change other than
// deleting in trim() or clear(),  we only have to ensure that there is at most one trim() or
// clear() process deleting files at any given time.
/**
 * 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.
 */
class FileLruCache(private val tag: String, private val limits: Limits) {
  private val directory: File = File(FacebookSdk.getCacheDir(), tag)
  private var isTrimPending = false
  private var isTrimInProgress = false
  private val lock = ReentrantLock()
  private val condition = lock.newCondition()
  private val lastClearCacheTime = AtomicLong(0)

  // This is not robust to files changing dynamically underneath it and should therefore only be
  // used for test code.  If we ever need this for product code we need to think through
  // synchronization.
  // See the threading notes at the top of this class.
  //
  // Also, since trim() runs asynchronously now, this blocks until any pending trim has completed.
  fun sizeInBytesForTest(): Long {
    lock.withLock {
      while (isTrimPending || isTrimInProgress) {
        try {
          condition.await()
        } catch (e: InterruptedException) {
          // intentional no-op
        }
      }
    }
    val files = directory.listFiles()
    var total: Long = 0
    if (files != null) {
      for (file in files) {
        total += file.length()
      }
    }
    return total
  }

  @JvmOverloads
  @Throws(IOException::class)
  operator fun get(key: String, contentTag: String? = null): InputStream? {
    val file = File(directory, Utility.md5hash(key))
    val input: FileInputStream
    input =
        try {
          FileInputStream(file)
        } catch (e: IOException) {
          return null
        }
    val buffered = BufferedInputStream(input, Utility.DEFAULT_STREAM_BUFFER_SIZE)
    var success = false
    return try {
      val header = StreamHeader.readHeader(buffered) ?: return null
      val foundKey = header.optString(HEADER_CACHEKEY_KEY)
      if (foundKey != key) {
        return null
      }
      val headerContentTag = header.optString(HEADER_CACHE_CONTENT_TAG_KEY, null)
      if (contentTag == null && contentTag != headerContentTag) {
        return null
      }
      val accessTime = Date().time
      log(
          LoggingBehavior.CACHE,
          TAG,
          "Setting lastModified to " + java.lang.Long.valueOf(accessTime) + " for " + file.name)
      file.setLastModified(accessTime)
      success = true
      buffered
    } finally {
      if (!success) {
        buffered.close()
      }
    }
  }

  @JvmOverloads
  @Throws(IOException::class)
  fun openPutStream(key: String, contentTag: String? = null): OutputStream {
    val buffer = BufferFile.newFile(directory)
    buffer.delete()
    if (!buffer.createNewFile()) {
      throw IOException("Could not create file at " + buffer.absolutePath)
    }
    val file: FileOutputStream
    file =
        try {
          FileOutputStream(buffer)
        } catch (e: FileNotFoundException) {
          log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating buffer output stream: $e")
          throw IOException(e.message)
        }
    val bufferFileCreateTime = System.currentTimeMillis()
    val renameToTargetCallback: StreamCloseCallback =
        object : StreamCloseCallback {
          override fun onClose() {
            // if the buffer file was created before the cache was cleared, then the buffer file
            // should be deleted rather than renamed and saved.
            if (bufferFileCreateTime < lastClearCacheTime.get()) {
              buffer.delete()
            } else {
              renameToTargetAndTrim(key, buffer)
            }
          }
        }
    val cleanup = CloseCallbackOutputStream(file, renameToTargetCallback)
    val buffered = BufferedOutputStream(cleanup, Utility.DEFAULT_STREAM_BUFFER_SIZE)
    var success = false
    return try {
      // Prefix the stream with the actual key, since there could be collisions
      val header = JSONObject()
      header.put(HEADER_CACHEKEY_KEY, key)
      if (!Utility.isNullOrEmpty(contentTag)) {
        header.put(HEADER_CACHE_CONTENT_TAG_KEY, contentTag)
      }
      StreamHeader.writeHeader(buffered, header)
      success = true
      buffered
    } catch (e: JSONException) {
      // JSON is an implementation detail of the cache, so don't let JSON exceptions out.
      log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating JSON header for cache file: $e")
      throw IOException(e.message)
    } finally {
      if (!success) {
        buffered.close()
      }
    }
  }

  fun clearCache() {
    // get the current directory listing of files to delete
    val filesToDelete = directory.listFiles(BufferFile.excludeBufferFiles())
    lastClearCacheTime.set(System.currentTimeMillis())
    if (filesToDelete != null) {
      FacebookSdk.getExecutor().execute {
        for (file in filesToDelete) {
          file.delete()
        }
      }
    }
  }

  /**
   * The location of the cache.
   *
   * @return The location of the cache.
   */
  val location: String
    get() = directory.path

  private fun renameToTargetAndTrim(key: String, buffer: File) {
    val target = File(directory, Utility.md5hash(key))

    // This is triggered by close().  By the time close() returns, the file should be cached, so
    // this needs to happen synchronously on this thread.
    //
    // However, it does not need to be synchronized, since in the race we will just start an
    // unnecessary trim operation.  Avoiding the cost of holding the lock across the file
    // operation seems worth this cost.
    if (!buffer.renameTo(target)) {
      buffer.delete()
    }
    postTrim()
  }

  // Opens an output stream for the key, and creates an input stream wrapper to copy
  // the contents of input into the new output stream.  The effect is to store a
  // copy of input, and associate that data with key.
  @Throws(IOException::class)
  fun interceptAndPut(key: String, input: InputStream): InputStream {
    val output = openPutStream(key)
    return CopyingInputStream(input, output)
  }

  override fun toString(): String {
    return "{FileLruCache:" + " tag:" + tag + " file:" + directory.name + "}"
  }

  private fun postTrim() {
    lock.withLock {
      if (!isTrimPending) {
        isTrimPending = true
        FacebookSdk.getExecutor().execute { trim() }
      }
    }
  }

  private fun trim() {
    lock.withLock {
      isTrimPending = false
      isTrimInProgress = true
    }
    try {
      log(LoggingBehavior.CACHE, TAG, "trim started")
      val heap = PriorityQueue<ModifiedFile>()
      var size: Long = 0
      var count: Long = 0
      val filesToTrim = directory.listFiles(BufferFile.excludeBufferFiles())
      if (filesToTrim != null) {
        for (file in filesToTrim) {
          val modified = ModifiedFile(file)
          heap.add(modified)
          log(
              LoggingBehavior.CACHE,
              TAG,
              "  trim considering time=" +
                  java.lang.Long.valueOf(modified.modified) +
                  " name=" +
                  modified.file.name)
          size += file.length()
          count++
        }
      }
      while (size > limits.byteCount || count > limits.fileCount) {
        val file = heap.remove().file
        log(LoggingBehavior.CACHE, TAG, "  trim removing " + file.name)
        size -= file.length()
        count--
        file.delete()
      }
    } finally {
      lock.withLock {
        isTrimInProgress = false
        condition.signalAll()
      }
    }
  }

  private object BufferFile {
    private const val FILE_NAME_PREFIX = "buffer"
    private val filterExcludeBufferFiles = FilenameFilter { dir, filename ->
      !filename.startsWith(FILE_NAME_PREFIX)
    }
    private val filterExcludeNonBufferFiles = FilenameFilter { dir, filename ->
      filename.startsWith(FILE_NAME_PREFIX)
    }
    fun deleteAll(root: File) {
      val filesToDelete = root.listFiles(excludeNonBufferFiles())
      if (filesToDelete != null) {
        for (file in filesToDelete) {
          file.delete()
        }
      }
    }

    fun excludeBufferFiles(): FilenameFilter = filterExcludeBufferFiles

    fun excludeNonBufferFiles(): FilenameFilter = filterExcludeNonBufferFiles

    fun newFile(root: File?): File {
      val name = FILE_NAME_PREFIX + java.lang.Long.valueOf(bufferIndex.incrementAndGet()).toString()
      return File(root, name)
    }
  }

  // Treats the first part of a stream as a header, reads/writes it as a JSON blob, and
  // leaves the stream positioned exactly after the header.
  //
  // The format is as follows:
  //     byte: meaning
  // ---------------------------------
  //        0: version number
  //      1-3: big-endian JSON header blob size
  // 4-size+4: UTF-8 JSON header blob
  //      ...: stream data
  private object StreamHeader {
    private const val HEADER_VERSION = 0
    @Throws(IOException::class)
    fun writeHeader(stream: OutputStream, header: JSONObject) {
      val headerString = header.toString()
      val headerBytes = headerString.toByteArray()

      // Write version number and big-endian header size
      stream.write(HEADER_VERSION)
      stream.write(headerBytes.size shr 16 and 0xff)
      stream.write(headerBytes.size shr 8 and 0xff)
      stream.write(headerBytes.size shr 0 and 0xff)
      stream.write(headerBytes)
    }

    @Throws(IOException::class)
    fun readHeader(stream: InputStream): JSONObject? {
      val version = stream.read()
      if (version != HEADER_VERSION) {
        return null
      }
      var headerSize = 0
      repeat(3) {
        val b = stream.read()
        if (b == -1) {
          log(
              LoggingBehavior.CACHE,
              TAG,
              "readHeader: stream.read returned -1 while reading header size")
          return null
        }
        headerSize = headerSize shl 8
        headerSize += b and 0xff
      }
      val headerBytes = ByteArray(headerSize)
      var count = 0
      while (count < headerBytes.size) {
        val readCount = stream.read(headerBytes, count, headerBytes.size - count)
        if (readCount < 1) {
          log(
              LoggingBehavior.CACHE,
              TAG,
              "readHeader: stream.read stopped at " +
                  Integer.valueOf(count) +
                  " when expected " +
                  headerBytes.size)
          return null
        }
        count += readCount
      }
      val headerString = String(headerBytes)
      val header: JSONObject
      val tokener = JSONTokener(headerString)
      header =
          try {
            val parsed = tokener.nextValue()
            if (parsed !is JSONObject) {
              log(
                  LoggingBehavior.CACHE,
                  TAG,
                  "readHeader: expected JSONObject, got " + parsed.javaClass.canonicalName)
              return null
            }
            parsed
          } catch (e: JSONException) {
            throw IOException(e.message)
          }
      return header
    }
  }

  private class CloseCallbackOutputStream
  constructor(val innerStream: OutputStream, val callback: StreamCloseCallback) : OutputStream() {
    @Throws(IOException::class)
    override fun close() {
      try {
        innerStream.close()
      } finally {
        callback.onClose()
      }
    }

    @Throws(IOException::class)
    override fun flush() {
      innerStream.flush()
    }

    @Throws(IOException::class)
    override fun write(buffer: ByteArray, offset: Int, count: Int) {
      innerStream.write(buffer, offset, count)
    }

    @Throws(IOException::class)
    override fun write(buffer: ByteArray) {
      innerStream.write(buffer)
    }

    @Throws(IOException::class)
    override fun write(oneByte: Int) {
      innerStream.write(oneByte)
    }
  }

  private class CopyingInputStream constructor(val input: InputStream, val output: OutputStream) :
      InputStream() {
    @Throws(IOException::class)
    override fun available(): Int {
      return input.available()
    }

    @Throws(IOException::class)
    override fun close() {
      // According to http://www.cs.cornell.edu/andru/javaspec/11.doc.html:
      //  "If a finally clause is executed because of abrupt completion of a try block and the
      //   finally clause itself completes abruptly, then the reason for the abrupt completion
      //   of the try block is discarded and the new reason for abrupt completion is
      //   propagated from there."
      //
      // Android does appear to behave like this.
      try {
        input.close()
      } finally {
        output.close()
      }
    }

    override fun mark(readlimit: Int) {
      throw UnsupportedOperationException()
    }

    override fun markSupported(): Boolean = false

    @Throws(IOException::class)
    override fun read(buffer: ByteArray): Int {
      val count = input.read(buffer)
      if (count > 0) {
        output.write(buffer, 0, count)
      }
      return count
    }

    @Throws(IOException::class)
    override fun read(): Int {
      val b = input.read()
      if (b >= 0) {
        output.write(b)
      }
      return b
    }

    @Throws(IOException::class)
    override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
      val count = input.read(buffer, offset, length)
      if (count > 0) {
        output.write(buffer, offset, count)
      }
      return count
    }

    @Synchronized
    override fun reset() {
      throw UnsupportedOperationException()
    }

    @Throws(IOException::class)
    override fun skip(byteCount: Long): Long {
      val buffer = ByteArray(1024)
      var total: Long = 0
      while (total < byteCount) {
        val count = read(buffer, 0, Math.min(byteCount - total, buffer.size.toLong()).toInt())
        if (count < 0) {
          return total
        }
        total += count.toLong()
      }
      return total
    }
  }

  class Limits {
    // A Samsung Galaxy Nexus can create 1k files in half a second.  By the time
    // it gets to 5k files it takes 5 seconds.  10k files took 15 seconds.  This
    // continues to slow down as files are added.  This assumes all files are in
    // a single directory.
    //
    // Following a git-like strategy where we partition MD5-named files based on
    // the first 2 characters is slower across the board.
    var byteCount: Int = 1024 * 1024
      set(value) {
        if (value < 0) {
          throw InvalidParameterException("Cache byte-count limit must be >= 0")
        }
        field = value
      }
    var fileCount = 1024
      set(value) {
        if (value < 0) {
          throw InvalidParameterException("Cache file count limit must be >= 0")
        }
        field = value
      }
  }

  // Caches the result of lastModified during sort/heap operations
  private class ModifiedFile constructor(val file: File) : Comparable<ModifiedFile> {
    val modified = file.lastModified()
    override fun compareTo(another: ModifiedFile): Int {
      return when {
        modified < another.modified -> -1
        modified > another.modified -> 1
        else -> file.compareTo(another.file)
      }
    }

    override fun equals(another: Any?): Boolean {
      return another is ModifiedFile && compareTo(another) == 0
    }

    override fun hashCode(): Int {
      var result = HASH_SEED
      result = result * HASH_MULTIPLIER + file.hashCode()
      result = result * HASH_MULTIPLIER + (modified % Int.MAX_VALUE).toInt()
      return result
    }

    companion object {
      private const val HASH_SEED = 29 // Some random prime number
      private const val HASH_MULTIPLIER = 37 // Some random prime number
    }
  }

  private fun interface StreamCloseCallback {
    fun onClose()
  }

  companion object {
    val TAG = FileLruCache::class.java.simpleName
    private const val HEADER_CACHEKEY_KEY = "key"
    private const val HEADER_CACHE_CONTENT_TAG_KEY = "tag"
    private val bufferIndex = AtomicLong()
  }

  // The value of tag should be a final String that works as a directory name.
  init {
    // Ensure the cache dir exists
    if (directory.mkdirs() || directory.isDirectory) {
      // Remove any stale partially-written files from a previous run
      BufferFile.deleteAll(directory)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Handler
import android.os.Looper
import androidx.annotation.VisibleForTesting
import com.facebook.FacebookException
import com.facebook.internal.ImageResponseCache.getCachedImageStream
import com.facebook.internal.ImageResponseCache.interceptAndCacheImageStream
import com.facebook.internal.UrlRedirectCache.cacheUriRedirect
import com.facebook.internal.UrlRedirectCache.getRedirectedUri
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL

/**
 * 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.
 */
object ImageDownloader {
  private const val DOWNLOAD_QUEUE_MAX_CONCURRENT = WorkQueue.DEFAULT_MAX_CONCURRENT
  private const val CACHE_READ_QUEUE_MAX_CONCURRENT = 2

  @get:Synchronized
  private var handler: Handler? = null
    private get() {
      if (field == null) {
        field = Handler(Looper.getMainLooper())
      }
      return field
    }
  private val downloadQueue = WorkQueue(DOWNLOAD_QUEUE_MAX_CONCURRENT)
  private val cacheReadQueue = WorkQueue(CACHE_READ_QUEUE_MAX_CONCURRENT)
  private val pendingRequests: MutableMap<RequestKey, DownloaderContext> = HashMap()

  /**
   * Downloads the image specified in the passed in request. If a callback is specified, it is
   * guaranteed to be invoked on the calling thread.
   *
   * @param request Request to process
   */
  @JvmStatic
  fun downloadAsync(request: ImageRequest?) {
    if (request == null) {
      return
    }

    // NOTE: This is the ONLY place where the original request's Url is read. From here on,
    // we will keep track of the Url separately. This is because we might be dealing with a
    // redirect response and the Url might change. We can't create our own new ImageRequests
    // for these changed Urls since the caller might be doing some book-keeping with the
    // requests object reference. So we keep the old references and just map them to new urls in
    // the downloader.
    val key = RequestKey(request.imageUri, request.callerTag)
    synchronized(pendingRequests) {
      val downloaderContext = pendingRequests[key]
      if (downloaderContext != null) {
        downloaderContext.request = request
        downloaderContext.isCancelled = false
        downloaderContext.workItem?.moveToFront()
      } else {
        enqueueCacheRead(request, key, request.isCachedRedirectAllowed)
      }
    }
  }

  @JvmStatic
  fun cancelRequest(request: ImageRequest): Boolean {
    var cancelled = false
    val key = RequestKey(request.imageUri, request.callerTag)
    synchronized(pendingRequests) {
      val downloaderContext = pendingRequests[key]
      if (downloaderContext != null) {
        // If we were able to find the request in our list of pending requests, then we will
        // definitely be able to prevent an ImageResponse from being issued. This is
        // regardless of whether a cache-read or network-download is underway for this
        // request.
        cancelled = true
        val workItem = downloaderContext.workItem
        if (workItem !== null && workItem.cancel()) {
          pendingRequests.remove(key)
        } else {
          // May be attempting a cache-read right now. So keep track of the cancellation
          // to prevent network calls etc
          downloaderContext.isCancelled = true
        }
      }
    }
    return cancelled
  }

  @JvmStatic
  fun prioritizeRequest(request: ImageRequest) {
    val key = RequestKey(request.imageUri, request.callerTag)
    synchronized(pendingRequests) {
      val downloaderContext = pendingRequests[key]
      if (downloaderContext != null) {
        downloaderContext.workItem?.moveToFront()
      }
    }
  }

  @JvmStatic
  fun clearCache() {
    ImageResponseCache.clearCache()
    UrlRedirectCache.clearCache()
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  fun getPendingRequests(): Map<RequestKey, DownloaderContext> = pendingRequests

  private fun enqueueCacheRead(
      request: ImageRequest,
      key: RequestKey,
      allowCachedRedirects: Boolean
  ) {
    enqueueRequest(request, key, cacheReadQueue, CacheReadWorkItem(key, allowCachedRedirects))
  }

  private fun enqueueDownload(request: ImageRequest, key: RequestKey) {
    enqueueRequest(request, key, downloadQueue, DownloadImageWorkItem(key))
  }

  private fun enqueueRequest(
      request: ImageRequest,
      key: RequestKey,
      workQueue: WorkQueue,
      workItem: Runnable
  ) {
    synchronized(pendingRequests) {
      val downloaderContext = DownloaderContext(request)
      pendingRequests[key] = downloaderContext

      // The creation of the WorkItem should be done after the pending request has been
      // registered. This is necessary since the WorkItem might kick off right away and
      // attempt to retrieve the request's DownloaderContext prior to it being ready for
      // access.
      //
      // It is also necessary to hold on to the lock until after the workItem is created,
      // since calls to cancelRequest or prioritizeRequest might come in and expect a
      // registered request to have a workItem available as well.
      downloaderContext.workItem = workQueue.addActiveWorkItem(workItem)
    }
  }

  private fun issueResponse(
      key: RequestKey,
      error: Exception?,
      bitmap: Bitmap?,
      isCachedRedirect: Boolean
  ) {
    // Once the old downloader context is removed, we are thread-safe since this is the
    // only reference to it
    val completedRequestContext = removePendingRequest(key)
    if (completedRequestContext != null && !completedRequestContext.isCancelled) {
      val request = completedRequestContext.request
      val callback = request?.callback
      if (callback != null) {
        handler?.post(
            Runnable {
              val response = ImageResponse(request, error, isCachedRedirect, bitmap)
              callback.onCompleted(response)
            })
      }
    }
  }

  private fun readFromCache(key: RequestKey, allowCachedRedirects: Boolean) {
    var cachedStream: InputStream? = null
    var isCachedRedirect = false
    if (allowCachedRedirects) {
      val redirectUri = getRedirectedUri(key.uri)
      if (redirectUri != null) {
        cachedStream = getCachedImageStream(redirectUri)
        isCachedRedirect = cachedStream != null
      }
    }
    if (!isCachedRedirect) {
      cachedStream = getCachedImageStream(key.uri)
    }
    if (cachedStream != null) {
      // We were able to find a cached image.
      val bitmap = BitmapFactory.decodeStream(cachedStream)
      Utility.closeQuietly(cachedStream)
      issueResponse(key, null, bitmap, isCachedRedirect)
    } else {
      // Once the old downloader context is removed, we are thread-safe since this is the
      // only reference to it
      val downloaderContext = removePendingRequest(key)
      val request = downloaderContext?.request
      if (downloaderContext != null && !downloaderContext.isCancelled && request !== null) {
        enqueueDownload(request, key)
      }
    }
  }

  private fun download(key: RequestKey) {
    var connection: HttpURLConnection? = null
    var stream: InputStream? = null
    var error: Exception? = null
    var bitmap: Bitmap? = null
    var issueResponse = true
    try {
      val url = URL(key.uri.toString())
      connection = url.openConnection() as HttpURLConnection
      connection.instanceFollowRedirects = false
      when (connection.responseCode) {
        HttpURLConnection.HTTP_MOVED_PERM,
        HttpURLConnection.HTTP_MOVED_TEMP -> {
          // redirect. So we need to perform further requests
          issueResponse = false
          val redirectLocation = connection.getHeaderField("location")
          if (!Utility.isNullOrEmpty(redirectLocation)) {
            val redirectUri = Uri.parse(redirectLocation)
            cacheUriRedirect(key.uri, redirectUri)

            // Once the old downloader context is removed, we are thread-safe since this
            // is the only reference to it
            val downloaderContext = removePendingRequest(key)
            if (downloaderContext != null && !downloaderContext.isCancelled) {
              enqueueCacheRead(downloaderContext.request, RequestKey(redirectUri, key.tag), false)
            }
          }
        }
        HttpURLConnection.HTTP_OK -> {
          // image should be available
          stream = interceptAndCacheImageStream(connection)
          bitmap = BitmapFactory.decodeStream(stream)
        }
        else -> {
          stream = connection.errorStream
          val errorMessageBuilder = StringBuilder()
          if (stream != null) {
            val reader = InputStreamReader(stream)
            val buffer = CharArray(128)
            var bufferLength: Int
            while (reader.read(buffer, 0, buffer.size).also { bufferLength = it } > 0) {
              errorMessageBuilder.append(buffer, 0, bufferLength)
            }
            Utility.closeQuietly(reader)
          } else {
            errorMessageBuilder.append("Unexpected error while downloading an image.")
          }
          error = FacebookException(errorMessageBuilder.toString())
        }
      }
    } catch (e: IOException) {
      error = e
    } finally {
      Utility.closeQuietly(stream)
      Utility.disconnectQuietly(connection)
    }
    if (issueResponse) {
      issueResponse(key, error, bitmap, false)
    }
  }

  private fun removePendingRequest(key: RequestKey): DownloaderContext? {
    synchronized(pendingRequests) {
      return pendingRequests.remove(key)
    }
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  class RequestKey(var uri: Uri, var tag: Any) {
    override fun hashCode(): Int {
      var result = HASH_SEED
      result = result * HASH_MULTIPLIER + uri.hashCode()
      result = result * HASH_MULTIPLIER + tag.hashCode()
      return result
    }

    override fun equals(o: Any?): Boolean {
      var isEqual = false
      if (o != null && o is RequestKey) {
        val compareTo = o
        isEqual = compareTo.uri === uri && compareTo.tag === tag
      }
      return isEqual
    }

    companion object {
      private const val HASH_SEED = 29 // Some random prime number
      private const val HASH_MULTIPLIER = 37 // Some random prime number
    }
  }

  @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
  class DownloaderContext internal constructor(var request: ImageRequest) {
    var workItem: WorkQueue.WorkItem? = null
    var isCancelled = false
  }

  private class CacheReadWorkItem
  internal constructor(private val key: RequestKey, private val allowCachedRedirects: Boolean) :
      Runnable {
    override fun run() {
      readFromCache(key, allowCachedRedirects)
    }
  }

  private class DownloadImageWorkItem internal constructor(private val key: RequestKey) : Runnable {
    override fun run() {
      download(key)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.content.Context
import android.net.Uri
import android.util.Log
import com.facebook.FacebookSdk
import java.util.Locale

/**
 * 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.
 */
class ImageRequest
private constructor(
    val context: Context,
    val imageUri: Uri,
    val callback: Callback?,
    val allowCachedRedirects: Boolean,
    val callerTag: Any
) {
  fun interface Callback {
    /**
     * This method should always be called on the UI thread. ImageDownloader makes sure to do this
     * when it is responsible for issuing the ImageResponse
     *
     * @param response
     */
    fun onCompleted(response: ImageResponse?)
  }

  val isCachedRedirectAllowed: Boolean
    get() {
      return allowCachedRedirects
    }

  companion object {
    const val UNSPECIFIED_DIMENSION = 0
    private const val PATH = "%s/%s/picture"
    private const val HEIGHT_PARAM = "height"
    private const val WIDTH_PARAM = "width"
    private const val ACCESS_TOKEN_PARAM = "access_token"
    private const val MIGRATION_PARAM = "migration_overrides"
    private const val MIGRATION_VALUE = "{october_2012:true}"

    @JvmStatic
    fun getProfilePictureUri(userId: String?, width: Int, height: Int): Uri {
      return getProfilePictureUri(userId, width, height, "")
    }

    @JvmStatic
    fun getProfilePictureUri(userId: String?, width: Int, height: Int, accessToken: String?): Uri {
      var width = width
      var height = height
      Validate.notNullOrEmpty(userId, "userId")
      width = Math.max(width, UNSPECIFIED_DIMENSION)
      height = Math.max(height, UNSPECIFIED_DIMENSION)
      require(!(width == UNSPECIFIED_DIMENSION && height == UNSPECIFIED_DIMENSION)) {
        "Either width or height must be greater than 0"
      }
      val builder =
          Uri.parse(ServerProtocol.getGraphUrlBase())
              .buildUpon()
              .path(String.format(Locale.US, PATH, FacebookSdk.getGraphApiVersion(), userId))
      if (height != UNSPECIFIED_DIMENSION) {
        builder.appendQueryParameter(HEIGHT_PARAM, height.toString())
      }
      if (width != UNSPECIFIED_DIMENSION) {
        builder.appendQueryParameter(WIDTH_PARAM, width.toString())
      }
      builder.appendQueryParameter(MIGRATION_PARAM, MIGRATION_VALUE)
      if (!Utility.isNullOrEmpty(accessToken)) {
        builder.appendQueryParameter(ACCESS_TOKEN_PARAM, accessToken)
      } else {
        if (!Utility.isNullOrEmpty(FacebookSdk.getClientToken()) &&
            !Utility.isNullOrEmpty(FacebookSdk.getApplicationId())) {
          builder.appendQueryParameter(
              ACCESS_TOKEN_PARAM,
              FacebookSdk.getApplicationId() + "|" + FacebookSdk.getClientToken())
        } else {
          Log.d(
              "ImageRequest",
              "Needs access token to fetch profile picture. Without an access token a default silhoutte picture is returned")
        }
      }
      return builder.build()
    }
  }

  data class Builder(
      private val context: Context,
      private val imageUri: Uri,
  ) {

    private var callback: Callback? = null
    private var allowCachedRedirects: Boolean = false
    private var callerTag: Any? = null

    fun setCallback(callback: Callback?): Builder {
      this.callback = callback
      return this
    }

    fun setCallerTag(callerTag: Any?): Builder {
      this.callerTag = callerTag
      return this
    }

    fun setAllowCachedRedirects(allowCachedRedirects: Boolean): Builder {
      this.allowCachedRedirects = allowCachedRedirects
      return this
    }

    fun build(): ImageRequest {
      return ImageRequest(
          context,
          imageUri,
          callback,
          allowCachedRedirects,
          if (callerTag == null) Any() else checkNotNull(callerTag))
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.graphics.Bitmap

/**
 * 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.
 */
class ImageResponse
internal constructor(
    val request: ImageRequest,
    val error: Exception?,
    val isCachedRedirect: Boolean,
    val bitmap: Bitmap?
)


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.net.Uri
import android.util.Log
import com.facebook.LoggingBehavior
import java.io.BufferedInputStream
import java.io.IOException
import java.io.InputStream
import java.net.HttpURLConnection

/**
 * 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.
 */
object ImageResponseCache {
  val TAG = ImageResponseCache::class.java.simpleName
  private lateinit var imageCache: FileLruCache

  @Synchronized
  @JvmStatic
  @Throws(IOException::class)
  fun getCache(): FileLruCache {
    if (!this::imageCache.isInitialized) {
      imageCache = FileLruCache(TAG, FileLruCache.Limits())
    }
    return imageCache
  }

  // Get stream from cache, or return null if the image is not cached.
  // Does not throw if there was an error.
  @JvmStatic
  fun getCachedImageStream(uri: Uri?): InputStream? {
    var imageStream: InputStream? = null
    if (uri != null) {
      if (isCDNURL(uri)) {
        try {
          val cache = getCache()
          imageStream = cache[uri.toString()]
        } catch (e: IOException) {
          Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, e.toString())
        }
      }
    }
    return imageStream
  }

  @JvmStatic
  @Throws(IOException::class)
  fun interceptAndCacheImageStream(connection: HttpURLConnection): InputStream? {
    var stream: InputStream? = null
    if (connection.responseCode == HttpURLConnection.HTTP_OK) {
      val uri = Uri.parse(connection.url.toString())
      stream = connection.inputStream // Default stream in case caching fails
      try {
        if (isCDNURL(uri)) {
          val cache = getCache()

          // Wrap stream with a caching stream
          stream =
              cache.interceptAndPut(uri.toString(), BufferedHttpInputStream(stream, connection))
        }
      } catch (e: IOException) {
        // Caching is best effort
      }
    }
    return stream
  }

  private fun isCDNURL(uri: Uri?): Boolean {
    if (uri != null) {
      val uriHost = uri.host
      if (uriHost != null) {
        if (uriHost == "fbcdn.net" || uriHost.endsWith(".fbcdn.net")) {
          return true
        }
        if (uriHost.startsWith("fbcdn") && uriHost.endsWith(".akamaihd.net")) {
          return true
        }
      }
    }
    return false
  }
  @JvmStatic
  fun clearCache() {
    try {
      getCache().clearCache()
    } catch (e: IOException) {
      Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "clearCache failed " + e.message)
    }
  }

  private class BufferedHttpInputStream
  internal constructor(stream: InputStream?, var connection: HttpURLConnection) :
      BufferedInputStream(stream, Utility.DEFAULT_STREAM_BUFFER_SIZE) {
    @Throws(IOException::class)
    override fun close() {
      super.close()
      Utility.disconnectQuietly(connection)
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.content.Context
import android.os.RemoteException
import com.android.installreferrer.api.InstallReferrerClient
import com.android.installreferrer.api.InstallReferrerClient.InstallReferrerResponse
import com.android.installreferrer.api.InstallReferrerStateListener
import com.android.installreferrer.api.ReferrerDetails
import com.facebook.FacebookSdk
import com.facebook.internal.instrument.crashshield.AutoHandleExceptions

object InstallReferrerUtil {
  private const val IS_REFERRER_UPDATED = "is_referrer_updated"

  @JvmStatic
  fun tryUpdateReferrerInfo(callback: Callback) {
    if (!isUpdated) {
      tryConnectReferrerInfo(callback)
    }
  }

  private fun tryConnectReferrerInfo(callback: Callback) {
    val referrerClient =
        InstallReferrerClient.newBuilder(FacebookSdk.getApplicationContext()).build()
    val installReferrerStateListener =
        object : InstallReferrerStateListener {
          @AutoHandleExceptions
          override fun onInstallReferrerSetupFinished(responseCode: Int) {
            when (responseCode) {
              InstallReferrerResponse.OK -> {
                val response: ReferrerDetails =
                    try {
                      referrerClient.installReferrer
                    } catch (e: RemoteException) {
                      return
                    }
                val referrerUrl = response.installReferrer
                if (referrerUrl != null &&
                    (referrerUrl.contains("fb") || referrerUrl.contains("facebook"))) {
                  callback.onReceiveReferrerUrl(referrerUrl)
                }
                // Even if we are not interested in the url, there is no reason to update again
                updateReferrer()
              }
              InstallReferrerResponse.FEATURE_NOT_SUPPORTED ->
                  updateReferrer() // No point retrying if feature not supported
              InstallReferrerResponse.SERVICE_UNAVAILABLE -> {}
            }
            try {
              referrerClient.endConnection()
            } catch (e: Exception) {
              // Silent endConnection errors for unit test and else
            }
          }

          override fun onInstallReferrerServiceDisconnected() = Unit
        }
    try {
      referrerClient.startConnection(installReferrerStateListener)
    } catch (e: Exception) {
      return
    }
  }

  private fun updateReferrer() {
    val preferences =
        FacebookSdk.getApplicationContext()
            .getSharedPreferences(FacebookSdk.APP_EVENT_PREFERENCES, Context.MODE_PRIVATE)
    preferences.edit().putBoolean(IS_REFERRER_UPDATED, true).apply()
  }

  private val isUpdated: Boolean
    get() {
      val preferences =
          FacebookSdk.getApplicationContext()
              .getSharedPreferences(FacebookSdk.APP_EVENT_PREFERENCES, Context.MODE_PRIVATE)
      return preferences.getBoolean(IS_REFERRER_UPDATED, false)
    }

  fun interface Callback {
    fun onReceiveReferrerUrl(s: String?)
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

object InternalSettings {
  /**
   * This value is used by the unity sdk to set the custom version. The user agent format is
   * sdk_version/custom_user_agent For example FBAndroidSDK.4.2.0/Unity.7.0.1
   */
  private const val UNITY_PREFIX = "Unity."

  @Volatile private var customUserAgent: String? = null

  @JvmStatic fun getCustomUserAgent(): String? = customUserAgent
  @JvmStatic
  fun setCustomUserAgent(value: String) {
    customUserAgent = value
  }

  @JvmStatic
  val isUnityApp: Boolean
    get() = customUserAgent?.startsWith(UNITY_PREFIX) == true
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import com.facebook.FacebookSdk
import java.util.concurrent.Callable
import java.util.concurrent.CountDownLatch
import java.util.concurrent.FutureTask

class LockOnGetVariable<T> {
  private var storedValue: T? = null
  private var initLatch: CountDownLatch? = null

  val value: T?
    get() {
      waitOnInit()
      return storedValue
    }

  constructor(value: T) {
    this.storedValue = value
  }

  constructor(callable: Callable<T>) {
    initLatch = CountDownLatch(1)
    FacebookSdk.getExecutor()
        .execute(
            FutureTask<Void> {
              try {
                storedValue = callable.call()
              } finally {
                initLatch?.countDown()
              }
              null
            })
  }

  private fun waitOnInit() {
    val latch = initLatch ?: return
    try {
      latch.await()
    } catch (ex: InterruptedException) {
      // ignore
    }
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.util.Log
import com.facebook.FacebookSdk
import com.facebook.LoggingBehavior

/**
 * 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.
 */
class Logger(behavior: LoggingBehavior, tag: String) {
  private val behavior: LoggingBehavior
  private val tag: String
  private var contents: StringBuilder

  var priority = Log.DEBUG
    set(value: Int) {
      Validate.oneOf(
          value, "value", Log.ASSERT, Log.DEBUG, Log.ERROR, Log.INFO, Log.VERBOSE, Log.WARN)
      priority = value
    }

  fun getContents(): String {
    return replaceStrings(contents.toString())
  }

  // Writes the accumulated contents, then clears contents to start again.
  fun log() {
    logString(contents.toString())
    contents = StringBuilder()
  }

  // Immediately logs a string, ignoring any accumulated contents, which are left unchanged.
  fun logString(string: String) {
    log(behavior, priority, tag, string)
  }

  fun append(stringBuilder: StringBuilder) {
    if (shouldLog()) {
      contents.append(stringBuilder)
    }
  }

  fun append(string: String) {
    if (shouldLog()) {
      contents.append(string)
    }
  }

  fun append(format: String, vararg args: Any) {
    if (shouldLog()) {
      contents.append(String.format(format, *args))
    }
  }

  fun appendKeyValue(key: String, value: Any) {
    append("  %s:\t%s\n", key, value)
  }

  private fun shouldLog(): Boolean {
    return FacebookSdk.isLoggingBehaviorEnabled(behavior)
  }

  companion object {
    const val LOG_TAG_BASE = "FacebookSDK."
    private val stringsToReplace = HashMap<String, String>()

    // Note that the mapping of replaced strings is never emptied, so it should be used only for
    // things that are not expected to be too numerous, such as access tokens.
    @Synchronized
    @JvmStatic
    fun registerStringToReplace(original: String, replace: String) {
      stringsToReplace[original] = replace
    }

    @Synchronized
    @JvmStatic
    fun registerAccessToken(accessToken: String) {
      if (!FacebookSdk.isLoggingBehaviorEnabled(LoggingBehavior.INCLUDE_ACCESS_TOKENS)) {
        registerStringToReplace(accessToken, "ACCESS_TOKEN_REMOVED")
      }
    }

    @JvmStatic
    fun log(behavior: LoggingBehavior, tag: String, string: String) {
      log(behavior, Log.DEBUG, tag, string)
    }

    @JvmStatic
    fun log(behavior: LoggingBehavior, tag: String, format: String, vararg args: Any) {
      if (FacebookSdk.isLoggingBehaviorEnabled(behavior)) {
        val string = String.format(format, *args)
        log(behavior, Log.DEBUG, tag, string)
      }
    }

    @JvmStatic
    fun log(
        behavior: LoggingBehavior,
        priority: Int,
        tag: String,
        format: String,
        vararg args: Any
    ) {
      if (FacebookSdk.isLoggingBehaviorEnabled(behavior)) {
        val string = String.format(format, *args)
        log(behavior, priority, tag, string)
      }
    }

    @JvmStatic
    fun log(behavior: LoggingBehavior, priority: Int, tag: String, string: String) {
      var tag = tag
      var string = string
      if (FacebookSdk.isLoggingBehaviorEnabled(behavior)) {
        string = replaceStrings(string)
        if (tag.startsWith(LOG_TAG_BASE) == false) {
          tag = LOG_TAG_BASE + tag
        }
        Log.println(priority, tag, string)

        // Developer errors warrant special treatment by printing out a stack trace, to make
        // both more noticeable, and let the source of the problem be more easily pinpointed.
        if (behavior == LoggingBehavior.DEVELOPER_ERRORS) {
          Exception().printStackTrace()
        }
      }
    }

    @Synchronized
    private fun replaceStrings(string: String): String {
      var string = string
      for ((key, value) in stringsToReplace) {
        string = string.replace(key, value)
      }
      return string
    }
  }

  init {
    this.behavior = behavior
    this.tag = LOG_TAG_BASE + Validate.notNullOrEmpty(tag, "tag")
    contents = StringBuilder()
  }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 * All rights reserved.
 *
 * This source code is licensed under the license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.internal

import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import com.facebook.FacebookContentProvider
import com.facebook.FacebookException
import com.facebook.FacebookSdk
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.net.URLEncoder
import java.util.UUID

/**
 * 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.
 *
 * This class works in conjunction with [com.facebook.FacebookContentProvider] to allow apps to
 * attach binary attachments (e.g., images) to native dialogs launched via the sdk.It stores
 * attachments in temporary files and allows the Facebook application to retrieve them via the
 * content provider.
 */
object NativeAppCallAttachmentStore {
  private val TAG = NativeAppCallAttachmentStore::class.java.name
  const val ATTACHMENTS_DIR_NAME = "com.facebook.NativeAppCallAttachmentStore.files"
  private var attachmentsDirectory: File? = null

  @JvmStatic
  fun createAttachment(callId: UUID, attachmentBitmap: Bitmap): Attachment {
    return Attachment(callId, attachmentBitmap, null)
  }

  @JvmStatic
  fun createAttachment(callId: UUID, attachmentUri: Uri): Attachment {
    return Attachment(callId, null, attachmentUri)
  }

  @Throws(IOException::class)
  private fun processAttachmentBitmap(bitmap: Bitmap, outputFile: File) {
    val outputStream = FileOutputStream(outputFile)
    try {
      bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
    } finally {
      Utility.closeQuietly(outputStream)
    }
  }

  @Throws(IOException::class)
  private fun processAttachmentFile(imageUri: Uri, isContentUri: Boolean, outputFile: File) {
    val outputStream = FileOutputStream(outputFile)
    try {
      val inputStream =
          if (!isContentUri) {
            FileInputStream(imageUri.path)
          } else {
            FacebookSdk.getApplicationContext().contentResolver.openInputStream(imageUri)
          }
      Utility.copyAndCloseInputStream(inputStream, outputStream)
    } finally {
      Utility.closeQuietly(outputStream)
    }
  }

  @Throws(FacebookException::class)
  @JvmStatic
  fun addAttachments(attachments: Collection<Attachment>?) {
    if (attachments == null || attachments.isEmpty()) {
      return
    }

    // If this is the first time we've been instantiated, clean up any existing attachments.
    if (attachmentsDirectory == null) {
      cleanupAllAttachments()
    }
    ensureAttachmentsDirectoryExists()
    val filesToCleanup: MutableList<File?> = ArrayList()
    try {
      for (attachment in attachments) {
        if (!attachment.shouldCreateFile) {
          continue
        }
        val file = getAttachmentFile(attachment.callId, attachment.attachmentName, true)
        if (file != null) {
          filesToCleanup.add(file)
          if (attachment.bitmap != null) {
            processAttachmentBitmap(attachment.bitmap, file)
          } else if (attachment.originalUri != null) {
            processAttachmentFile(attachment.originalUri, attachment.isContentUri, file)
          }
        }
      }
    } catch (exception: IOException) {
      Log.e(TAG, "Got unexpected exception:$exception")
      for (file in filesToCleanup) {
        try {
          file?.delete()
        } catch (e: Exception) {
          // Always try to delete other files.
        }
      }
      throw FacebookException(exception)
    }
  }

  /**
   * Removes any temporary files associated with a particular native app call.
   *
   * @param callId the unique ID of the call
   */
  @JvmStatic
  fun cleanupAttachmentsForCall(callId: UUID) {
    val dir = getAttachmentsDirectoryForCall(callId, false)
    dir?.deleteRecursively()
  }

  @Throws(FileNotFoundException::class)
  @JvmStatic
  fun openAttachment(callId: UUID?, attachmentName: String?): File? {
    if (Utility.isNullOrEmpty(attachmentName) || callId == null) {
      throw FileNotFoundException()
    }
    return try {
      getAttachmentFile(callId, attachmentName, false)
    } catch (e: IOException) {
      // We don't try to create the file, so we shouldn't get any IOExceptions. But if we do,
      // just act like the file wasn't found.
      throw FileNotFoundException()
    }
  }

  @Synchronized
  @JvmStatic
  fun getAttachmentsDirectory(): File? {
    if (attachmentsDirectory == null) {
      attachmentsDirectory =
          File(FacebookSdk.getApplicationContext().cacheDir, ATTACHMENTS_DIR_NAME)
    }
    return attachmentsDirectory
  }

  @JvmStatic
  fun ensureAttachmentsDirectoryExists(): File? {
    val dir = getAttachmentsDirectory()
    dir?.mkdirs()
    return dir
  }

  @JvmStatic
  fun getAttachmentsDirectoryForCall(callId: UUID, create: Boolean): File? {
    if (attachmentsDirectory == null) {
      return null
    }
    val dir = File(attachmentsDirectory, callId.toString())
    if (create && !dir.exists()) {
      dir.mkdirs()
    }
    return dir
  }

  @Throws(IOException::class)
  @JvmStatic
  fun getAttachmentFile(callId: UUID, attachmentName: String?, createDirs: Boolean): File? {
    val dir = getAttachmentsDirectoryForCall(callId, createDirs) ?: return null
    return try {
      File(dir, URLEncoder.encode(attachmentName, "UTF-8"))
    } catch (e: UnsupportedEncodingException) {
      null
    }
  }

  @JvmStatic
  fun cleanupAllAttachments() {
    // Attachments directory may or may not exist; we won't create it if not, since we are just
    // going to delete it.
    getAttachmentsDirectory()?.deleteRecursively()
  }

  class Attachment(val callId: UUID, val bitmap: Bitmap?, val originalUri: Uri?) {
    val attachmentUrl: String
    val attachmentName: String?
    var isContentUri = false
    var shouldCreateFile = false

    init {
      if (originalUri != null) {
        val scheme = originalUri.scheme
        if ("content".equals(scheme, ignoreCase = true)) {
          isContentUri = true
          shouldCreateFile =
              with(originalUri.authority) { this != null && !this.startsWith("media") }
        } else if ("file".equals(originalUri.scheme, ignoreCase = true)) {
          shouldCreateFile = true
        } else if (!Utility.isWebUri(originalUri)) {
          throw FacebookException("Unsupported scheme for media Uri : $scheme")
        }
      } else if (bitmap != null) {
        shouldCreateFile = true
      } else {
        throw FacebookException("Cannot share media without a bitmap or Uri set")
      }
      attachmentName = if (!shouldCreateFile) null else UUID.randomUUID().toString()
      attachmentUrl =
          if (!shouldCreateFile) {
            originalUri.toString()
          } else {
            FacebookContentProvider.getAttachmentUrl(
                FacebookSdk.getApplicationId(), callId, attachmentName)
          }
    }
  }
}